Featured image thumbnail for post 'Self-hosting SSO with Traefik (Part 1): Keycloak'

Self-hosting SSO with Traefik (Part 1): Keycloak

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:

The dream

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.

Overview of setup

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:

  • Single sign-on and sign-out for browser applications
  • SAML, OAuth 2.0, OpenID Connect
  • LDAP (needs to be federated with an external LDAP service). For setting up authentication with services that need LDAP, see part 3 of this guide series.

The benefits of Traefik (over other reverse proxy solutions)

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:

  • Native Container Support - Traefik integrates seamlessly with container orchestration platforms like Docker and Kubernetes. It can automatically discover new containers and adjust the routing accordingly. This makes Traefik an excellent choice for self-hosting or modern microservices architectures.
  • Configuration as Code - By combining the dynamic configuration capabilities of Traefik with a version-controlled static configuration, you can achieve a robust and replicable approach to managing and deploying your proxy configuration. This was hugely important to me, as I disliked how my Nginx Proxy Manager configuration was stored in a database and could not be exported/imported between deployments or included in my infrastructure's git repository.
  • Ease of Use - Traefik is designed to be easy to use and configure. It supports automatic service discovery and can adapt to changes in your infrastructure without manual intervention. Traefik also provides a dashboard that shows you the current active routes being handled.

Project structure

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

Setting up Traefik

Traefik static configuration

To get started with setting up Traefik, let's create a static configuration file. This file will accomplish a few things including:

  • Setting up SSL/TLS certificates (using Let's Encrypt with a Namecheap DNS challenge). There are many providers available to use instead of Namecheap.
  • Redirecting HTTP requests to HTTPS
  • Enabling the Traefik dashboard
  • Telling Traefik to use the Docker provider and read any additional config in the conf.d directory

conf/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

Traefik dynamic configuration

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

Docker configuration

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.

Setting up Keycloak

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).

Restricting users from services

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.

Enabling 2FA

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.

  1. Go to the Keycloak Administration Console

  2. Go to Authentication > Required actions and for Configure OTP toggle Set as default action to On.

    • This gives new users the ability to configure 2FA
  3. 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.

What next?

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:


If you found this post helpful, please share it around:


Comments