For production deployments, consider using external authentication services or a full WebAuthn server with Traefik's ForwardAuth middleware.
A Traefik middleware plugin that provides WebAuthn/FIDO2 passkey authentication with user credentials specified in the plugin configuration.
- π Passkey Authentication: Modern passwordless authentication using WebAuthn
- π€ Configuration-Based Users: Define authorized users directly in Traefik config
- πͺ Session Management: Cookie-based sessions with configurable timeout
- π¨ Built-in Login Page: Clean, responsive authentication UI
- π Secure: HttpOnly, Secure cookies with SameSite protection
- π¦ No External Dependencies: Uses only Go standard library
- Uses simplified ECDSA P-256 (ES256) signature verification
- Public key extraction from COSE format is simplified
- No support for RSA keys or other algorithms currently
- Intended for internal/development use
Add the plugin to your Traefik static configuration:
# traefik.yml
experimental:
plugins:
passkey:
moduleName: github.com/CangioUni/traefik-passkey-plugin
version: v0.2.0Configure the middleware in your dynamic configuration:
# dynamic-config.yml
http:
middlewares:
passkey-auth:
plugin:
passkey:
users:
- id: "user-123"
name: "john.doe"
displayName: "John Doe"
credentials:
- id: "Y3JlZGVudGlhbC1pZA" # Base64URL-encoded credential ID
publicKey: "pQECAyYgASFYIPr5..." # Base64URL-encoded attestation object
signCount: 0
- id: "user-456"
name: "jane.smith"
displayName: "Jane Smith"
credentials:
- id: "YW5vdGhlci1jcmVkZW50aWFs"
publicKey: "pQECAyYgASFYIC3m..."
signCount: 0
rpDisplayName: "My Application"
rpId: "example.com"
rpOrigin: "https://example.com"
cookieName: "traefik_passkey_session"
cookieDomain: "example.com"
sessionTimeout: 60 # minutes
routers:
my-router:
rule: "Host(`example.com`)"
service: my-service
middlewares:
- passkey-auth
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
my-service:
loadBalancer:
servers:
- url: "http://backend:8080"| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
users |
array | β | - | List of authorized users with their credentials |
users[].id |
string | β | - | Unique user identifier |
users[].name |
string | β | - | Username for login |
users[].displayName |
string | β | - | User's display name |
users[].credentials |
array | β | - | User's passkey credentials |
users[].credentials[].id |
string | β | - | Base64URL credential ID |
users[].credentials[].publicKey |
string | β | - | Base64URL attestation object |
users[].credentials[].signCount |
int | β | - | Signature counter (start with 0) |
rpDisplayName |
string | β | "Traefik Passkey Auth" | Relying party display name |
rpId |
string | β | - | Relying party ID (domain) |
rpOrigin |
string | β | - | Relying party origin (full URL) |
cookieName |
string | β | "traefik_passkey_session" | Session cookie name |
cookieDomain |
string | β | "" | Cookie domain |
sessionTimeout |
int | β | 60 | Session timeout in minutes |
To add users to your configuration, you need to register their passkeys. Use the provided register.html file:
-
Serve the registration page:
# For local testing python3 -m http.server 8080 # Then open http://localhost:8080/register.html # For production, serve from your actual domain over HTTPS
-
Register a passkey:
- Open the registration page in your browser
- Enter username, display name, and RP ID (must match your domain)
- Click "Register Passkey"
- Follow your device's authentication prompts
- Copy the generated JSON configuration
-
Add to Traefik configuration:
- Paste the user object into your
usersarray - Restart Traefik or reload the configuration
- Paste the user object into your
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "john.doe",
"displayName": "John Doe",
"credentials": [{
"id": "dGVzdC1jcmVkZW50aWFsLWlk",
"publicKey": "pQECAyYgASFYIFN...",
"signCount": 0
}]
}- HTTPS Required: Passkey registration requires HTTPS in production (localhost works for testing)
- Domain Matching: The RP ID used during registration must match the domain where you'll use the passkey
- Multiple Devices: Register each device separately and add all credentials to the same user
- Unauthenticated Request: When a user accesses a protected route without a valid session, they're shown a login page
- Authentication Flow:
- User enters their username
- Plugin generates a WebAuthn challenge
- User's authenticator (fingerprint, face recognition, security key) signs the challenge
- Plugin verifies the signature against the configured public key
- Session Creation: On successful authentication, a secure session cookie is created
- Protected Access: Subsequent requests include the session cookie and are allowed through
- HTTPS Required: Passkey authentication requires HTTPS in production
- Secure Cookies: Cookies are marked as HttpOnly, Secure, and use SameSite=Lax
- Challenge Expiry: Authentication challenges expire after 5 minutes
- Session Timeout: Sessions expire based on the configured timeout
- RP ID/Origin: Must match your domain exactly
The plugin creates special authentication endpoints:
/.auth/passkey/begin- Start authentication ceremony (POST)/.auth/passkey/finish- Complete authentication (POST)/.auth/passkey/logout- End session (GET/POST)
version: '3.8'
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.file.directory=/config"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--experimental.plugins.passkey.modulename=github.com/yourusername/traefik-passkey-plugin"
- "--experimental.plugins.passkey.version=v1.0.0"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/config
- ./certs:/certs
app:
image: nginx:alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.middlewares=passkey-auth"- Verify the username matches the
nameoridfield in your configuration - Check for typos in the configuration
- Ensure
rpIdmatches your domain exactly (no protocol, no port) - Verify
rpOriginincludes the full URL with protocol - Check that credentials were registered for the same domain
- Confirm the public key is correctly base64-encoded
- Ensure cookies are enabled in the browser
- Check
cookieDomainmatches your domain - Verify you're using HTTPS (required in production)
MIT License - See LICENSE file for details
Contributions are welcome! Please open an issue or submit a pull request.