Secure Docker containers with Traefik reverse proxy and Authelia authentication

Advanced 45 min Apr 19, 2026 719 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



    
    
    
    


    <div class="container">
        <h1>🔒 Secure Application</h1>
        <div class="status">
            ✅ Authentication successful - You are accessing a protected resource
        </div>
        <p>This page is protected by Authelia authentication and served through Traefik reverse proxy with SSL.</p>
        <h3>Security Features Active:</h3>
        <ul>
            <li>SSL/TLS encryption via Let's Encrypt</li>
            <li>Multi-factor authentication</li>
            <li>Security headers protection</li>
            <li>Rate limiting</li>
            <li>Container security hardening</li>
        </ul>
        <p><strong>Timestamp:</strong> <span></span></p>
    </div>
    

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.

Automated install script

Run this to automate the entire setup

Vous ne voulez pas gérer cela vous-même ?

Nous gérons l'infrastructure des entreprises qui dépendent de leur disponibilité. Entièrement infogéré, avec un interlocuteur fixe qui connaît votre environnement.

Vous avez un interlocuteur fixe qui connaît votre installation

À son bureau à Rotterdam 14:58 · joignable par message, sans formulaire de ticket