A Traefik middleware plugin that adds TOTP (Time-based One-Time Password) authentication to your services. Protect your applications with two-factor authentication using standard authenticator apps like Google Authenticator, Authy, or Microsoft Authenticator.
- 🔒 TOTP Authentication: RFC 6238 compliant time-based one-time passwords
- 💾 In-Memory Sessions: Fast session management with configurable expiration
- 🎨 Beautiful UI: Modern, responsive authentication page
- ⚡ Auto-Submit: Automatically submits when 6 digits are entered
- 🕐 Clock Skew Tolerance: Handles time synchronization issues
- 🔐 Secure Cookies: HttpOnly, Secure, SameSite protection
- 📱 Mobile Friendly: Works great on phones and tablets
- 🌐 Optional IP Validation: Optionally tie sessions to IP addresses for extra security
# traefik.yml
experimental:
plugins:
totp-auth:
moduleName: github.com/CangioUni/traefik-totp-plugin
version: v0.1.0Add the plugin to your Traefik static configuration:
# traefik.yml
experimental:
localPlugins:
totp-auth:
moduleName: github.com/CangioUni/traefik-totp-plugincd traefik/plugins
git clone https://github.com/CangioUni/traefik-totp-plugin.gitFirst, you need to generate a TOTP secret key. You can use an online generator like: https://totp.danhersam.com/
Otherwise, if you have Python installed, run this command:
# Using Python
python3 -c "import base64, os; print(base64.b32encode(os.urandom(20)).decode())"This will output something like: JBSWY3DPEHPK3PXP
Add the middleware to your Traefik dynamic configuration:
# dynamic-config.yml
http:
middlewares:
totp-auth:
plugin:
totp-auth:
secretKey: "JBSWY3DPEHPK3PXP" # Your base32 encoded secret
sessionExpiry: 3600 # 1 hour (in seconds)
issuer: "MyApp" # Name shown in authenticator app
accountName: "user@example.com" # Account name in authenticator apphttp:
middlewares:
totp-auth:
plugin:
totp-auth:
secretKey: "JBSWY3DPEHPK3PXP"
sessionExpiry: 7200 # 2 hours
cookieName: "my_totp_session"
cookieDomain: ".example.com"
cookieSecure: true
issuer: "MyCompany"
accountName: "admin@company.com"
timeStep: 30 # Time step in seconds
codeDigits: 6 # Number of digits in code
allowedSkew: 1 # Allow ±1 time step for clock skew
validateIP: false # Set to true for stricter security (may break with proxies)
pageTitle: "Secure Access Required"
pageDescription: "Enter your authentication code to continue"If you're behind a load balancer or reverse proxy and want to enable IP validation, you need to configure trusted proxies:
http:
middlewares:
totp-auth:
plugin:
totp-auth:
secretKey: "JBSWY3DPEHPK3PXP"
sessionExpiry: 3600
validateIP: true # Enable IP validation
trustedProxies: # Define trusted proxy IP ranges
- "10.0.0.0/8" # Private network range
- "172.16.0.0/12" # Docker/Kubernetes range
- "192.168.0.0/16" # Local network range
issuer: "MyApp"
accountName: "user@example.com"How Trusted Proxies Work:
- When a request comes from a trusted proxy IP, the plugin uses
X-Forwarded-FororX-Real-IPheaders to get the real client IP - When a request comes from an untrusted IP, the plugin uses the direct connection IP and ignores forwarded headers (security measure)
- This prevents header spoofing while allowing proper IP validation behind load balancers
Common Trusted Proxy Ranges:
- Private Networks:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - Docker Default:
172.17.0.0/16 - Kubernetes: Depends on your CNI plugin (commonly
10.0.0.0/8) - Cloud Load Balancers: Check your cloud provider's IP ranges
http:
routers:
secure-app:
rule: "Host(`app.example.com`)"
service: my-service
middlewares:
- totp-auth # Add TOTP authentication
services:
my-service:
loadBalancer:
servers:
- url: "http://localhost:8080"- Generate a QR code with your secret key:
# Install qrencode (if not already installed)
# Ubuntu/Debian: apt-get install qrencode
# macOS: brew install qrencode
# Generate QR code
SECRET="JBSWY3DPEHPK3PXP"
ISSUER="MyApp"
ACCOUNT="user@example.com"
qrencode -t ANSIUTF8 "otpauth://totp/${ISSUER}:${ACCOUNT}?secret=${SECRET}&issuer=${ISSUER}"- Scan the QR code with your authenticator app
- The app will start generating codes
- Open your authenticator app (Google Authenticator, Authy, etc.)
- Choose "Enter a setup key" or "Manual entry"
- Enter these details:
- Account name: user@example.com (or whatever you configured)
- Secret key: JBSWY3DPEHPK3PXP (your secret)
- Type: Time-based
- Algorithm: SHA1
- Digits: 6
- Period: 30 seconds
#!/usr/bin/env python3
import qrcode
secret = "JBSWY3DPEHPK3PXP"
issuer = "MyApp"
account = "user@example.com"
uri = f"otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}"
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(uri)
qr.make(fit=True)
qr.print_ascii()
print(f"\nOr enter manually:")
print(f"Secret: {secret}")
print(f"Account: {account}")
print(f"Issuer: {issuer}")Visit: https://www.qr-code-generator.com/
- Input:
otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp - Generate and scan the QR code
| Parameter | Type | Default | Description |
|---|---|---|---|
secretKey |
string | required | Base32 encoded TOTP secret key |
sessionExpiry |
int | 3600 | Session duration in seconds (1 hour default) |
cookieName |
string | "totp_session" | Name of the session cookie |
cookieDomain |
string | "" | Cookie domain (empty = current domain) |
cookieSecure |
bool | true | Use secure cookies (HTTPS only) |
issuer |
string | "" | Issuer name shown in authenticator app |
accountName |
string | "" | Account name shown in authenticator app |
timeStep |
int | 30 | TOTP time step in seconds |
codeDigits |
int | 6 | Number of digits in TOTP code |
allowedSkew |
int | 1 | Number of time steps to allow for clock skew |
pageTitle |
string | "TOTP Authentication Required" | Custom page title |
pageDescription |
string | "Please enter your TOTP code..." | Custom page description |
validateIP |
bool | false | Enable IP validation for sessions (may break with proxies/NAT) |
trustedProxies |
[]string | [] | CIDR ranges of trusted proxies (e.g., ["10.0.0.0/8", "172.16.0.0/12"]) |
- First Visit: User accesses a protected resource
- Authentication Page: Plugin displays a beautiful TOTP input page
- Code Entry: User enters the 6-digit code from their authenticator app
- Validation: Plugin validates the code against the secret key
- Session Created: On success, plugin creates a session cookie
- Access Granted: User can now access the protected resource
- Session Expiry: After the configured time, user must re-authenticate
- In-Memory Sessions: Sessions are stored in memory only (not persisted to disk)
- Optional IP Validation: Optionally tie sessions to IP addresses (disabled by default for compatibility)
- Trusted Proxy Support: Only trusts forwarded headers from configured proxy IP ranges (prevents header spoofing)
- HttpOnly Cookies: Session cookies are not accessible via JavaScript
- Secure Cookies: Cookies only sent over HTTPS (configurable)
- SameSite Protection: CSRF protection via SameSite cookie attribute
- Clock Skew Tolerance: Accepts codes from ±1 time window (configurable)
- Auto Cleanup: Expired sessions are automatically removed every 5 minutes
# docker-compose.yml
version: '3'
services:
traefik:
image: traefik:v3.0
command:
- --api.insecure=true
- --providers.file.filename=/config/dynamic.yml
- --experimental.plugins.totp-auth.modulename=github.com/yourusername/traefik-totp-auth
- --experimental.plugins.totp-auth.version=v1.0.0
ports:
- "80:80"
- "8080:8080"
volumes:
- ./config:/config
whoami:
image: traefik/whoami# config/dynamic.yml
http:
middlewares:
totp:
plugin:
totp-auth:
secretKey: "JBSWY3DPEHPK3PXP"
sessionExpiry: 3600
issuer: "TestApp"
accountName: "test@example.com"
routers:
whoami:
rule: "Host(`localhost`)"
service: whoami
middlewares:
- totp
services:
whoami:
loadBalancer:
servers:
- url: "http://whoami"- Generate a test secret:
JBSWY3DPEHPK3PXP - Add to your authenticator app
- Configure the plugin with this secret
- Visit your protected URL
- Enter the code from your authenticator app
- Verify access is granted and session persists
- Ensure your secret is properly base32 encoded
- Remove any spaces or special characters
- Valid characters: A-Z and 2-7
- Check that your server time is synchronized (use NTP)
- Try increasing
allowedSkewto 2 or 3 - Verify the secret key matches in both plugin and authenticator app
- Increase
sessionExpiryvalue (in seconds) - Default is 3600 seconds (1 hour)
- Ensure
cookieSecure: falseif testing without HTTPS - Check browser console for cookie errors
- Verify
cookieDomainis correctly set (or empty)
- IP validation is disabled by default to prevent this issue
- If you enabled
validateIP: trueand users are behind proxies/NAT, their IP may change between requests - Solution 1: Keep
validateIP: false(default) for maximum compatibility - Solution 2: Configure
trustedProxieswith your proxy/load balancer IP ranges to use forwarded headers - Solution 3: Only enable IP validation in controlled environments with stable client IPs
- The plugin sees the load balancer's IP instead of the client's IP
- Solution: Configure
trustedProxieswith your load balancer's CIDR range - Example:
trustedProxies: ["10.0.0.0/8", "172.16.0.0/12"] - The plugin will then use
X-Forwarded-FororX-Real-IPheaders from trusted proxies - Ensure your load balancer is setting these headers correctly
- Generate Secret:
python3 -c "import base64, os; print(base64.b32encode(os.urandom(20)).decode())"
# Output: N3V2IY4HLQHC6VKF2LJNBXAU6M- Configure Plugin:
http:
middlewares:
my-totp:
plugin:
totp-auth:
secretKey: "N3V2IY4HLQHC6VKF2LJNBXAU6M"
sessionExpiry: 3600
issuer: "MyCompany Portal"
accountName: "admin"- Setup Authenticator:
# Generate QR code
qrencode -t ANSIUTF8 "otpauth://totp/MyCompany%20Portal:admin?secret=N3V2IY4HLQHC6VKF2LJNBXAU6M&issuer=MyCompany%20Portal"- Test:
- Visit your protected URL
- Enter code from authenticator app
- Enjoy secure access for 1 hour
- Secret Key Security: Never commit secrets to version control
- Use Environment Variables: Store secrets in environment variables
- HTTPS Only: Always use
cookieSecure: truein production - Regular Rotation: Consider rotating secrets periodically
- Backup Codes: Implement backup authentication methods
- Monitor Failed Attempts: Log and monitor authentication failures
- Session Duration: Balance security with user convenience (1-4 hours typical)
MIT License - See LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.
For issues, questions, or contributions, please visit the GitHub repository.