Authentication
Cetacean supports pluggable authentication with five modes. Authentication is optional: The default mode (none)
allows anonymous access. One mode is active at a time via auth.mode (see Configuration).
All authentication is identity-only (who you are). For per-resource access control, see Authorization.
Quick Start
All auth settings can be passed as CLI flags, environment variables, or config file keys. See Configuration for the full precedence rules. The examples below use CLI flags; equivalent env vars and config file keys are listed in each providerβs configuration table.
# No auth (default)
./cetacean
# OIDC (e.g., Keycloak, Auth0, Okta, Dex)
./cetacean \
-auth-mode oidc \
-auth-oidc-issuer https://idp.example.com \
-auth-oidc-client-id cetacean \
-auth-oidc-client-secret secret \
-auth-oidc-redirect-url https://cetacean.example.com/auth/callback
# Tailscale (local daemon)
./cetacean -auth-mode tailscale
# mTLS client certificates
./cetacean \
-auth-mode cert \
-auth-cert-ca /path/to/ca.pem \
-tls-cert /path/to/server.pem \
-tls-key /path/to/server-key.pem
# Trusted proxy headers
./cetacean \
-auth-mode headers \
-auth-headers-subject X-Remote-User \
-trusted-proxies 10.0.0.0/8
Identity Model
Every provider produces the same identity structure, available at GET /auth/whoami. The subject is the unique
identifier (OIDC sub, Tailscale user ID, certificate CN/SPIFFE URI, or header value). groups are used for
authorization audience matching. How each field is populated depends on the provider β see the
sections below.
Providers
None (Default)
Anonymous access. All requests receive a static identity with subject: "anonymous".
No configuration required β this is the default when auth.mode is unset. Use this when Cetacean is behind a VPN,
firewall, or reverse proxy that handles authentication externally.
OIDC
OpenID Connect with authorization code flow for browsers and Bearer token validation for machines/scripts.
Configuration
See OIDC configuration for all parameters.
Browser Flow
Unauthenticated browser requests are redirected to /auth/login, which initiates the standard authorization code flow
with your IdP. After authentication, the callback exchanges the code for tokens, validates the ID token, sets a session
cookie, and redirects to the original URL.
Browser Cetacean IdP
β β β
βββ GET /services ββββββββββββββΊβ β
β βββ 302 /auth/login ββββββββββββΊβ
βββββββββββββββββββββββββββββββββ€ β
βββ GET /auth/login ββββββββββββΊβ β
ββββ 302 to IdP authorize βββββββ€ β
βββ GET authorize ββββββββββββββββββββββββββββββββββββββββββββββΊβ
β β
ββββ 302 /auth/callback?code=...&state=... ββββββββββββββββββββββ€
βββ GET /auth/callback βββββββββΊβ β
β βββ Validate state, nonce β
β βββ Exchange code for tokens βββΊβ
β ββββ ID token + access token ββββ€
β βββ Validate ID token β
β βββ Set session cookie β
ββββ 302 to original URL ββββββββ€ β
βββ GET /services ββββββββββββββΊβ β
β βββ Validate session cookie β
ββββ 200 JSON βββββββββββββββββββ€ β
Machine Flow
For scripts and API clients, send an ID token in the Authorization header. The token is validated against the IdPβs
JWKS endpoint on every request.
GET /services HTTP/1.1
Authorization: Bearer eyJhbGci...
Accept: application/json
curl -H "Authorization: Bearer eyJhbGci..." \
-H "Accept: application/json" \
http://localhost:9000/services
Session Persistence
By default, the session signing key is generated randomly at startup β restarting the server invalidates all browser
sessions. Set auth.oidc.session_key to a fixed value for persistence across restarts:
openssl rand -hex 32 # generate a 32-byte key
./cetacean -auth-oidc-session-key a1b2c3...
Logout
POST /auth/logout clears the session cookie. If the IdP supports it (RFC 9722), the user is also redirected to the IdP for sign-out.
IdP Setup Examples
- Create a client with
confidentialaccess type - Set valid redirect URI to
https://cetacean.example.com/auth/callback - Enable βStandard Flowβ (authorization code)
- Note the client ID and secret from the Credentials tab
- Create a βRegular Web Applicationβ
- Add
https://cetacean.example.com/auth/callbackto Allowed Callback URLs - Add
https://cetacean.example.comto Allowed Logout URLs - Use the Auth0 domain as the issuer (e.g.,
https://your-tenant.auth0.com)
Dex:
staticClients:
- id: cetacean
secret: your-secret
name: Cetacean
redirectURIs:
- https://cetacean.example.com/auth/callback
Tailscale
Identifies users via the Tailscale WhoIs API. Every request from a tailnet peer is automatically authenticated β no login flow needed.
Choosing a Mode
Tailscale auth has two modes. Pick based on your deployment:
| Local mode (default) | tsnet mode | |
|---|---|---|
| How it works | Queries the hostβs Tailscale daemon to identify peers | Embeds a Tailscale node inside the Cetacean process |
| Tailscale installed on host? | Yes (daemon must be running) | No |
| Network binding | Listens on all interfaces (server.listen_addr); only Tailscale IPs are authenticated, others rejected | Authenticated routes listen exclusively on the tailnet; non-tailnet traffic cannot reach them |
| Docker health checks | Work normally (health endpoint is auth-exempt) | Work normally β meta endpoints (/-/health, /-/ready) remain on the regular listener |
| Config complexity | Minimal: just -auth-mode tailscale | Requires an auth key, hostname, and persistent state directory |
| Best for | Hosts already running Tailscale (bare-metal, VMs) | Containers, Docker Swarm services, or hosts without Tailscale installed |
Security note on local mode: Cetacean binds to server.listen_addr (default :9000, all interfaces). A
defense-in-depth IP range check rejects requests not from Tailscaleβs CGNAT (100.64.0.0/10) or ULA
(fd7a:115c:a1e0::/48) ranges, but this is an application-layer check, not a socket-level restriction. For tighter
isolation, bind to your nodeβs Tailscale IP (e.g. -listen-addr 100.x.x.x:9000) or use tsnet mode, which only
accepts connections through the embedded Tailscale node.
Local Mode (Default)
Uses the local Tailscale daemon to identify peers. Cetacean must run on a node inside the tailnet.
./cetacean -auth-mode tailscale
Requires the Tailscale daemon running locally (access to /run/tailscale/tailscaled.sock).
tsnet Mode
Embeds a Tailscale node directly into Cetacean. No local Tailscale installation is needed.
./cetacean \
-auth-mode tailscale \
-auth-tailscale-mode tsnet \
-auth-tailscale-authkey tskey-auth-... \
-auth-tailscale-hostname cetacean \
-auth-tailscale-state-dir /var/lib/cetacean/tsnet
In tsnet mode, authenticated routes are served on the tailnet listener. Meta-endpoints (/-/health, /-/ready)
remain on the regular listener for Docker health checks.
Configuration
See Tailscale configuration for all parameters.
Capability-Based Groups
Tailscale ACL capabilities can map users to application groups. Set auth.tailscale.capability:
./cetacean -auth-mode tailscale -auth-tailscale-capability example.com/cap/cetacean
Then in your Tailscale ACL policy, grant capabilities to users or groups:
{
"grants": [
{
"src": [
"group:admins"
],
"dst": [
"tag:cetacean"
],
"app": {
"example.com/cap/cetacean": [
{
"groups": [
"admin",
"operators"
]
}
]
}
}
]
}
Multiple grants are deduplicated and merged into the identityβs groups array.
Client Certificates (mTLS)
Authenticates via mTLS client certificates. Supports standard X.509 certificates and SPIFFE X.509-SVIDs for workload identity.
Requires TLS termination at Cetacean (not behind a TLS-terminating proxy).
Configuration
See Client certificate configuration for CA settings and TLS settings for server certificate and key.
./cetacean \
-auth-mode cert \
-auth-cert-ca /etc/cetacean/ca.pem \
-tls-cert /etc/cetacean/server.pem \
-tls-key /etc/cetacean/server-key.pem
Clients without a valid certificate cannot connect.
Identity is extracted from the certificate: SPIFFE URI SAN (highest priority), then email SAN, then Common Name. Groups come from Organizational Unit (OU) fields. SPIFFE X.509-SVIDs are supported for workload identity.
Trusted Proxy Headers
Reads identity from HTTP headers set by a trusted reverse proxy (nginx, Traefik, Envoy, etc.).
Important: This mode trusts that the proxy sets headers correctly. You must configure at least one security mechanism to prevent clients from spoofing headers by bypassing the proxy.
Configuration
See Trusted proxy header configuration for all parameters.
Header auth requires the general trusted_proxies setting (see General Settings).
Note: The headers-specific
auth.headers.trusted_proxiesoption is deprecated and will be removed in v1.
Security
The trusted_proxies setting is requiredβit restricts which IPs can set identity headers. Supports individual IPs and
CIDR notation (10.0.0.0/8). For additional protection, configure a shared secret that the proxy must include with
every request:
./cetacean \
-auth-mode headers \
-auth-headers-subject X-Remote-User \
-auth-headers-secret-header X-Proxy-Secret \
-auth-headers-secret-value my-secret-value \
-trusted-proxies 10.0.0.0/8
Proxy Configuration Examples
nginx with OAuth2 Proxy:
location / {
auth_request /oauth2/auth;
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
auth_request_set $groups $upstream_http_x_auth_request_groups;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-Email $email;
proxy_set_header X-Remote-Groups $groups;
proxy_set_header X-Proxy-Secret "my-secret-value";
proxy_pass http://cetacean:9000;
}
Traefik with ForwardAuth:
http:
middlewares:
auth:
forwardAuth:
address: "http://auth-server/verify"
authResponseHeaders:
- "X-Remote-User"
- "X-Remote-Email"
- "X-Remote-Groups"
routers:
cetacean:
middlewares:
- auth
service: cetacean
services:
cetacean:
loadBalancer:
servers:
- url: "http://cetacean:9000"
TLS
TLS termination is available in any auth mode and required for cert mode (mTLS). Set -tls-cert and -tls-key to
enable HTTPS. See the TLS settings in the configuration reference.
Docker Compose Examples
OIDC with Keycloak
services:
cetacean:
image: cetacean:latest
environment:
CETACEAN_AUTH_MODE: oidc
CETACEAN_AUTH_OIDC_ISSUER: https://keycloak.example.com/realms/myorg
CETACEAN_AUTH_OIDC_CLIENT_ID: cetacean
CETACEAN_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_secret
CETACEAN_AUTH_OIDC_REDIRECT_URL: https://cetacean.example.com/auth/callback
secrets:
- oidc_secret
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
deploy:
placement:
constraints: [ node.role == manager ]
secrets:
oidc_secret:
external: true
Tailscale (tsnet)
services:
cetacean:
image: cetacean:latest
environment:
CETACEAN_AUTH_MODE: tailscale
CETACEAN_AUTH_TAILSCALE_MODE: tsnet
CETACEAN_AUTH_TAILSCALE_AUTHKEY_FILE: /run/secrets/ts_authkey
CETACEAN_AUTH_TAILSCALE_HOSTNAME: cetacean
secrets:
- ts_authkey
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- tsnet-state:/var/lib/cetacean/tsnet
deploy:
placement:
constraints: [ node.role == manager ]
secrets:
ts_authkey:
external: true
volumes:
tsnet-state:
Behind nginx with Header Auth
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
deploy:
placement:
constraints: [ node.role == manager ]
cetacean:
image: cetacean:latest
environment:
CETACEAN_AUTH_MODE: headers
CETACEAN_AUTH_HEADERS_SUBJECT: X-Remote-User
CETACEAN_AUTH_HEADERS_NAME: X-Remote-Name
CETACEAN_AUTH_HEADERS_EMAIL: X-Remote-Email
CETACEAN_TRUSTED_PROXIES: "10.0.0.0/8"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
deploy:
placement:
constraints: [ node.role == manager ]
Verifying Your Setup
Check the current identity with GET /auth/whoami:
GET /auth/whoami HTTP/1.1
curl -s http://localhost:9000/auth/whoami | jq .
See the API reference for response schema and auth endpoint details.
Authorization
For per-resource access control β controlling which users can view or modify which resources β see Authorization.