Intro
I started using Docker Swarm in 2022 and am still very satisfied with it. I am currenyl using it as a one node swarm.
This post assumes you deployed Swarm with a Traefik reverse proxy as described on DockerSwarm.rocksi, that all services are deployed under the doomain stored in the DOMAIN
environment variable, and that the variable DOCKER_HOST
is set correctly.
I wanted to test authelia for protecting a web app to be deployed on a Docker Swarm, and I decided to test it on an existing Docker Swarm.
The idea is to deploy Authelia, and protect a newly deployed whoami service running the image traefik/whoami
.
This is a test to validate the use of Authelia, so it will not use best practices to be deployed in production:
-
it will use a docker volume to store the config consisting of:
- the list of users in a yaml file
- the authelia config, which we will edit by hand
- it will store session info in memory
- it will record notifications in the mounted volume rather than send an email
Deploy authelia
Authelia can be deployed with this file compose-authelia.yml
:
version: "3.8"
services:
authelia:
image: authelia/authelia
ports:
- 9091
volumes:
- authelia-config:/config
environment:
TZ: "Europe/Brussels"
AUTHELIA_STORAGE_ENCRYPTION_KEY: "I3noKDFBshUzefnf89yFef2juS"
AUTHELIA_SESSION_DOMAIN: "${DOMAIN?Variable not set}"
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.auth.rule=Host(`auth.${DOMAIN?Variable not set}`)
- traefik.http.routers.auth.entrypoints=http
- traefik.http.routers.auth.middlewares=https-redirect
- traefik.constraint-label=traefik-public
- traefik.http.services.auth.loadbalancer.server.port=9091
- traefik.docker.network=traefik-public
# for https
- traefik.http.routers.auths.entrypoints=https
- traefik.http.routers.auths.rule=Host(`auth.${DOMAIN?Variable not set}`)
# to generate certificates for this service, these labels are required:
- traefik.http.routers.auths.tls=true
- traefik.http.routers.auths.tls.certresolver=le
- traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/verify?rd=https%3A%2F%2Fauth.${DOMAIN?Variable not set}%2F
- traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true
- traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email
- traefik.http.middlewares.authelia-basic.forwardAuth.address=http://authelia:9091/api/verify?auth=basic
- traefik.http.middlewares.authelia-basic.forwardAuth.trustForwardHeader=true
- traefik.http.middlewares.authelia-basic.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email
networks:
- traefik-public
networks:
# Use the previously created public network "traefik-public", shared with other
traefik-public:
external: true
volumes:
authelia-config:
Deploying it with docker stack deploy -c compose-authelia.yml authelia
will fail, but will create all volumes and authelia config files.
We then need to edit the config file stored on the volume, commenting some sections and uncommenting their replacement:
-
remove
ldap
section-
add
file
section
-
add
-
remove
redis
section -
remove
smtp
notification-
add
filesystem
-
add
-
remove
mysql
storage-
add
local
-
add
Editing a file on a volume is a very bad idea in production, but in this exploration phase it’s a huge time saver. We can use the approach previously published here:
docker run --rm -it -v authelia_authelia-config:/config ubuntu bash
apt update && apt install vim -y
vim /config/configuration.yml
Stop the authelia container to have Docker Swarm restart it:
docker stop $(docker ps -f "name=authelia" -q)
Check that it was restarted as expected: you should get a different id from the previous step with this command:
docker ps -f "name=authelia" -q
Login with authelia/authelia
Authelia generates a default user authelia/authelia
that we can use to check we can login.
Accessing the url auth.${DOMAIN}
should display the Authelia login form, and authenticating with login authelia and password authelia should work. even if it asks to add a device for a second factor authentication, it means the first factor passed successfully.
Deploy the whoami service
Use this compose file:
version: '3'
services:
whoami:
# A container that exposes an API to show its IP address
image: traefik/whoami
ports:
- 80
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule=Host(`whoami.${DOMAIN?Variable not set}`)
- traefik.http.routers.whoami.entrypoints=http
- traefik.http.routers.whoami.middlewares=https-redirect
- traefik.constraint-label=traefik-public
- traefik.http.services.whoami.loadbalancer.server.port=80
- traefik.docker.network=traefik-public
# for https
- traefik.http.routers.whoamis.entrypoints=https
- traefik.http.routers.whoamis.rule=Host(`whoami.${DOMAIN?Variable not set}`)
# to generate certificates for this service, these labels are required:
- traefik.http.routers.whoamis.tls=true
- traefik.http.routers.whoamis.tls.certresolver=le
networks:
- traefik-public
networks:
# Use the previously created public network "traefik-public", shared with other
traefik-public:
external: true
After deployment with docker stack deploy -c compose-whoami.yml whoami
you should be able to access the service at
http://whoami.$DOMAIN
.
Protecting the whoami service
Protecting the whoami service is just a matter of adding this label to it
- traefik.http.routers.whoamis.middlewares=authelia@docker
If you prefer using HTTP Basic Authentication, use this label instead:
- traefik.http.routers.whoamis.middlewares=authelia-basic@docker
But with the default configuration generated by Authelia, no policy will match your request the the whoami service, and access will be denied. Edit the authelia config file as previously, and add a rule the whoami service domain. I first made a mistake setting the policy of single factor (wrong domain name):
rules:
- domain: 'whoami.example.com'
policy: one_factor
looking at logs with
docker service logs authelia_authelia
, we have
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="Mark 1FA authentication attempt made by user 'authelia'" method=POST path=/api/firstfactor remote_ip=XXX.XXX.XXX.XXX
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="Successful 1FA authentication attempt made by user 'authelia'" method=POST path=/api/firstfactor remote_ip=XXX.XXX.XXX.XXX
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="Check authorization of subject username=authelia groups=admins,dev ip=XXX.XXX.XXX.XXX and object https://whoami.$DOMAIN/ (method GET)."
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="No matching rule for subject username=authelia groups=admins,dev ip=XXX.XXX.XXX.XXX and url https://whoami.$DOMAIN/... Applying default policy."
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="Required level for the URL https://whoami.$DOMAIN/ is 3" method=POST path=/api/firstfactor remote_ip=XXX.XXX.XXX.XXX
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:13+02:00" level=debug msg="Redirection URL https://whoami.$DOMAIN/ is safe" method=POST path=/api/firstfactor remote_ip=XXX.XXX.XXX.XXX
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:14+02:00" level=debug msg="Check authorization of subject username=authelia groups=admins,dev ip=XXX.XXX.XXX.XXX and object https://whoami.$DOMAIN/ (method GET)."
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:14+02:00" level=debug msg="No matching rule for subject username=authelia groups=admins,dev ip=XXX.XXX.XXX.XXX and url h
ttps://whoami.$DOMAIN/... Applying default policy."
authelia_authelia.1.rl9qtuan54yq@vmi763847 | time="2022-08-29T14:25:14+02:00" level=info msg="Access to https://whoami.$DOMAIN/ is forbidden to user authelia" method=GET path=/api/verify remote_ip=XXX.XXX.XXX.XXX
The correct rule is
rules:
- domain: 'whoami.$DOMAIN'
policy: one_factor
Remember to restart the authelia container after the change as previously.
Adding users
As detailed in the Authelia documentation, the yaml file storing users has this format:
james: # this key is also the login
displayname: "James Dean"
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: james.dean@authelia.com
With Authelia running on your Swarm, you can generate th password hash, using the authelia config file like this:
docker exec -it $(docker ps -f "name=authelia" -q) authelia --config /config/configuration.yml hash-password "my_secret_password" | sed -e "s/.* //"
The password is passed as argument to the command, which is not ideal (can be read in the terminal, is stored in the shell history unless you take your precaution, eg by undefining the environment variableHISTFILE
).
After adding a new user, you will need to restart authelia (at v4.36.5, though auto reload is on its way).