Version: 1.0 Last Updated: 2025-11-02 Target Audience: Security engineers, operations teams
This document provides comprehensive security hardening guidelines for deploying MrWhoOidc in production environments using Docker.
MrWhoOidc implements multiple layers of security:
Threats Addressed:
Out of Scope (requires additional infrastructure):
MrWhoOidc containers run as non-root by default:
# Already configured in Dockerfile
USER 1654
Verification:
# Check user in running container
docker compose exec webauth whoami
# Should output: "appuser" or numeric UID 1654, NOT root
Why Important: Prevents container escape exploits from gaining root access on host.
# Dockerfile uses chiseled Ubuntu base
FROM mcr.microsoft.com/dotnet/aspnet:9.0-jammy-chiseled
Benefits:
For webauth container, consider read-only root filesystem:
services:
webauth:
image: ghcr.io/popicka70/mrwhooidc:latest
read_only: true
tmpfs:
- /tmp
- /var/tmp
Important: Test thoroughly - some features may require write access to specific paths.
services:
webauth:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to ports <1024
Prevent DoS from resource exhaustion:
services:
webauth:
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
Scan images for vulnerabilities:
# Using Docker Scout
docker scout cves ghcr.io/popicka70/mrwhooidc:latest
# Using Trivy
trivy image ghcr.io/popicka70/mrwhooidc:latest
# Using Grype
grype ghcr.io/popicka70/mrwhooidc:latest
Best Practice: Scan on every build in CI/CD pipeline.
Sign images with Docker Content Trust:
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Pull only signed images
docker pull ghcr.io/popicka70/mrwhooidc:latest
For Publishers: Sign images in CI/CD:
# .github/workflows/docker-publish.yml
- name: Sign image
run: |
docker trust sign ghcr.io/popicka70/mrwhooidc:$
Architecture:
networks:
internal:
driver: bridge
internal: true # CRITICAL: No external access
edge:
driver: bridge
services:
webauth:
networks:
- internal # Access to database/redis
- edge # Public access
postgres:
networks:
- internal # Isolated, no public access
redis:
networks:
- internal # Isolated, no public access
Verification:
# Postgres should NOT have external connectivity
docker compose exec postgres ping -c 1 google.com
# Should fail: "ping: bad address 'google.com'"
Host Firewall (iptables/firewalld):
# Allow only HTTPS traffic to webauth
iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Block direct access to PostgreSQL/Redis ports
iptables -A INPUT -p tcp --dport 5432 -j DROP
iptables -A INPUT -p tcp --dport 6379 -j DROP
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
Cloud Security Groups (AWS/Azure/GCP):
Recommended Architecture:
Internet → Reverse Proxy (nginx/Traefik) → Webauth Container
↓ TLS termination
nginx Configuration:
upstream mrwhooidc {
server localhost:8443;
}
server {
listen 443 ssl http2;
server_name auth.company.com;
# TLS Configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Rate Limiting
limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/s;
limit_req zone=auth burst=20 nodelay;
location / {
proxy_pass https://mrwhooidc;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Traefik Configuration (docker-compose.yml):
services:
traefik:
image: traefik:v2.10
command:
- --providers.docker=true
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=admin@company.com
- --certificatesresolvers.letsencrypt.acme.storage=/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports:
- "443:443"
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
networks:
- edge
webauth:
labels:
- "traefik.enable=true"
- "traefik.http.routers.mrwhooidc.rule=Host(`auth.company.com`)"
- "traefik.http.routers.mrwhooidc.entrypoints=websecure"
- "traefik.http.routers.mrwhooidc.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.ratelimit.ratelimit.burst=50"
Only expose required ports:
services:
webauth:
ports:
- "8443:8443" # HTTPS only
# DO NOT expose 8080 (HTTP) in production
PostgreSQL TLS (production recommendation):
postgres:
command: >
postgres
-c ssl=on
-c ssl_cert_file=/etc/ssl/certs/server.crt
-c ssl_key_file=/etc/ssl/private/server.key
volumes:
- ./certs/postgres-server.crt:/etc/ssl/certs/server.crt:ro
- ./certs/postgres-server.key:/etc/ssl/private/server.key:ro
Update connection string:
webauth:
environment:
ConnectionStrings__authdb: "Host=postgres;Database=authdb;Username=oidc;Password=${POSTGRES_PASSWORD};SSL Mode=Require"
Redis TLS (if using Redis Cloud or requiring encryption):
redis:
command: >
redis-server
--tls-port 6379
--port 0
--tls-cert-file /etc/redis/certs/redis.crt
--tls-key-file /etc/redis/certs/redis.key
--tls-ca-cert-file /etc/redis/certs/ca.crt
Critical Files (add to .gitignore):
.env
certs/*.pfx
certs/*.key
secrets/
Verification:
# Check repository for leaked secrets
git log -p | grep -i "password\|secret\|key" --color
# Use tools
trufflehog git file://. --only-verified
gitleaks detect --source . -v
Development (.env file):
# Set restrictive permissions
chmod 600 .env
# Verify
ls -la .env
# Should show: -rw------- (owner read/write only)
Production Options:
services:
webauth:
secrets:
- postgres_password
- cert_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
CERT_PASSWORD_FILE: /run/secrets/cert_password
secrets:
postgres_password:
external: true
cert_password:
external: true
Create secrets:
echo "strong-password" | docker secret create postgres_password -
echo "cert-password" | docker secret create cert_password -
# Fetch secrets from Vault at runtime
export POSTGRES_PASSWORD=$(vault kv get -field=password secret/mrwhooidc/postgres)
export CERT_PASSWORD=$(vault kv get -field=password secret/mrwhooidc/cert)
docker compose up -d
AWS Secrets Manager:
# Fetch from AWS Secrets Manager
export POSTGRES_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id mrwhooidc/postgres-password \
--query SecretString \
--output text)
Azure Key Vault:
# Fetch from Azure Key Vault
export POSTGRES_PASSWORD=$(az keyvault secret show \
--vault-name mrwhooidc-vault \
--name postgres-password \
--query value \
--output tsv)
Google Secret Manager:
# Fetch from GCP Secret Manager
export POSTGRES_PASSWORD=$(gcloud secrets versions access latest \
--secret="mrwhooidc-postgres-password")
Storage:
# Store certificates with restrictive permissions
chmod 600 certs/aspnetapp.pfx
chmod 600 certs/*.key
chmod 644 certs/*.crt
# Verify
ls -la certs/
Rotation:
# Rotate certificates annually (Let's Encrypt: every 90 days)
# 1. Generate new certificate
# 2. Update certs/ directory
# 3. Restart webauth: docker compose restart webauth
Backup:
# Backup certificates to secure location (encrypted)
tar czf certs-backup-$(date +%Y%m%d).tar.gz certs/
gpg --encrypt --recipient admin@company.com certs-backup-*.tar.gz
# Store encrypted backup offsite
Requirements:
Generation:
# Generate strong password
openssl rand -base64 32
# Or using pwgen
pwgen -s 32 1
Rotation Procedure:
# 1. Update password in secret store (Vault/AWS/Azure)
# 2. Update PostgreSQL password
docker compose exec postgres psql -U postgres -c "ALTER USER oidc WITH PASSWORD 'new-password';"
# 3. Update .env or secret
POSTGRES_PASSWORD=new-password
# 4. Restart webauth
docker compose restart webauth
# 5. Verify
docker compose logs webauth | grep "database connection"
Production Certificates:
Using Certbot:
# Install certbot
apt-get install certbot
# Obtain certificate (standalone mode - stops on port 80)
certbot certonly --standalone -d auth.company.com
# Certificates saved to:
# /etc/letsencrypt/live/auth.company.com/fullchain.pem
# /etc/letsencrypt/live/auth.company.com/privkey.pem
# Convert to PFX for ASP.NET Core
openssl pkcs12 -export \
-out ./certs/aspnetapp.pfx \
-inkey /etc/letsencrypt/live/auth.company.com/privkey.pem \
-in /etc/letsencrypt/live/auth.company.com/fullchain.pem \
-password pass:YourCertPassword
# Auto-renewal (cron)
0 0 * * * certbot renew --post-hook "docker compose restart webauth"
Enforce TLS 1.2+ only:
webauth:
environment:
Kestrel__Endpoints__Https__Protocols: Http1AndHttp2
Kestrel__Endpoints__Https__SslProtocols: Tls12,Tls13
Enable HSTS in reverse proxy (see nginx config above) or application:
webauth:
environment:
HSTS_ENABLED: true
HSTS_MAX_AGE: 31536000 # 1 year
Connection Limits:
postgres:
command: >
postgres
-c max_connections=100
-c shared_buffers=256MB
-c log_connections=on
-c log_disconnections=on
Authentication:
# Edit pg_hba.conf (inside container)
docker compose exec postgres bash
echo "host all all 0.0.0.0/0 scram-sha-256" >> /var/lib/postgresql/data/pg_hba.conf
Best Practice: Use scram-sha-256 instead of md5 or password.
Volume Encryption:
Example (LUKS):
# Create encrypted volume
cryptsetup luksFormat /dev/sdb
cryptsetup open /dev/sdb postgres-encrypted
# Format and mount
mkfs.ext4 /dev/mapper/postgres-encrypted
mount /dev/mapper/postgres-encrypted /var/lib/docker/volumes/mrwhooidc_postgres-data/_data
# Encrypt backups with GPG
docker compose exec postgres pg_dump -U oidc authdb | \
gzip | \
gpg --encrypt --recipient admin@company.com \
> backup-encrypted-$(date +%Y%m%d).sql.gz.gpg
# Decrypt when restoring
gpg --decrypt backup-encrypted-YYYYMMDD.sql.gz.gpg | \
gunzip | \
docker compose exec -T postgres psql -U oidc authdb
Enable PostgreSQL audit logging:
postgres:
command: >
postgres
-c log_statement=all
-c log_duration=on
-c log_line_prefix='%t [%p]: user=%u,db=%d,app=%a,client=%h '
Warning: log_statement=all logs everything (verbose). For production, use log_statement=ddl or mod.
Require password:
redis:
command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1 --loglevel warning
Update connection string:
webauth:
environment:
Redis__ConnectionString: "redis:6379,password=${REDIS_PASSWORD},abortConnect=false"
redis:
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--rename-command FLUSHDB ""
--rename-command FLUSHALL ""
--rename-command CONFIG ""
--save 60 1
Redis should NEVER be exposed publicly:
redis:
networks:
- internal # Isolated network only
# NO ports section - do not expose to host
Restrict Admin Access:
admin.auth.company.comnginx IP Whitelist:
location /admin {
allow 10.0.0.0/8; # Internal network
allow 203.0.113.0/24; # Office IP range
deny all;
proxy_pass https://mrwhooidc;
}
Disable Shell in Production:
services:
webauth:
security_opt:
- no-new-privileges:true
Audit Shell Access:
# Log all docker exec commands
auditctl -w /usr/bin/docker -p x -k docker_exec
Docker Host Access:
docker group membershipEnable Audit Logs:
webauth:
environment:
Logging__LogLevel__MrWhoOidc.Auth: Information
Logging__LogLevel__MrWhoOidc.WebAuth.Handlers: Information
Critical Events to Log:
Centralized Logging (ELK Stack):
services:
webauth:
logging:
driver: "fluentd"
options:
fluentd-address: "fluentd:24224"
tag: "mrwhooidc.webauth"
Syslog:
webauth:
logging:
driver: "syslog"
options:
syslog-address: "udp://logserver:514"
tag: "mrwhooidc"
Metrics to Monitor:
Alerting Rules:
# Prometheus alert example
groups:
- name: mrwhooidc_security
rules:
- alert: HighFailedLogins
expr: rate(failed_logins_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High rate of failed logins detected"
Host-Based IDS (OSSEC/Wazuh):
# Monitor Docker logs for suspicious activity
<localfile>
<log_format>json</log_format>
<location>/var/lib/docker/containers/*/*-json.log</location>
</localfile>
Container Runtime Security (Falco):
# Falco rules for MrWhoOidc
- rule: Unauthorized Process in Container
condition: spawned_process and container.name = "mrwhooidc-webauth" and not proc.name in (dotnet)
output: "Unexpected process in MrWhoOidc container (proc=%proc.name)"
priority: WARNING
Data Protection:
Requirements:
Requirements:
Requirements:
Use this checklist for production deployments:
.env in .gitignore, no secrets in git history.env and certificates have 600/644 permissionssecurity@mrwhooidc.com (or create private issue)Document Version: 1.0 Last Updated: 2025-11-02 Maintained By: MrWhoOidc Security Team