How to use Docker and Traefik to get started with self-hosting single sign-on with Keycloak.
Joey Miller • Posted July 06, 2023
This guide is the first part in a multi-part series of guides:
There are plenty of great services to self-host, including Nextcloud, and Tandoor Recipes. If you've ever tried self-hosting more than a few services you'll understand the frustration of remembering many different passwords and continuously having to log in. After doing some research, I realized my homelab needed "single sign-on" (SSO). SSO is an authentication method (typically viewed as an enterprise feature) that allows secure authentication with many services using "just one set of credentials". Using SSO I would be able to achieve my dream of needing only a single-login event to access all of my services.
There are many tools that we can use for SSO, such as Authelia, Authentik, or Keycloak.
Although some of the aforementioned SSO tools may be easier to set up, I decided to go with Keycloak. Keycloak is an enterprise-level tool that is supported by Redhat. Using Keycloak will give us a lot of flexibility, and ticks the boxes for acceptable memory usage, theme-ability, and multi-factor authentication support.
Keycloak supports:
Traefik is an open-source reverse proxy and load balancer designed for containerized environments (such as Docker or Kubernetes).
I have earlier guides that cover a similar setup using Nginx Proxy Manager - there is some overlap. For self-hosting, the benefits of using Traefik become clear:
git
repository.When we are done setting up, the project will be structured as follows:
├── conf
│ └── traefik
│ ├── conf.d
│ │ ├── traefik_dynamic_default.yml
│ └── traefik.yml
└── docker-compose.yml
To get started with setting up Traefik, let's create a static configuration file. This file will accomplish a few things including:
HTTP
requests to HTTPS
conf.d
directoryconf/traefik/traefik.yml
entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ':443'
http:
tls:
certresolver: lets-encrypt
domains:
- main: example.com
sans:
- '*.example.com'
middlewares:
- default@file
api:
dashboard: true
certificatesResolvers:
lets-encrypt:
acme:
email: [email protected]
storage: acme.json
dnsChallenge:
provider: namecheap
providers:
docker:
exposedByDefault: false
file:
directory: conf.d
Then, let's create a dynamic configuration file that sets some sane and secure defaults. This configuration is inspired by Benjamin Rancourt's Traefik configuration, with a few minor changes such as the prevention of search engine indexing (a good idea when self-hosting).
conf/traefik/conf.d/traefik_dynamic_default.yml
http:
middlewares:
# Inspired by
# https://www.benjaminrancourt.ca/a-complete-traefik-configuration/
# Recommended default middleware for most of the services
default:
chain:
middlewares:
- default-security-headers
- gzip
# Add automatically some security headers
default-security-headers:
headers:
browserXssFilter: true # X-XSS-Protection=1; mode=block
contentTypeNosniff: true # X-Content-Type-Options=nosniff
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow"
forceSTSHeader: true # Add the Strict-Transport-Security header even when the connection is HTTP
frameDeny: true # X-Frame-Options=deny
referrerPolicy: "strict-origin-when-cross-origin"
stsIncludeSubdomains: true # Add includeSubdomains to the Strict-Transport-Security header
stsPreload: true # Add preload flag appended to the Strict-Transport-Security header
stsSeconds: 63072000 # Set the max-age of the Strict-Transport-Security header (63072000 = 2 years)
# Enable GZIP compression
gzip:
compress: {}
tls:
options:
default:
sniStrict: true
Add the following to the services:
section of your docker-compose.yml
file:
traefik:
image: traefik:latest
ports:
- '80:80/tcp'
- '443:443/tcp'
environment:
NAMECHEAP_API_USER: '< username >'
NAMECHEAP_API_KEY: '< key >'
volumes:
- ./conf/traefik/traefik.yml:/traefik.yml
- ./conf/traefik/conf.d:/conf.d
- ./data/traefik/acme.json:/acme.json
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.enable=true"
- "traefik.http.routers.route-reverseproxy.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.route-reverseproxy.service=api@internal"
restart: unless-stopped
Note: Exposing
/var/run/docker.sock
can be dangerous. Consider using (docker-socket-proxy
)[https://github.com/Tecnativa/docker-socket-proxy] or running a rootless Docker installation.
Make sure to replace example.com
with your domain name in all the above config files.
After running docker compose up
from the directory that contains docker-compose.yml
you will be able to access the Traefik dashboard at https://traefik.example.com
. Initially, you may experience some SSL errors for a few minutes until Traefik retrieves the certificates from Lets Encrypt.
To get started with Keycloak, add the following keycloak
entry to the services:
section of your docker-compose.yml
file:
keycloak:
# internal: keycloak on port 8080
image: quay.io/keycloak/keycloak:latest
command: start
environment:
KC_HOSTNAME: 'auth.example.com'
KC_PROXY: 'edge'
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin'
volumes:
- ./data/keycloak:/opt/keycloak/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.route-auth.rule=Host(`auth.example.com`)"
- "traefik.http.services.route-auth.loadbalancer.server.port=8080"
# Redirect '/' to '/admin'
- "traefik.http.middlewares.custom-redirect.redirectregex.regex=^https:\\/\\/([^\\/]+)\\/?$$"
- "traefik.http.middlewares.custom-redirect.redirectregex.replacement=https://$$1/admin"
- "traefik.http.routers.route-auth.middlewares=default@file,custom-redirect"
restart: unless-stopped
Note: The above example will have Keycloak running with the built-in H2 database. It is recommended in production to use an external database (i.e. MySQL, PostgreSQL, etc).
The labels:
section in the above snippet is what configures Traefik to serve the Keycloak Administration Console at https://auth.example.com
via Traefik (after re-running docker compose up
).
We don't always want every user to be able to access every service. For example, you may want to give a friend access to your recipe application but not to the Pihole admin console.
If you have a compliant OIDC Client Application, setting Client authentication
and Authorization
to ON
will be enough to access the Authorization
tab for the client. Then you will be able to create a Group-based policy
from the Policies
sub-tab.
Unfortunately, this isn't always suitable - such as if the client uses SAML for authentication/authorization. In this case, another way to handle this in Keycloak (without needing any extensions) is to duplicate the default browser flow and add a condition that denies access if the user doesn't have the required role.
Note: This method requires a separate browser flow for each unique set of roles you would like to mandate for users of clients/services.
For more details see the answers by @Stuck and @heilerich on StackOverflow.
To increase the security of your authentication process, Keycloak allows enabling two-factor authentication (2FA) for users. This requires users to provide a valid one-time-pass (OTP) from an authenticator app on their smartphone.
Go to the Keycloak Administration Console
Go to Authentication > Required actions
and for Configure OTP
toggle Set as default action
to On
.
Go to the Users
page. By clicking on each existing user, add Configure OTP
to Required user actions
from the User details > Details
tab.
After these steps - on the next login users will be required to set up their authenticator for 2FA.
We've successfully set up a basic SSO implementation with Keycloak.
If all the services you are using are capable of authentication via OAuth, SAML, or Keycloak - the journey ends here for you. This implementation will be sufficient.
If you have other services that expect HTTP Header Auth or manage their own login flow (i.e. via LDAP) - continue reading. This guide continues in:
Tags
If you found this post helpful, please share it around: