Secure Docker containers with Traefik reverse proxy and Authelia authentication

Advanced 45 min Apr 19, 2026 136 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up a production-grade security stack using Traefik v3 reverse proxy with SSL automation and Authelia for multi-factor authentication. This tutorial covers Docker hardening, LDAP integration, and container security monitoring.

Prerequisites

  • Root access to server
  • Domain name with DNS control
  • Basic Docker knowledge
  • Email account for SSL certificates

What this solves

This tutorial creates a secure container environment where Traefik handles SSL termination and routing while Authelia provides authentication with support for LDAP and OIDC providers. You get automatic SSL certificates, multi-factor authentication, and hardened Docker containers suitable for production workloads.

Step-by-step installation

Update system and install Docker with security hardening

Start by updating your system and installing Docker with security-focused configuration.

sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo dnf update -y
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Configure Docker daemon with security hardening

Create a secure Docker daemon configuration that enables user namespaces, restricts capabilities, and sets security options.

{
  "userns-remap": "default",
  "live-restore": true,
  "userland-proxy": false,
  "no-new-privileges": true,
  "seccomp-profile": "/etc/docker/seccomp.json",
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  },
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER
newgrp docker

Create project directory structure

Set up the directory structure for your secure container stack with proper permissions.

mkdir -p ~/secure-stack/{traefik,authelia,config,data,logs}
cd ~/secure-stack
chmod 755 ~/secure-stack
chmod 750 ~/secure-stack/{config,data,logs}

Create Docker network for secure communication

Create a custom Docker network that isolates your secure services and enables encrypted communication.

docker network create --driver bridge \
  --subnet=172.20.0.0/16 \
  --opt encrypted=true \
  --opt com.docker.network.bridge.name=secure-br0 \
  secure-network

Configure Traefik v3 with SSL automation

Set up Traefik as a reverse proxy with automatic SSL certificate generation and security headers.

global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  dashboard: true
  debug: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
    http:
      middlewares:
        - security-headers@file
      tls:
        certResolver: letsencrypt

serversTransport:
  insecureSkipVerify: false

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /certificates/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: secure-network
  file:
    filename: /config/middlewares.yml
    watch: true

log:
  level: INFO
  filePath: "/logs/traefik.log"
  format: json

accessLog:
  filePath: "/logs/access.log"
  format: json

Create Traefik security middlewares

Define security middlewares for headers, rate limiting, and authentication integration.

http:
  middlewares:
    security-headers:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
          - POST
          - DELETE
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        referrerPolicy: "same-origin"
        customRequestHeaders:
          X-Forwarded-Proto: "https"
        customResponseHeaders:
          X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive"
        sslRedirect: true
        sslTemporaryRedirect: true
        sslHost: "example.com"
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        forceSTSHeader: true
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true
        permissionsPolicy: "camera=(), microphone=(), geolocation=()"
    
    rate-limit:
      rateLimit:
        average: 100
        period: 1m
        burst: 50
    
    authelia:
      forwardAuth:
        address: "http://authelia:9091/api/verify?rd=https://auth.example.com"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"
          - "Remote-Name"
          - "Remote-Email"

Generate Authelia configuration

Create Authelia configuration with LDAP support, session security, and multi-factor authentication.

openssl rand -base64 32 > config/session_secret
openssl rand -base64 32 > config/storage_secret
openssl rand -base64 32 > config/jwt_secret
theme: auto
default_redirection_url: https://example.com
server:
  host: 0.0.0.0
  port: 9091
  asset_path: /config/assets/
  headers:
    csp_template: ""
  buffers:
    read: 4096
    write: 4096
  timeouts:
    read: 6s
    write: 6s
    idle: 30s

log:
  level: info
  format: text
  file_path: /logs/authelia.log
  keep_stdout: true

telemetry:
  metrics:
    enabled: true
    address: tcp://0.0.0.0:9959

totp:
  disable: false
  issuer: authelia.com
  algorithm: sha1
  digits: 6
  period: 30
  skew: 1
  secret_size: 32

webauthn:
  disable: false
  display_name: Authelia
  attestation_conveyance_preference: indirect
  user_verification: preferred
  timeout: 60s

ntp:
  address: "time.cloudflare.com:123"
  version: 3
  max_desync: 3s
  disable_startup_check: false
  disable_failure: false

authentication_backend:
  password_reset:
    disable: false
  refresh_interval: 5m
  ldap:
    implementation: custom
    url: ldap://openldap:389
    timeout: 5s
    start_tls: false
    tls:
      skip_verify: false
      minimum_version: TLS1.2
    base_dn: dc=example,dc=com
    username_attribute: uid
    additional_users_dn: ou=users
    users_filter: (&({username_attribute}={input})(objectClass=person))
    additional_groups_dn: ou=groups
    groups_filter: (&(member={dn})(objectClass=groupOfNames))
    group_name_attribute: cn
    mail_attribute: mail
    display_name_attribute: displayName
    user: cn=admin,dc=example,dc=com
    password: changeme123

session:
  name: authelia_session
  domain: example.com
  same_site: lax
  secret: file:///config/session_secret
  expiration: 1h
  inactivity: 5m
  remember_me_duration: 1M
  redis:
    host: redis
    port: 6379
    password: ""
    database_index: 0
    maximum_active_connections: 8
    minimum_idle_connections: 0

regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 5m

storage:
  encryption_key: file:///config/storage_secret
  local:
    path: /data/db.sqlite3

notifier:
  disable_startup_check: false
  smtp:
    host: smtp.gmail.com
    port: 587
    timeout: 5s
    username: admin@example.com
    password: changeme123
    sender: admin@example.com
    identifier: localhost
    subject: "[Authelia] {title}"
    startup_check_address: admin@example.com
    disable_require_tls: false
    disable_html_emails: false
    tls:
      skip_verify: false
      minimum_version: TLS1.2

identity_providers:
  oidc:
    hmac_secret: file:///config/jwt_secret
    issuer_private_key: |
      -----BEGIN RSA PRIVATE KEY-----
      # Generate with: openssl genrsa -out private.pem 4096
      -----END RSA PRIVATE KEY-----
    access_token_lifespan: 1h
    authorize_code_lifespan: 1m
    id_token_lifespan: 1h
    refresh_token_lifespan: 90m
    enable_client_debug_messages: false
    clients:
      - id: example-app
        description: Example Application
        secret: $pbkdf2-sha512$310000$example-hash
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://app.example.com/oauth/callback
        scopes:
          - openid
          - profile
          - email
          - groups
        response_types:
          - code
        grant_types:
          - authorization_code
        response_modes:
          - form_post
          - query
          - fragment

access_control:
  default_policy: deny
  networks:
    - name: internal
      networks:
        - 10.0.0.0/8
        - 172.16.0.0/12
        - 192.168.0.0/16
  rules:
    - domain: auth.example.com
      policy: bypass
    - domain: traefik.example.com
      policy: two_factor
    - domain: "*.example.com"
      policy: two_factor

Create Redis configuration for session storage

Configure Redis with security settings for storing Authelia sessions.

bind 127.0.0.1
port 6379
protected-mode yes
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo no
set-proc-title yes
proc-title-template "{title} {listen-addr} {server-mode}"
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
rdb-del-sync-files no
dir ./
maxmemory 256mb
maxmemory-policy allkeys-lru

Create Docker Compose configuration

Define the complete secure stack with proper networking, volumes, and security constraints.

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    ports:
      - "80:80"
      - "443:443"
    environment:
      - TRAEFIK_LOG_LEVEL=INFO
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/middlewares.yml:/config/middlewares.yml:ro
      - ./data/certificates:/certificates
      - ./logs:/logs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.rule=Host(traefik.example.com)"
      - "traefik.http.routers.traefik.middlewares=authelia@file"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.service=api@internal"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 9091
    environment:
      - TZ=UTC
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./authelia/configuration.yml:/config/configuration.yml:ro
      - ./config:/config:ro
      - ./data/authelia:/data
      - ./logs:/logs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authelia.entrypoints=websecure"
      - "traefik.http.routers.authelia.rule=Host(auth.example.com)"
      - "traefik.http.routers.authelia.tls=true"
      - "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
      - "traefik.http.routers.authelia.service=authelia"
      - "traefik.http.services.authelia.loadbalancer.server.port=9091"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 6379
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
      - ./data/redis:/data
    command: redis-server /usr/local/etc/redis/redis.conf
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - SETGID
      - SETUID
      - DAC_OVERRIDE

  example-app:
    image: nginx:alpine
    container_name: example-app
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 80
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./data/www:/usr/share/nginx/html:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.example-app.entrypoints=websecure"
      - "traefik.http.routers.example-app.rule=Host(app.example.com)"
      - "traefik.http.routers.example-app.middlewares=authelia@file"
      - "traefik.http.routers.example-app.tls=true"
      - "traefik.http.routers.example-app.tls.certresolver=letsencrypt"
    read_only: true
    tmpfs:
      - /var/cache/nginx
      - /var/run
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID
      - NET_BIND_SERVICE

networks:
  secure-network:
    external: true

volumes:
  certificates:
  authelia-data:
  redis-data:

Set proper file permissions and ownership

Configure secure file permissions for certificates and configuration files.

sudo chown -R $(id -u):$(id -g) ~/secure-stack
chmod 600 ~/secure-stack/config/*_secret
chmod 644 ~/secure-stack/traefik/traefik.yml
chmod 644 ~/secure-stack/authelia/configuration.yml
touch ~/secure-stack/data/certificates/acme.json
chmod 600 ~/secure-stack/data/certificates/acme.json
Never use chmod 777. It gives every user on the system full access to your files. Instead, use specific permissions: 600 for secrets, 644 for config files, and 755 for directories.

Create sample application content

Add a simple test page to verify the authentication flow works correctly.

mkdir -p data/www



    
    
    Secure Application
    


    

🔒 Secure Application

✅ Authentication successful - You are accessing a protected resource

This page is protected by Authelia authentication and served through Traefik reverse proxy with SSL.

Security Features Active:

  • SSL/TLS encryption via Let's Encrypt
  • Multi-factor authentication
  • Security headers protection
  • Rate limiting
  • Container security hardening

Timestamp:

Start the secure container stack

Launch all services and verify they start correctly with proper networking.

docker compose up -d
docker compose logs -f --tail=20

Configure DNS and SSL certificates

Set up DNS records and verify SSL certificate generation for your domains.

Note: Replace example.com with your actual domain and ensure DNS A records point to your server's IP address before proceeding.
# Check certificate generation
docker compose logs traefik | grep -i certificate

Verify ACME challenge

curl -I http://auth.example.com/.well-known/acme-challenge/test

Test SSL endpoint

curl -I https://auth.example.com

Set up container security monitoring

Configure monitoring for container security events and performance metrics.

#!/bin/bash

Container security monitoring script

LOG_FILE="/var/log/container-security.log" DATE=$(date '+%Y-%m-%d %H:%M:%S')

Check for privileged containers

echo "[$DATE] Checking for privileged containers..." >> $LOG_FILE docker ps --filter "label=privileged=true" --format "table {{.Names}}\t{{.Status}}" >> $LOG_FILE

Monitor container resource usage

echo "[$DATE] Container resource usage:" >> $LOG_FILE docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" >> $LOG_FILE

Check for containers running as root

echo "[$DATE] Checking containers running as root:" >> $LOG_FILE for container in $(docker ps -q); do user=$(docker exec $container whoami 2>/dev/null || echo "unknown") name=$(docker inspect --format '{{.Name}}' $container) if [ "$user" = "root" ]; then echo "WARNING: Container $name running as root user" >> $LOG_FILE fi done

Verify security options

echo "[$DATE] Security options check:" >> $LOG_FILE docker ps --format "table {{.Names}}" | tail -n +2 | while read container; do security_opts=$(docker inspect --format '{{.HostConfig.SecurityOpt}}' $container) echo "$container: $security_opts" >> $LOG_FILE done
chmod +x config/docker-security.sh
sudo mkdir -p /var/log
sudo touch /var/log/container-security.log
sudo chmod 640 /var/log/container-security.log

Configure automated security monitoring

Set up a systemd timer to run security monitoring checks every hour.

[Unit]
Description=Container Security Monitor
Wants=container-security.timer

[Service]
Type=oneshot
User=root
ExecStart=/bin/bash /home/$USER/secure-stack/config/docker-security.sh

[Install]
WantedBy=multi-user.target
[Unit]
Description=Run Container Security Monitor hourly
Requires=container-security.service

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable container-security.timer
sudo systemctl start container-security.timer

Verify your setup

Test the complete security stack to ensure all components are working correctly.

# Check all containers are running
docker compose ps

Verify Traefik dashboard (should redirect to Authelia)

curl -I https://traefik.example.com

Test Authelia authentication endpoint

curl -I https://auth.example.com

Check SSL certificates

openssl s_client -connect auth.example.com:443 -servername auth.example.com < /dev/null 2>/dev/null | openssl x509 -noout -text | grep -A2 "Validity"

Verify security headers

curl -I https://app.example.com | grep -E "(X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security)"

Check container security status

bash config/docker-security.sh tail -20 /var/log/container-security.log

Test authentication flow

curl -L https://app.example.com

Configure LDAP integration

If you have an existing LDAP server, you can integrate it with Authelia for centralized authentication. This section shows how to connect to common LDAP implementations.

Configure Active Directory LDAP

Update the Authelia configuration to connect to your Active Directory server.

authentication_backend:
  ldap:
    implementation: activedirectory
    url: ldap://dc1.company.com:389
    timeout: 5s
    start_tls: true
    tls:
      skip_verify: false
      minimum_version: TLS1.2
      server_name: dc1.company.com
    base_dn: dc=company,dc=com
    additional_users_dn: cn=users
    users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
    username_attribute: sAMAccountName
    mail_attribute: mail
    display_name_attribute: displayName
    additional_groups_dn: cn=groups
    groups_filter: (&(member={dn})(objectClass=group))
    group_name_attribute: cn
    user: cn=authelia,cn=users,dc=company,dc=com
    password: your-service-account-password

Configure OpenLDAP integration

For OpenLDAP servers, use this configuration format in your Authelia setup.

authentication_backend:
  ldap:
    implementation: custom
    url: ldap://ldap.company.com:389
    timeout: 5s
    start_tls: true
    tls:
      skip_verify: false
      minimum_version: TLS1.2
    base_dn: dc=company,dc=com
    username_attribute: uid
    additional_users_dn: ou=people
    users_filter: (&({username_attribute}={input})(objectClass=inetOrgPerson))
    additional_groups_dn: ou=groups
    groups_filter: (&(member={dn})(objectClass=groupOfNames))
    group_name_attribute: cn
    mail_attribute: mail
    display_name_attribute: cn
    user: cn=authelia,ou=service,dc=company,dc=com
    password: your-service-account-password

Set up OIDC provider integration

Configure Authelia as an OIDC provider for other applications that support OAuth2/OIDC authentication.

Generate OIDC signing keys

Create RSA key pairs for signing OIDC tokens securely.

# Generate RSA private key
openssl genrsa -out config/oidc-private-key.pem 4096

Extract public key

openssl rsa -in config/oidc-private-key.pem -pubout -out config/oidc-public-key.pem

Set secure permissions

chmod 600 config/oidc-private-key.pem chmod 644 config/oidc-public-key.pem

Configure OIDC client applications

Add client configurations for applications that will use Authelia for authentication.

identity_providers:
  oidc:
    hmac_secret: file:///config/jwt_secret
    issuer_private_key: file:///config/oidc-private-key.pem
    access_token_lifespan: 1h
    authorize_code_lifespan: 1m
    id_token_lifespan: 1h
    refresh_token_lifespan: 90m
    enable_client_debug_messages: false
    clients:
      - id: grafana
        description: Grafana Dashboard
        secret: $pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzO2aIlqeYcGqpv.U4nfRpv8lzSu4wJZVu.p6LsyIrBwAbvP5.7sW4i.Y6MKdmN7N2zA
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://grafana.example.com/login/generic_oauth
        scopes:
          - openid
          - profile
          - email
          - groups
        response_types:
          - code
        grant_types:
          - authorization_code
      
      - id: nextcloud
        description: Nextcloud
        secret: $pbkdf2-sha512$310000$example-hash-for-nextcloud
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://cloud.example.com/apps/oidc_login/oidc
        scopes:
          - openid
          - profile
          - email
        response_types:
          - code
        grant_types:
          - authorization_code

Implement advanced security policies

Configure granular access control rules and security policies for different user groups and applications.

Configure advanced access control rules

Set up sophisticated access control based on user groups, networks, and resources.

access_control:
  default_policy: deny
  networks:
    - name: internal
      networks:
        - 10.0.0.0/8
        - 172.16.0.0/12
        - 192.168.0.0/16
    - name: vpn
      networks:
        - 10.8.0.0/24
    - name: office
      networks:
        - 203.0.113.0/24
  
  rules:
    # Public endpoints
    - domain: auth.example.com
      policy: bypass
    
    # Admin interfaces require two-factor from office network
    - domain: 
        - traefik.example.com
        - grafana.example.com
      networks:
        - office
        - vpn
      subject:
        - "group:admins"
      policy: two_factor
    
    # Development resources for dev team
    - domain: 
        - dev.example.com
        - staging.example.com
      subject:
        - "group:developers"
        - "group:admins"
      policy: one_factor
    
    # Production resources require MFA
    - domain: 
        - app.example.com
        - api.example.com
      networks:
        - internal
        - office
        - vpn
      policy: two_factor
    
    # High-security admin resources
    - domain: vault.example.com
      networks:
        - office
      subject:
        - "group:security-admins"
      policy: two_factor
    
    # Default for all subdomains
    - domain: "*.example.com"
      policy: two_factor

Common issues

SymptomCauseFix
Traefik shows 404 for all domainsDNS not pointing to server or wrong domain in configVerify DNS A records and update domain names in configuration files
SSL certificates not generatingLet's Encrypt can't access HTTP challengeCheck port 80 is accessible and /.well-known/acme-challenge/ responds
Authelia login page not loadingContainer networking or wrong proxy configurationCheck docker network ls and verify all containers are on secure-network
LDAP authentication failingWrong credentials or network accessTest LDAP connectivity: ldapsearch -x -H ldap://server:389 -D "user" -W
Redis connection errorsRedis container not accessibleCheck Redis logs: docker compose logs redis
Permission denied on certificatesWrong file ownership or permissionsFix with chown $(id -u):$(id -g) and chmod 600 acme.json
High memory usage in containersNo resource limits setAdd memory limits to docker-compose.yml: mem_limit: 512m

Next steps

Running this in production?

Want this handled for you? Running this at scale adds a second layer of work: capacity planning, failover drills, cost control, and on-call. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.