by , last updated January 11, 2023, in

Authelia & lldap: Authentication, SSO, User Management & Password Reset for Home Networks

This article explains how to set up a simple but modern user management and authentication system for services on your internal home network. The solution supports important security features like two-factor authentication and single sign-on, and only requires minimal maintenance due to self-service password reset. This article is part of my series on home automation that shows how to install, configure, and run a home server with (dockerized or virtualized) services such as Home Assistant and ownCloud.

Preparation

I’m assuming that you’ve set up Docker and the Caddy container as described in the previous articles in this series.

lldap: User Directory

What is lldap?

Light LDAP (lldap) is a lightweight LDAP authentication server with a friendly web UI that aims to make user management easy. lldap is simple to set up, comes with “opinionated basic defaults”, and targets self-hosting servers.

Dockerized lldap Directory Structure

This is what the directory structure will look like when we’re done:

rpool/
 └── encrypted/
     └── docker/
         └── lldap/
             ├── data/
                 └── lldap_config.toml
             ├── secrets/
                 ├── JWT_SECRET
                 └── LDAP_USER_PASS
             ├── container-vars.env
             └── docker-compose.yml

We’re placing the configuration on the encrypted ZFS dataset (rpool/encrypted) because it contains secrets such as private keys and passwords.

Create the directories:

mkdir -p /rpool/encrypted/docker/lldap/data
mkdir -p /rpool/encrypted/docker/lldap/secrets

lldap Docker Compose File

Create docker-compose.yml with the following content:

version: "3.9"

services:

  lldap:
    container_name: lldap
    hostname: lldap
    image: nitnelave/lldap:stable
    restart: unless-stopped
    networks:
      - caddy_caddynet
    expose:
      - 3890   # LDAP
      - 17170  # Web UI
    env_file:
      - container-vars.env
    volumes:
      - ./data:/data
      - ./secrets:/secrets

networks:
  caddy_caddynet:
    external: true

lldap container-vars.env File

Create container-vars.env with the following content:

LLDAP_LDAP_BASE_DN=dc=example,dc=com               # replace with your domain
UID=0                                              # run as root
GID=0                                              # run as root

# Secrets: lldap reads them from the specified files.
# This way, the secrets are not part of any process' environment.
LLDAP_JWT_SECRET_FILE=/secrets/JWT_SECRET
LLDAP_LDAP_USER_PASS_FILE=/secrets/LDAP_USER_PASS

lldap Secrets Files

Generate random alphanumeric strings and store them in individual files in the secrets directory:

cd /rpool/encrypted/docker/lldap/
tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/JWT_SECRET
tr -cd '[:alnum:]' < /dev/urandom | fold -w "20" | head -n 1 > ./secrets/LDAP_USER_PASS

lldap TOML Configuration File

We’re using a customized version of lldap’s configuration template. Create data/lldap_config.toml with the following content:

database_url = "sqlite:///data/users.db?mode=rwc"
key_file = "/data/private_key"
enable_password_reset = false

Start the lldap Container

Navigate into the directory with docker-compose.yml and run:

docker compose up -d

Inspect the container logs for errors with the command docker compose logs --tail 30 --timestamps.

lldap Let’s Encrypt Certificate via Caddy

Caddyfile

Add the following to Caddyfile (details):

lldap.{$MY_DOMAIN} {
	reverse_proxy lldap:17170
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
	}
}
DNS A Record

Add the following A record to your DNS domain:

lldap.home.yourdomain.com 192.168.0.4     # replace with your Docker host's IP address

Try to resolve the name on a machine in your network (e.g., nslookup lldap.home.yourdomain.com). If that fails, you might need to work around DNS rebind protection in your router.

Reload Caddy’s Configuration

Instruct Caddy to reload its configuration by running:

docker exec -w /etc/caddy caddy caddy reload

You should now be able to access the lldap web interface at https://lldap.home.yourdomain.com without getting a certificate warning from your browser.

lldap User Configuration via UI

In your browser, open https://lldap.home.yourdomain.com. Log on with the user admin and the password stored in the file /secrets/LDAP_USER_PASS. Create a new admin user with your own name (e.g., anne) by clicking Create a user and filling out the form. Add this personal user account to the group lldap_admin.

Free Sendgrid Account To Send Email From Your Server

A service like Authelia needs to send emails, e.g., for password resets. Setting up your own SMTP server for the task is not a very good idea: those emails would be marked as spam by any self-respecting email application.
Instead, create a free account at Sendgrid (or an alternative service). At the time of writing, Sendgrid’s free plan allows you to send 100 emails per day, which should be more than enough for the occasional message from your home server.
When you set up your Sendgrid account, make sure to authenticate your sending address – or, better yet, your sending domain. The sending address/domain is configured via the environment variable AUTHELIA_NOTIFIER_SMTP_SENDER (see below).

Redis: Persistent Session Storage

What is Redis?

Redis is an in-memory data store that can be used by Authelia as a session storage provider. By using an external service for session data, Authelia can be restarted without users having to re-authenticate.

Dockerized Redis Directory Structure

This is what the directory structure will look like when we’re done:

rpool/
 └── encrypted/
     └── docker/
         └── redis/
             ├── data/
             └── docker-compose.yml

Create the directory:

mkdir -p /rpool/encrypted/docker/redis/data

Redis Docker Compose File

Create docker-compose.yml with the following content:

version: "3.9"

services:

  redis:
    container_name: redis
    hostname: redis
    image: redis:latest
    restart: unless-stopped
    networks:
      - caddy_caddynet
    expose:
      - 6379
    volumes:
      - ./data:/data

networks:
  caddy_caddynet:
    external: true

Start the Redis Container

Navigate into the directory with docker-compose.yml and run:

docker compose up -d

Inspect the container logs for errors with the command docker compose logs --tail 30 --timestamps. Ignore the warning that recommends setting vm.overcommit_memory = 1 on the host.

Authelia: User Authentication & Authorization

What is Authelia?

Authelia is a lightweight identity and access management (IAM) server. It provides single sign-on (SSO) and multi-factor authentication (MFA) via a friendly web portal. Authelia is typically used in conjunction with reverse proxies such as Caddy.

Dockerized Authelia Directory Structure

This is what the directory structure will look like when we’re done:

rpool/
 └── encrypted/
     └── docker/
         └── authelia/
             ├── config/
                 └── configuration.yml
             ├── secrets/
                 ├── JWT_SECRET
                 ├── SESSION_SECRET
                 ├── STORAGE_ENCRYPTION_KEY
                 ├── NOTIFIER_SMTP_PASSWORD
                 └── AUTHENTICATION_BACKEND_LDAP_PASSWORD
             ├── container-vars.env
             └── docker-compose.yml

We’re placing the configuration on the encrypted ZFS dataset (rpool/encrypted) because it contains secrets such as private keys and passwords.

Create the directories:

mkdir -p /rpool/encrypted/docker/authelia/config
mkdir -p /rpool/encrypted/docker/authelia/secrets

Authelia Docker Compose File

Create docker-compose.yml with the following content:

version: "3.9"

services:

  authelia:
    container_name: authelia
    hostname: authelia
    image: authelia/authelia:4
    restart: unless-stopped
    networks:
      - caddy_caddynet
    expose:
      - 9091
    env_file:
      - container-vars.env
    volumes:
      - ./config:/config
      - ./secrets:/secrets

networks:
  caddy_caddynet:
    external: true

Authelia container-vars.env File

Create container-vars.env with the following content:

AUTHELIA_SESSION_DOMAIN=home.yourdomain.com                  # replace with your domain
AUTHELIA_TOTP_ISSUER=home.yourdomain.com                     # replace with your domain
AUTHELIA_WEBAUTHN_DISPLAY_NAME=home                          # replace with name of your home
AUTHELIA_NOTIFIER_SMTP_HOST=smtp.sendgrid.net                # replace with your SMTP server's FQDN
AUTHELIA_NOTIFIER_SMTP_PORT=587                              # replace with your SMTP server's port
AUTHELIA_NOTIFIER_SMTP_USERNAME=apikey                       # replace with your SMTP server's username
AUTHELIA_NOTIFIER_SMTP_SENDER="Your Name <[email protected]>"  # replace with your name/email
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL=ldap://lldap:3890
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN=dc=example,dc=com  # must be identical to what is configured in lldap
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER=uid=admin,ou=people,dc=example,dc=com  # must be the DN of your admin configured in lldap

# Secrets: Authelia reads them from the specified files.
# This way, the secrets are not part of any process' environment.
AUTHELIA_JWT_SECRET_FILE=/secrets/JWT_SECRET
AUTHELIA_SESSION_SECRET_FILE=/secrets/SESSION_SECRET
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/secrets/STORAGE_ENCRYPTION_KEY
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/NOTIFIER_SMTP_PASSWORD
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE=/secrets/AUTHENTICATION_BACKEND_LDAP_PASSWORD

Authelia Secrets Files

Generate random alphanumeric strings and store them in individual files in the secrets directory:

cd /rpool/encrypted/docker/authelia/
tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/JWT_SECRET
tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/SESSION_SECRET
tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/STORAGE_ENCRYPTION_KEY

Create the file /secrets/NOTIFIER_SMTP_PASSWORD and paste the password of your SMTP server’s user into it.

Create the file /secrets/AUTHENTICATION_BACKEND_LDAP_PASSWORD and paste the lldap admin password from lldap/secrets/LDAP_USER_PASS into it.

Authelia YAML Configuration File

We’re using a customized version of Authelia’s configuration template. Create config/configuration.yml with the following content:

---
theme: auto
default_2fa_method: totp
server:
  host: 0.0.0.0
  port: 9091
log:
  level: debug
totp:
  disable: false
  algorithm: sha1
  digits: 6
  period: 30
  skew: 1
  secret_size: 32
webauthn:
  disable: false
  timeout: 60s
  attestation_conveyance_preference: indirect
  user_verification: preferred
authentication_backend:
  password_reset:
    disable: false
  refresh_interval: 5m
  ldap:
    implementation: custom
    timeout: 5s
    start_tls: false
    username_attribute: uid
    additional_users_dn: ou=people
    users_filter: "(&({username_attribute}={input})(objectClass=person))"
    additional_groups_dn: ou=groups
    groups_filter: "(member={dn})"
    group_name_attribute: cn
    mail_attribute: mail
    display_name_attribute: displayName
password_policy:
  standard:
    enabled: false
    min_length: 8
    max_length: 0
    require_uppercase: true
    require_lowercase: true
    require_number: true
    require_special: false
access_control:
  default_policy: two_factor
session:
  name: authelia_session
  same_site: lax
  expiration: 1h
  inactivity: 5m
  remember_me_duration: 1M
  redis:
    host: redis
    port: 6379
regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 5m
storage:
  local:
    path: /config/db.sqlite3
notifier:
  disable_startup_check: false
...

Start the Authelia Container

Navigate into the directory with docker-compose.yml and run:

docker compose up -d

Inspect the container logs for errors with the command docker compose logs --tail 30 --timestamps.

Authelia Let’s Encrypt Certificate via Caddy

Caddyfile

Add the following to Caddyfile (details):

auth.{$MY_DOMAIN} {
	reverse_proxy authelia:9091
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
	}
}
DNS A Record

Add the following A record to your DNS domain:

auth.home.yourdomain.com 192.168.0.4     # replace with your Docker host's IP address

Try to resolve the name on a machine in your network (e.g., nslookup auth.home.yourdomain.com). If that fails, you might need to work around DNS rebind protection in your router.

Reload Caddy’s Configuration

Instruct Caddy to reload its configuration by running:

docker exec -w /etc/caddy caddy caddy reload

You should now be able to access the Authelia web interface at https://authelia.home.yourdomain.com without getting a certificate warning from your browser.

Add a Protected Endpoint to Authelia via Caddy

To protect an endpoint with Authelia’s authentication (and optionally enable SSO), add a forward_auth directive to the endpoint’s Caddy configuration. The configuration for the whoami container we used as an example in the previous article looks as follows with the added forward_auth directive:

whoami.{$MY_DOMAIN} {
	forward_auth authelia:9091 {
		uri /api/verify?rd=https://auth.{$MY_DOMAIN}/
		copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
	}
	reverse_proxy whoami:80
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
	}
}

Authelia & OpenID Connect

Authelia’s support for OpenID Connect (OIDC) is marked as “beta”. It’s not yet feature-complete but should work well enough in most scenarios. Authelia’s OpenID Connect roadmap documents the progress in detail. Authelia’s OpenID Connect documentation has example configurations for popular services such as Nextcloud.

See this follow-up article for a detailed description of how to set up OpenID Connect authentication to Authelia in ownCloud Infinite Scale.

Changelog

2022-12-17

  • Added Redis as persistent Authelia session storage so that Authelia restarts don’t force users to re-authenticate.

Previous Article Automatic HTTPS Certificates for Services on Internal Home Network
Next Article authentik: Authentication, SSO, User Management & Password Reset for Home Networks