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
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
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.
# 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
| Symptom | Cause | Fix |
|---|---|---|
| Traefik shows 404 for all domains | DNS not pointing to server or wrong domain in config | Verify DNS A records and update domain names in configuration files |
| SSL certificates not generating | Let's Encrypt can't access HTTP challenge | Check port 80 is accessible and /.well-known/acme-challenge/ responds |
| Authelia login page not loading | Container networking or wrong proxy configuration | Check docker network ls and verify all containers are on secure-network |
| LDAP authentication failing | Wrong credentials or network access | Test LDAP connectivity: ldapsearch -x -H ldap://server:389 -D "user" -W |
| Redis connection errors | Redis container not accessible | Check Redis logs: docker compose logs redis |
| Permission denied on certificates | Wrong file ownership or permissions | Fix with chown $(id -u):$(id -g) and chmod 600 acme.json |
| High memory usage in containers | No resource limits set | Add memory limits to docker-compose.yml: mem_limit: 512m |
Next steps
- Monitor Docker containers with Prometheus and Grafana using cAdvisor for comprehensive metrics collection
- Implement container security monitoring with Falco runtime detection
- Setup Kubernetes Ingress NGINX with cert-manager for automated SSL certificates
- Setup centralized log aggregation with Elasticsearch 8, Logstash 8, and Kibana 8 (ELK Stack)
- Set up Grafana Enterprise SSO authentication with LDAP, SAML, and OAuth2 integration
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Usage function
usage() {
echo "Usage: $0 <domain> <email>"
echo "Example: $0 example.com admin@example.com"
exit 1
}
# Cleanup function for rollback
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
cd /
rm -rf ~/secure-stack 2>/dev/null || true
docker network rm secure-network 2>/dev/null || true
exit 1
}
# Set trap for cleanup on error
trap cleanup ERR
# Check arguments
if [ $# -ne 2 ]; then
usage
fi
DOMAIN="$1"
EMAIL="$2"
# Validate email format
if ! echo "$EMAIL" | grep -qE "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"; then
echo -e "${RED}Invalid email format${NC}"
exit 1
fi
# Check if running as root or with sudo
if [ $EUID -eq 0 ]; then
SUDO=""
else
SUDO="sudo"
if ! command -v sudo >/dev/null 2>&1; then
echo -e "${RED}This script requires sudo privileges${NC}"
exit 1
fi
fi
echo -e "${GREEN}Installing Secure Docker Stack with Traefik and Authelia${NC}"
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
DOCKER_REPO="https://download.docker.com/linux/$ID"
GPG_KEY_CMD="curl -fsSL https://download.docker.com/linux/$ID/gpg | $SUDO gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"
REPO_CMD="echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] $DOCKER_REPO \$(lsb_release -cs) stable\" | $SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
DOCKER_REPO="https://download.docker.com/linux/centos/docker-ce.repo"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
DOCKER_REPO="https://download.docker.com/linux/centos/docker-ce.repo"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${YELLOW}[1/8] Updating system and installing prerequisites${NC}"
$SUDO $PKG_UPDATE
case "$PKG_MGR" in
apt)
$SUDO $PKG_INSTALL ca-certificates curl gnupg lsb-release
;;
*)
$SUDO $PKG_INSTALL ca-certificates curl gnupg2
;;
esac
echo -e "${YELLOW}[2/8] Installing Docker${NC}"
case "$PKG_MGR" in
apt)
eval "$GPG_KEY_CMD"
eval "$REPO_CMD"
$SUDO apt update
;;
*)
$SUDO dnf config-manager --add-repo $DOCKER_REPO || $SUDO yum-config-manager --add-repo $DOCKER_REPO
;;
esac
$SUDO $PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-compose-plugin
echo -e "${YELLOW}[3/8] Configuring Docker daemon with security hardening${NC}"
$SUDO mkdir -p /etc/docker
$SUDO tee /etc/docker/daemon.json > /dev/null <<EOF
{
"userns-remap": "default",
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
$SUDO systemctl enable docker
$SUDO systemctl start docker
$SUDO usermod -aG docker $USER
echo -e "${YELLOW}[4/8] Creating project directory structure${NC}"
mkdir -p ~/secure-stack/{traefik,authelia,config,data,logs,certificates}
cd ~/secure-stack
chmod 755 ~/secure-stack
chmod 750 ~/secure-stack/{config,data,logs,certificates}
echo -e "${YELLOW}[5/8] Creating Docker network${NC}"
docker network create --driver bridge \
--subnet=172.20.0.0/16 \
--opt com.docker.network.bridge.name=secure-br0 \
secure-network
echo -e "${YELLOW}[6/8] Configuring Traefik${NC}"
tee ~/secure-stack/traefik/traefik.yml > /dev/null <<EOF
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
certificatesResolvers:
letsencrypt:
acme:
email: $EMAIL
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
EOF
tee ~/secure-stack/config/middlewares.yml > /dev/null <<EOF
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: "none,noarchive,nosnippet,notranslate,noimageindex"
X-Forwarded-Proto: "https"
sslProxyHeaders:
X-Forwarded-Proto: "https"
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
authelia:
forwardAuth:
address: "http://authelia:9091/api/verify?rd=https://auth.$DOMAIN/"
trustForwardHeader: true
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Name"
- "Remote-Email"
EOF
echo -e "${YELLOW}[7/8] Configuring Authelia${NC}"
mkdir -p ~/secure-stack/authelia
tee ~/secure-stack/authelia/configuration.yml > /dev/null <<EOF
server:
host: 0.0.0.0
port: 9091
log:
level: info
format: json
default_redirection_url: https://auth.$DOMAIN
totp:
issuer: $DOMAIN
authentication_backend:
file:
path: /config/users.yml
access_control:
default_policy: deny
rules:
- domain: "auth.$DOMAIN"
policy: bypass
- domain: "*.$DOMAIN"
policy: two_factor
session:
name: authelia_session
domain: $DOMAIN
expiration: 1h
inactivity: 5m
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
EOF
# Create a default user (admin/admin - should be changed in production)
HASHED_PASSWORD='$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NZYK.Y.5yaE.'
tee ~/secure-stack/authelia/users.yml > /dev/null <<EOF
users:
admin:
displayname: "Admin User"
password: "$HASHED_PASSWORD"
email: $EMAIL
groups:
- admins
- dev
EOF
echo -e "${YELLOW}[8/8] Creating Docker Compose configuration${NC}"
tee ~/secure-stack/docker-compose.yml > /dev/null <<EOF
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"
volumes:
- ./traefik/traefik.yml:/traefik.yml:ro
- ./config:/config:ro
- ./logs:/logs
- ./certificates:/certificates
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(\`traefik.$DOMAIN\`)"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=authelia@file"
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- secure-network
volumes:
- ./authelia:/config
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(\`auth.$DOMAIN\`)"
- "traefik.http.routers.authelia.tls=true"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
networks:
secure-network:
external: true
EOF
# Set proper permissions
chmod 644 ~/secure-stack/traefik/traefik.yml
chmod 644 ~/secure-stack/config/middlewares.yml
chmod 644 ~/secure-stack/authelia/configuration.yml
chmod 600 ~/secure-stack/authelia/users.yml
chmod 644 ~/secure-stack/docker-compose.yml
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Configure your DNS to point auth.$DOMAIN and traefik.$DOMAIN to this server"
echo "2. Start the stack: cd ~/secure-stack && docker compose up -d"
echo "3. Access Authelia at https://auth.$DOMAIN (admin/admin - change password!)"
echo "4. Access Traefik dashboard at https://traefik.$DOMAIN"
echo -e "${YELLOW}Note: Default login is admin/admin. Please change this immediately!${NC}"
Review the script before running. Execute with: bash install.sh