Deploying Authentik SSO with Azure SMTP and HAProxy: A Complete Guide

Setting up a robust Single Sign-On (SSO) solution is essential for modern infrastructure management. In this guide, I'll walk you through deploying Authentik with high availability using Docker Swarm, configuring email notifications, and integrating everything with HAProxy as a reverse proxy.

7 min read
Deploying Authentik SSO with Azure SMTP and HAProxy: A Complete Guide

Why This Stack?

Authentik is an open-source identity provider that supports multiple protocols (OAuth2, SAML, LDAP) and provides excellent flexibility for authentication flows. Combined with:

  • Docker Swarm for container orchestration and high availability
  • External PostgreSQL for centralized database management
  • Azure Communication Services for reliable email delivery
  • HAProxy for load balancing and SSL termination

You get a production-ready SSO solution that scales across multiple nodes.

Architecture Overview

The setup consists of:

  • 5 Docker Swarm worker nodes running Authentik services
  • External PostgreSQL database (outside the swarm) for data persistence
  • NFS shared storage for media and template files
  • HAProxy for SSL termination and load balancing
  • Azure SMTP for email delivery
  • .env file for centralized configuration management

This architecture provides clear separation of concerns and makes database maintenance independent of application deployments.

Part 1: Docker Compose Configuration

First, let's set up Authentik with Docker Swarm. Here's the complete structure:

services:
  server:
    command: server
    environment:
      # PostgreSQL - External Database
      AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_POSTGRESQL__HOST}
      AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}
      AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRESQL__USER}
      
      # Secret Key
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
      
      # SMTP Configuration
      AUTHENTIK_EMAIL__HOST: ${SMTP_HOST}
      AUTHENTIK_EMAIL__PORT: ${SMTP_PORT:-587}
      AUTHENTIK_EMAIL__USERNAME: ${SMTP_USERNAME}
      AUTHENTIK_EMAIL__PASSWORD: ${SMTP_PASSWORD}
      AUTHENTIK_EMAIL__USE_TLS: ${SMTP_USE_TLS:-true}
      AUTHENTIK_EMAIL__USE_SSL: ${SMTP_USE_SSL:-false}
      AUTHENTIK_EMAIL__TIMEOUT: ${SMTP_TIMEOUT:-30}
      AUTHENTIK_EMAIL__FROM: ${SMTP_FROM}
      
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.3}
    ports:
      - 9002:9000
      - 9445:9443
    restart: unless-stopped
    deploy:
      placement:
        constraints: [node.role == worker]
    volumes:
      - nfs-authentik:/media
      - nfs-authentik:/templates

  worker:
    command: worker
    environment:
      # PostgreSQL - External Database
      AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_POSTGRESQL__HOST}
      AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}
      AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRESQL__USER}
      
      # Secret Key
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
      
      # SMTP Configuration (same as server)
      AUTHENTIK_EMAIL__HOST: ${SMTP_HOST}
      AUTHENTIK_EMAIL__PORT: ${SMTP_PORT:-587}
      AUTHENTIK_EMAIL__USERNAME: ${SMTP_USERNAME}
      AUTHENTIK_EMAIL__PASSWORD: ${SMTP_PASSWORD}
      AUTHENTIK_EMAIL__USE_TLS: ${SMTP_USE_TLS:-true}
      AUTHENTIK_EMAIL__USE_SSL: ${SMTP_USE_SSL:-false}
      AUTHENTIK_EMAIL__TIMEOUT: ${SMTP_TIMEOUT:-30}
      AUTHENTIK_EMAIL__FROM: ${SMTP_FROM}
      
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.3}
    restart: unless-stopped
    user: root
    deploy:
      placement:
        constraints: [node.role == worker]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - nfs-authentik:/media
      - nfs-authentik:/certs
      - nfs-authentik:/templates
    
volumes:
  nfs-authentik:
    driver: local
    driver_opts:
      device: :/mnt/storage/authentik
      o: addr=10.0.0.10,nolock,soft,rw,nfsvers=4
      type: nfs

Key Points:

  1. External Database - PostgreSQL runs on a dedicated server (e.g., 10.0.0.20:5432), separate from the Docker Swarm
  2. Both server and worker need identical environment variables
  3. NFS volumes ensure media/template persistence across nodes
  4. Placement constraints distribute services across worker nodes
  5. Custom ports (9002, 9445) avoid conflicts with other services
  6. .env file centralizes all configuration

Why External PostgreSQL?

Running PostgreSQL outside the swarm provides several benefits:

  • Independent scaling - Database resources separate from app nodes
  • Simplified backups - Standard PostgreSQL backup tools without container complexity
  • Better performance - Dedicated hardware for database operations
  • Easier maintenance - Database upgrades don't affect app deployments
  • Multi-service support - Same database server can support multiple applications

Part 2: Environment File Configuration

Create a .env file in the same directory as your docker-compose.yml:

# .env - Authentik Configuration
# WARNING: Keep this file secure and never commit to version control!

# ============================================================================
# Database Configuration - External PostgreSQL
# ============================================================================
AUTHENTIK_POSTGRESQL__HOST=10.0.0.20
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__USER=authentik_user
AUTHENTIK_POSTGRESQL__PASSWORD=secure_database_password_here

# ============================================================================
# Authentik Secret Key
# Generate with: openssl rand -base64 60
# ============================================================================
AUTHENTIK_SECRET_KEY=your_long_random_secret_key_here

# ============================================================================
# SMTP Configuration - Azure Communication Services
# ============================================================================
SMTP_HOST=smtp.azurecomm.net
SMTP_PORT=587
SMTP_USERNAME=your-azure-communication-endpoint
SMTP_PASSWORD=your-azure-access-key
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30
SMTP_FROM=noreply@yourdomain.com

# ============================================================================
# Docker Image Configuration
# ============================================================================
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
AUTHENTIK_TAG=2025.10.3

Alternative: Microsoft 365 SMTP

If using Microsoft 365 instead of Azure Communication Services:

# SMTP Configuration - Microsoft 365
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USERNAME=service-account@yourdomain.com
SMTP_PASSWORD=app-specific-password-here
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30
SMTP_FROM=service-account@yourdomain.com

Securing Your .env File

# Set restrictive permissions
chmod 600 .env
chown root:root .env

# Add to .gitignore
echo ".env" >> .gitignore

# Create a template for documentation
cp .env .env.example
# Edit .env.example to remove all sensitive values

Example .env.example for documentation:

# Database Configuration
AUTHENTIK_POSTGRESQL__HOST=your-db-host
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__USER=authentik_user
AUTHENTIK_POSTGRESQL__PASSWORD=your-secure-password

# Secret Key (generate with: openssl rand -base64 60)
AUTHENTIK_SECRET_KEY=your-secret-key

# SMTP Configuration
SMTP_HOST=smtp.azurecomm.net
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=noreply@yourdomain.com

Part 3: External PostgreSQL Setup

Database Server Prerequisites

On your PostgreSQL server (outside the swarm):

-- Connect to PostgreSQL as superuser
psql -U postgres

-- Create database and user
CREATE DATABASE authentik;
CREATE USER authentik_user WITH ENCRYPTED PASSWORD 'secure_password';

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE authentik TO authentik_user;

-- Enable required extensions
\c authentik
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Grant schema permissions
GRANT ALL ON SCHEMA public TO authentik_user;

Testing Database Connectivity

From your Docker Swarm manager node:

# Test connection from swarm network
docker run --rm -it postgres:15 psql \
  -h 10.0.0.20 \
  -U authentik_user \
  -d authentik \
  -c "SELECT version();"

Part 4: Azure SMTP Configuration

Azure offers two main options for SMTP: Azure Communication Services and Microsoft 365 SMTP Relay.

This is Azure's modern email service with better deliverability and features.

Setup Steps:

  1. Create an Azure Communication Services resource in Azure Portal
  2. Set up an Email Communication Service
  3. Add and verify your domain (requires DNS records)
  4. Navigate to your resource and copy the Endpoint and Access Key

Add to your .env file:

# Azure Communication Services
SMTP_HOST=smtp.azurecomm.net
SMTP_PORT=587
SMTP_USERNAME=<your-azure-endpoint>
SMTP_PASSWORD=<your-access-key>
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30
SMTP_FROM=noreply@yourdomain.com

Option B: Microsoft 365 SMTP Relay

For organizations already using Microsoft 365:

Add to your .env file:

# Microsoft 365
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USERNAME=service-account@yourdomain.com
SMTP_PASSWORD=<password-or-app-specific-password>
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30
SMTP_FROM=service-account@yourdomain.com

Important: If using MFA on your Microsoft 365 account, generate an app-specific password.

Testing Email Configuration

After deploying, test the email functionality:

# Deploy the stack
docker stack deploy -c docker-compose.yml authentik

# Watch logs
docker service logs -f authentik_server

# Send test email from Authentik UI
# Navigate to: System → Settings → Test Email

Common issues:

  • Authentication failures: Verify credentials in .env file
  • TLS errors: Ensure USE_TLS=true and USE_SSL=false
  • Rejected emails: Check domain verification in Azure
  • Connection timeouts: Verify firewall rules allow outbound port 587

Part 5: HAProxy Integration

Now let's integrate Authentik with HAProxy for high availability and SSL termination.

Frontend Configuration

Add the ACL and routing rules to your HTTPS frontend:

frontend https_frontend
    # SSL binding with HTTP/2 support
    bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
    
    # Security headers
    http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    http-response set-header X-Content-Type-Options nosniff
    http-response set-header X-XSS-Protection "1; mode=block"
    http-response set-header Referrer-Policy strict-origin-when-cross-origin
    http-response set-header Permissions-Policy "geolocation=(), microphone=(), camera=()"
    
    # Remove server identification
    http-response del-header Server
    http-response del-header X-Powered-By
    
    # Standard reverse-proxy headers
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Host %[req.hdr(Host)]
    http-request set-header X-Forwarded-For %[src]
    http-request set-header Forwarded "proto=https;host=%[req.hdr(Host)];for=%[src]"
    
    # Authentik routing
    acl host_authentik hdr(host) -i sso.example.com
    use_backend authentik_backend if host_authentik

Backend Configuration

backend authentik_backend
    description Authentik SSO Identity Provider
    balance roundrobin
    
    # Extended timeouts for authentication flows
    timeout server 60s
    timeout tunnel 1h
    
    # WebSocket support for real-time updates
    option http-server-close
    option forwardfor
    
    # Health check
    option httpchk GET /
    http-check send meth GET uri / hdr Host sso.example.com
    http-check expect rstatus (200|302|404)
    option log-health-checks
    
    # High availability across three nodes
    server docker01 10.0.1.10:9445 check ssl verify none inter 20s rise 2 fall 3 maxconn 50
    server docker02 10.0.1.11:9445 check ssl verify none inter 20s rise 2 fall 3 maxconn 50
    server docker03 10.0.1.12:9445 check ssl verify none inter 20s rise 2 fall 3 maxconn 50

SSL Certificate Setup

Create a combined PEM file for HAProxy:

# Using Let's Encrypt certificates
cat /etc/letsencrypt/live/sso.example.com/fullchain.pem \
    /etc/letsencrypt/live/sso.example.com/privkey.pem \
    > /etc/haproxy/certs/sso.example.com.pem

chmod 600 /etc/haproxy/certs/sso.example.com.pem
chown haproxy:haproxy /etc/haproxy/certs/sso.example.com.pem

Testing and Validation

# Test HAProxy configuration
haproxy -c -f /etc/haproxy/haproxy.cfg

# Reload HAProxy (zero downtime)
systemctl reload haproxy

# Monitor logs
tail -f /var/log/haproxy.log

# Test backend connectivity
curl -k https://10.0.1.10:9445
curl -k https://10.0.1.11:9445
curl -k https://10.0.1.12:9445

Part 6: High Availability Considerations

Health Checks

HAProxy performs several types of checks:

  1. HTTP checks (option httpchk) - Verifies application responds correctly
  2. Rise/fall thresholds - Prevents flapping (2 successes to mark up, 3 failures to mark down)
  3. Inter intervals - Check every 20 seconds

Session Persistence

For Authentik, you typically don't need sticky sessions because:

  • Authentication state is stored in the external PostgreSQL database
  • JWT tokens are stateless
  • Sessions are shared across all nodes via the centralized database

However, for improved performance during active authentication flows:

backend authentik_backend
    # Optional: sticky sessions based on source IP
    stick-table type ip size 100k expire 30m
    stick on src
    
    # ... rest of configuration

Load Balancing Strategy

Roundrobin is ideal for Authentik because:

  • Distributes load evenly across all nodes
  • No single point of failure
  • Simple and predictable behavior
  • Works well with stateless applications backed by external database

External Database Benefits

With an external PostgreSQL database:

  • No split-brain scenarios - Single source of truth for all nodes
  • Simplified scaling - Add/remove app nodes without database concerns
  • Independent database tuning - Optimize PostgreSQL separately from application
  • Easier monitoring - Standard database monitoring tools work without container complexity

Next Steps

  1. Configure OIDC/SAML - Connect your applications
  2. Set up MFA - Add WebAuthn or TOTP
  3. Customize flows - Tailor authentication experience
  4. Enable LDAP - Integrate with legacy apps
  5. Implement monitoring - Add Prometheus/Grafana
  6. Set up automated backups - Database and configuration backups
  7. Configure log aggregation - Centralized logging with ELK or similar

Resources