Homelab Single Sign-On & TLS

Aymen Furter
5 min readMay 1, 2020

--

Having TLS Connections to your self-hosted apps that work effortlessly, on any device without having to install a certificate or skip an error message is awesome. On a web-facing reverse proxy, this is usually quite easy to achieve. Not so much on LAN. It is also preferable to have apps not exposed openly & unprotected to the local network. In this article, I’m going to demonstrate how I solved these challenges on my docker-compose based, self-hosted system.

The solution I came up with looks like this:

What you will need:

  • An LDAP Directory (I use a Windows Server VM with AD deployed. You could also use freeipa or substitute keycloak with google as OIDC Provider)
  • A domain name (one of the available providers). For this article, I assume you’ll own example.com — I went with namecheap.com (There service works and is quite competitive, careful: You’ll have to spend at least 50$ to get access to the ACME API)
  • A DNS Server with a wildcard DNS entry to route *.example.com to your server (within LAN).

Keycloak

See below the docker-compose YAML File:

version: "3"services:
mysqlkc:
image: mysql:5.7
volumes:
- /opt/appdata/keycloak:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: keycloak
MYSQL_USER: keycloak
MYSQL_PASSWORD: replaceme
labels:
- 'traefik.enable=false'
keycloak:
image: quay.io/keycloak/keycloak:latest
environment:
DB_VENDOR: MYSQL
DB_ADDR: mysqlkc
DB_DATABASE: keycloak
DB_USER: keycloak
DB_PASSWORD: replaceme
KEYCLOAK_USER: replaceme
KEYCLOAK_PASSWORD: replaceme
PROXY_ADDRESS_FORWARDING: 'true'
ports:
- 8080:8080
depends_on:
- mysqlkc
networks:
- default
- traefik
labels:
- 'traefik.http.routers.auth.rule=Host(`auth.example.com`)'
- 'traefik.http.routers.auth.tls=true'
- 'traefik.http.routers.auth.entrypoints=websecure'
- 'traefik.http.services.auth.loadbalancer.server.port=8080'
- 'traefik.frontend.passHostHeader=true'
networks:
traefik:
external:
name: traefik

Ensure to have PROXY_ADDRESS_FORWARDING configured, to allow keycloak to work properly behind TLS-secured traefik as a reverse proxy.

After deploying the docker-compose file, you should be able to access Keycloak on port 8080 and configure the Active Directory as a User federation provider. The login information specified in KEYCLOAK_PASSWORD and KEYCLOAK_USERNAME.

Then add traefik as a new client. You can either set the Valid Redirect URI to * or add all your URLs as a list (which is recommended).

Also, change the access type to “confidential”.

In my case, I needed to add the “groups” client scope & set email/groups to optional for it to work properly.

Copy the client secret (tab “Credentials”) for your traefik configuration later.

Traefik

Below docker-compose file contains both the relevant commands/labels for Single Sign-on and also ACME involving traefik & traefik-forward-auth:

version: "3"services:
traefik:
image: "traefik:latest"
container_name: "traefik"
command:
- '--log.level=INFO'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--providers.docker'
- '--providers.docker.network=traefik'
- '--api'
- '--api.dashboard=true'
- '--certificatesresolvers.domaincheap.acme.email=youremail@example.com'
- '--certificatesresolvers.domaincheap.acme.dnschallenge=true'
- '--certificatesresolvers.domaincheap.acme.dnschallenge.provider=namecheap'
- '--certificatesResolvers.domaincheap.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53'
- '--certificatesresolvers.domaincheap.acme.storage=/letsencrypt/acme.json'
- '--providers.file=true'
- '--providers.file.filename=/etc/traefik/middlewares.toml'
environment:
- NAMECHEAP_API_KEY=your-api-key
- NAMECHEAP_API_USER=your-namecheap-account
ports:
- "443:443"
- "80:80"
volumes:
- "/opt/appdata/traefik/acme:/letsencrypt"
- "/opt/appdata/traefik/config/middlewares.toml:/etc/traefik/middlewares.toml"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
- default
- traefik
labels:
- 'traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)'
- 'traefik.http.routers.http-catchall.entrypoints=web'
- 'traefik.http.routers.http-catchall.middlewares=redirect-to-https'
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
- 'traefik.http.routers.wildcard-certs.tls.certresolver=domaincheap'
- 'traefik.http.routers.wildcard-certs.tls.domains[0].main=example.com'
- 'traefik.http.routers.wildcard-certs.tls.domains[0].sans=*.example.com'
- 'traefik.http.routers.traefik.rule=Host(`traefik.example.com`)'
- 'traefik.http.routers.traefik.tls=true'
- 'traefik.http.routers.traefik.entrypoints=websecure'
- 'traefik.http.routers.traefik.service=api@internal'
- 'traefik.http.routers.traefik.middlewares=middlewares-oauth@file'
oauth:
container_name: oauth
image: thomseddon/traefik-forward-auth:latest
restart: unless-stopped
networks:
- traefik
- default
security_opt:
- no-new-privileges:true
extra_hosts:
- "auth.example.com:192.168.1.2"
- "oauth.example.com:192.168.1.2"
environment:
- PROVIDERS_OIDC_CLIENT_ID=your-client-id
- PROVIDERS_OIDC_CLIENT_SECRET=your-oidc-client-secret
- PROVIDERS_OIDC_ISSUER_URL=https://auth.example.com/auth/realms/master
- SECRET=foobar
- COOKIE_DOMAIN=example.com
- INSECURE_COOKIE=false
- AUTH_HOST=oauth.example.com
- URL_PATH=/_oauth
- LOG_LEVEL=info
- LOG_FORMAT=text
- LIFETIME=2592000
- DEFAULT_PROVIDER=oidc
labels:
- "traefik.enable=true"
- "traefik.http.routers.oauth.entrypoints=websecure"
- "traefik.http.routers.oauth.rule=Host(`oauth.example.com`)"
- "traefik.http.routers.oauth.tls=true"
- "traefik.http.routers.oauth.service=oauth"
- "traefik.http.services.oauth.loadbalancer.server.port=4181"
- "traefik.http.routers.oauth.middlewares=middlewares-oauth@file"
networks:
traefik:
external:
name: traefik

I want my stuff to be always up-to-date and break if needed. That’s why I use the “:latest” tag in all my docker-compose files. If reliability is important in your use case it probably makes sense to use some other tag (e.g. in case of traefik you could use v2.2).

Single Sign-On

You’ll need to add a local middleware toml file (in my case “/opt/appdata/traefik/config/middlewares.toml”) to add the forward auth middleware:

[http.middlewares]
[http.middlewares.middlewares-oauth]
[http.middlewares.middlewares-oauth.forwardAuth]
address = "http://oauth:4181"
trustForwardHeader = true
authResponseHeaders = ["X-Forwarded-User"]

ACME (Auto-issuing & renewal of TLS certificates)

You’ll need to set up your DNS Challange API call (this is different per provider). The ACME DNS Challange is a perfect fit for local-only, self-hosted systems, as these typically are not exposed to the web (which is a pre-requisite for the more common HTTP-01 method). For Namecheap I was able to generate the API Key within my account:

Testing

Now open traefik.example.com (Assuming you have configured your local DNS) in your browser. You’ll be redirected to the keycloak login page:

After a successful login, you’ll be redirected to your app:

You can add as many application as you like by adding the required labels:

---
version: "2"
services:
co2:
image: afurter/co2-docker
privileged: true
container_name: co2
volumes:
- /dev/hidraw0:/dev/hidraw0
- /opt/appdata/co2:/var/local/monitor
restart: unless-stopped
networks:
- default
- traefik
labels:
- 'traefik.http.routers.cotwo.rule=Host(`co2.example.com`)'
- 'traefik.http.routers.cotwo.tls=true'
- 'traefik.http.routers.cotwo.entrypoints=websecure'
- 'traefik.http.services.cotwo.loadbalancer.server.port=80'
- 'traefik.http.routers.cotwo.middlewares=middlewares-oauth@file'
networks:
traefik:
external:
name: traefik

In this sample above, I deployed Walter Reiner’s Office Weather application which monitors my CO2 levels in my flat.

Credits / Helpful Sources:

--

--

Aymen Furter

I am a Cloud Solution Architect working for Microsoft. The views expressed on this site are mine alone and do not necessarily reflect the views of my employer.