Authentication Guide
Authentication Guide
Section titled “Authentication Guide”Bifrost authenticates connections using an ordered list of auth providers.
Each provider declares a plugin type, an optional priority (lower numbers are
tried first), and a config map with the plugin-specific settings.
Schema note: The legacy
auth.modefield and the top-level typed blocks (auth.native,auth.ldap,auth.oauth,auth.system, …) are no longer supported and the server rejects them at startup. Useauth.providers[]with an explicittypeand a nestedconfigmap, as shown below.
This page is the reference for every provider type and the shape of its config
map. The companion Advanced Authentication guide
does not duplicate that schema — it covers the enterprise deployment side of
Kerberos, NTLM, mTLS and SPNEGO: keytab and CA creation, krb5.conf, browser and
curl client setup, the auth.negotiate middleware, and troubleshooting. Where
the two pages describe the same option they use the same keys; if they ever
disagree, the plugin source under internal/auth/plugin/ is the authority.
Providers that cannot authenticate
Section titled “Providers that cannot authenticate”A provider that accepts configuration and then rejects every login is indistinguishable, to the person trying to log in, from a wrong password. So Bifrost refuses to run one rather than let it fail quietly. Two states are treated differently, because they are different problems:
| State | Meaning | Result |
|---|---|---|
unimplemented |
The plugin can never authenticate anyone in any build — ntlm cannot verify an NTLM response |
Server refuses to start |
build_disabled |
This binary lacks what the plugin needs (a build tag, cgo, a platform library), but another build has it — system without the pam tag on Linux |
Starts, with a warning logged for that provider |
Note where the refusal happens: the config file itself parses and validates
normally, so bifrost-server validate and the dashboard’s config editor accept
it. The provider is rejected when the authenticator is constructed, which is
during server start:
create authenticator: provider "corp-ntlm": auth provider type "ntlm" cannotauthenticate anyone and is refused: NTLM response verification is notimplemented: there is no credential source (NT-hash store or domain-controllerpass-through) to verify a client's NTLMv2 response against, so every login isrejected. Use the 'kerberos' provider for Windows-domain SSO, or 'ldap' againstActive Directory. [...]The remedy is to remove the provider, or set enabled: false on it — a disabled
provider is never constructed. For Windows domain single sign-on use kerberos
with the auth.negotiate middleware, which is functional; see the
Advanced Authentication guide.
build_disabled deliberately does not block startup: the same configuration is
valid on a build that includes the support, so refusing it would make one config
file un-shareable across builds. It is logged at WARN instead:
auth provider cannot authenticate in this build; every login through it will berejected provider=sys type=system state=build_disabledTo see what the running binary actually supports, ask it — GET /api/v1/auth/providers
reports an availability state per plugin, and POST /api/v1/config/validate
reports a refused provider as a validation error. Both are documented in the
server API reference.
No Authentication
Section titled “No Authentication”Default behaviour - allows all connections.
auth: providers: - name: open type: none enabled: true priority: 1Native Authentication
Section titled “Native Authentication”Username/password authentication with bcrypt hashes.
auth: providers: - name: native type: native enabled: true priority: 1 config: users: - username: admin password_hash: "$2a$10$..." groups: - admins email: admin@example.com full_name: "Admin User" - username: user1 password_hash: "$2a$10$..." disabled: falseGenerating Password Hashes
Section titled “Generating Password Hashes”Using the bcrypt tool:
# Using htpasswdhtpasswd -bnBC 10 "" password | tr -d ':\n'
# Using Pythonpython3 -c "import bcrypt; print(bcrypt.hashpw(b'password', bcrypt.gensalt()).decode())"
# Using Gogo run -mod=mod github.com/rennerdo30/bifrost-proxy/tools/hashpw passwordLDAP Authentication
Section titled “LDAP Authentication”Authenticate against an LDAP directory.
auth: providers: - name: ldap type: ldap enabled: true priority: 1 config: url: "ldap://ldap.example.com:389" base_dn: "dc=example,dc=com" bind_dn: "cn=service,dc=example,dc=com" bind_password: "${LDAP_BIND_PASSWORD}" user_filter: "(uid=%s)" group_filter: "(memberUid=%s)" require_group: "proxy-users" tls: false insecure_skip_verify: falseLDAP Configuration
Section titled “LDAP Configuration”| Field | Default | Description |
|---|---|---|
url |
required | LDAP server URL |
base_dn |
required | Base DN for searches |
bind_dn |
- | DN for the service account |
bind_password |
- | Service account password |
user_filter |
(uid=%s) |
Filter to find users (%s = username) |
group_filter |
- | Filter to find user groups |
require_group |
- | Only allow users in this group |
user_attribute |
uid |
Attribute holding the username |
email_attribute |
mail |
Attribute holding the email address |
full_name_attribute |
cn |
Attribute holding the display name |
group_attribute |
cn |
Attribute holding the group name |
tls |
false |
Use LDAPS/TLS |
insecure_skip_verify |
false |
Skip TLS certificate verification |
group_lookup_fail_closed |
false |
Reject the login when the group lookup itself fails |
Both url and base_dn are required — the provider fails to start without
them.
System Authentication
Section titled “System Authentication”Authenticate against the operating system’s user database (PAM on Linux, Directory Services on macOS).
Warning: Platform Support System authentication is only supported on Linux and macOS. Windows is not currently supported. If you need authentication on Windows, use
native,ldap, oroauthinstead.
auth: providers: - name: system type: system enabled: true priority: 1 config: service: "login" # PAM service name (Linux only) allowed_users: # Optional: restrict to specific users - alice - bob allowed_groups: # Optional: restrict to specific groups - admin - staffPlatform Support Matrix
Section titled “Platform Support Matrix”| Platform | Support | Method |
|---|---|---|
| Linux | ⚠️ Requires custom build | PAM (only in CGO_ENABLED=1 -tags pam builds; the default/Docker build fails closed) |
| macOS | ✅ Supported | Directory Services (dscl) |
| Windows | ❌ Not Supported | Use native/ldap/oauth instead |
Warning: The default (and Docker) binaries are built without the
pamtag and with cgo disabled, so Linux system authentication fails closed — every login is rejected. To enable it, build from source withCGO_ENABLED=1 go build -tags pam ./...(requires libpam headers, e.g.libpam0g-dev/pam-devel). macOS does not need a special build.
System Authentication Configuration
Section titled “System Authentication Configuration”| Field | Description |
|---|---|
service |
PAM service name (default: login) - Linux only |
allowed_users |
Optional list of allowed usernames |
allowed_groups |
Optional list of allowed groups (user must be in at least one) |
Requirements
Section titled “Requirements”- Linux: Requires a binary built with
CGO_ENABLED=1 -tags pamand the PAM development headers. The process must be able to authenticate against the configured PAM service (often needs root or a suitable/etc/pam.dentry). Without thepambuild tag the plugin fails closed. - macOS: Requires access to Directory Services via
dscl
OAuth/OIDC Authentication
Section titled “OAuth/OIDC Authentication”Authenticate using OAuth 2.0 or OpenID Connect.
auth: providers: - name: oauth type: oauth enabled: true priority: 1 config: provider: "generic" client_id: "${OAUTH_CLIENT_ID}" # Required client_secret: "${OAUTH_CLIENT_SECRET}" issuer_url: "https://auth.example.com" # Optional explicit endpoints; otherwise derived from issuer_url # introspect_url: "https://auth.example.com/oauth2/introspect" # userinfo_url: "https://auth.example.com/oauth2/userinfo" scopes: - openid - profile - email # required_claims: # tid: "your-tenant-id"required_claims gates every validated token on exact claim values: a missing
claim fails, strings compare exactly, booleans and numbers compare by their
canonical text form ("true", "42"), an array claim (aud-style) matches
when the expected value is one of its string elements, and an object-valued
claim never matches. Enforcement covers both validation paths (introspection
and userinfo). Leaving the setting out — or empty — enforces nothing.
client_id is required, as is at least one of issuer_url, introspect_url or
userinfo_url. There is no redirect_url: Bifrost validates a bearer token the
client already holds (sent as the proxy password) rather than running a browser
redirect flow, so no callback URL is involved.
Multiple Providers
Section titled “Multiple Providers”Providers are tried in priority order until one accepts the credentials. This
allows, for example, a static admin account alongside LDAP:
auth: providers: - name: break-glass type: native enabled: true priority: 1 config: users: - username: admin password_hash: "$2a$10$..." - name: corp-ldap type: ldap enabled: true priority: 2 config: url: "ldap://ldap.example.com:389" base_dn: "dc=example,dc=com" user_filter: "(uid=%s)"Using Authentication with Proxy
Section titled “Using Authentication with Proxy”HTTP Proxy Authentication
Section titled “HTTP Proxy Authentication”Clients authenticate using Proxy-Authorization header:
curl -x http://user:pass@localhost:7080 http://example.comSOCKS5 Authentication
Section titled “SOCKS5 Authentication”curl --socks5 user:pass@localhost:7180 http://example.comClient Authentication
Section titled “Client Authentication”The client can authenticate with the server:
server: address: "proxy.example.com:7080" username: "myuser" password: "mypass"API Key Authentication
Section titled “API Key Authentication”Authenticate using API keys passed in headers.
auth: providers: - name: apikey type: apikey enabled: true priority: 1 config: header_name: "X-API-Key" # Header to read the key from (default: X-API-Key) keys: - name: "service-a" key_hash: "$2a$10$..." # bcrypt hash of the API key groups: ["api-access"] - name: "service-b" key_hash: "$2a$10$..." groups: ["api-access", "admin"] expires_at: "2027-01-01T00:00:00Z" # Optional, RFC 3339API Key Configuration
Section titled “API Key Configuration”| Field | Description |
|---|---|
header_name |
Header to read the key from (default: X-API-Key) |
keys |
Required, non-empty list of keys |
keys[].name |
Required identifier, used as the authenticated username |
keys[].key_hash |
bcrypt hash of the key (use this in production) |
keys[].key_plain |
Plaintext key — accepted as an alternative to key_hash |
keys[].groups |
Optional group memberships |
keys[].expires_at |
Optional RFC 3339 expiry timestamp |
Generating API Key Hashes
Section titled “Generating API Key Hashes”# Generate a random API keyopenssl rand -base64 32
# Hash the key for storagepython3 -c "import bcrypt; print(bcrypt.hashpw(b'your-api-key', bcrypt.gensalt()).decode())"JWT Authentication
Section titled “JWT Authentication”Validate JWT tokens with JWKS support for key rotation.
auth: providers: - name: jwt type: jwt enabled: true priority: 1 config: issuer: "https://auth.example.com" audience: "bifrost-proxy" jwks_url: "https://auth.example.com/.well-known/jwks.json" # Or pin a static verification key instead of JWKS: # public_key_pem: "${JWT_PUBLIC_KEY_PEM}" # RSA/EC public key # hmac_secret: "${JWT_HMAC_SECRET}" # required for HS* algorithms username_claim: "sub" groups_claim: "groups" algorithms: - RS256 - ES256JWT Configuration
Section titled “JWT Configuration”| Field | Default | Description |
|---|---|---|
jwks_url |
- | URL to fetch the JSON Web Key Set from |
public_key_pem |
- | PEM-encoded RSA/EC public key (alternative to JWKS) |
hmac_secret |
- | Symmetric secret for HS256/HS384/HS512 |
issuer |
- | Expected issuer (iss claim) |
audience |
- | Expected audience (aud claim) |
algorithms |
["RS256"] |
Allowed signing algorithms |
username_claim |
sub |
Claim to use as username |
groups_claim |
groups |
Claim containing user groups |
email_claim |
email |
Claim containing the email address |
leeway_seconds |
60 |
Clock-skew allowance for exp/nbf |
jwks_refresh_interval |
1h |
How often to refetch the JWKS |
Exactly one key source is required: the provider fails to start unless one of
jwks_url,public_key_pemorhmac_secretis set. The allow-list key isalgorithms— there is noallowed_algorithmsand nosigning_key. Listing an HMAC algorithm withouthmac_secretis rejected at startup rather than failing on every request.
TOTP Authentication
Section titled “TOTP Authentication”Time-based One-Time Password authentication, compatible with Google Authenticator.
auth: providers: - name: totp type: totp enabled: true priority: 1 config: issuer: "Bifrost Proxy" digits: 6 # 6 or 8 period: 30 algorithm: "SHA1" # SHA1, SHA256 or SHA512 skew: 1 # Periods of clock skew to accept secrets: # A list, not a map - username: user1 secret: "JBSWY3DPEHPK3PXP" # Base32-encoded groups: ["staff"] - username: user2 secret: "GEZDGNBVGY3TQOJQ" disabled: false # Or keep secrets out of the main config: # secrets_file: "/etc/bifrost/totp-secrets.yaml"
secretsmust be a list of objects withusernameandsecretkeys. Ausername: secretmap is rejected at startup with “secrets must be an array”, and everysecrethas to be valid Base32.
Setting Up TOTP
Section titled “Setting Up TOTP”- Generate a secret for each user
- Share the secret with the user via QR code or manual entry
- User scans with authenticator app (Google Authenticator, Authy, etc.)
# Generate a random TOTP secretpython3 -c "import secrets; import base64; print(base64.b32encode(secrets.token_bytes(20)).decode())"HOTP Authentication
Section titled “HOTP Authentication”Counter-based One-Time Password authentication, compatible with YubiKey and similar hardware tokens.
auth: providers: - name: hotp type: hotp enabled: true priority: 1 config: digits: 6 # 6 or 8 algorithm: "SHA1" # SHA1, SHA256 or SHA512 secrets: # A list, not a map - username: user1 secret: "JBSWY3DPEHPK3PXP" # Base32-encoded counter: 0 - username: user2 secret: "GEZDGNBVGY3TQOJQ" counter: 100 look_ahead: 10 # Accept codes within this window # secrets_file: "/etc/bifrost/hotp-secrets.yaml"As with TOTP,
secretsis a list of objects (username,secret, and for HOTP the startingcounter) — a map keyed by username is rejected at startup.
mTLS Certificate Authentication
Section titled “mTLS Certificate Authentication”Client certificate authentication for mutual TLS.
auth: providers: - name: mtls type: mtls enabled: true priority: 1 config: ca_cert_file: "/path/to/ca.crt" # or ca_cert_pem for an inline PEM require_client_cert: true verify_time: true # Map certificate fields to the user identity subject_mapping: username_field: "CN" groups_field: "OU" email_field: "emailAddress" # Optional: restrict which certificates are accepted. # These are regular expressions matched against the full subject/issuer # DN string — not shell globs. allowed_subjects: - "^CN=client1\\.example\\.com,.*$" - ".*OU=Engineering.*" allowed_issuers: - ".*CN=Bifrost CA.*" crl_file: "/path/to/crl.pem"Exactly one of ca_cert_file / ca_cert_pem is required — the provider fails to
start without it. There is no ca_cert, require_cn, allowed_cns or
username_from key; CN restrictions are expressed as an allowed_subjects
regex. See the Advanced Authentication guide
for the full option list, listener-level client_auth setup and certificate
generation.
Certificate Requirements
Section titled “Certificate Requirements”- Client must present a valid certificate signed by the configured CA
- Certificate must not be expired (
verify_time) or listed incrl_file. A configuredcrl_filethat cannot be read or parsed is a fatal startup error: revocation checking either works or the provider refuses to run. Remove the key to run without revocation checking — it is never disabled implicitly - The field named by
subject_mapping.username_field(defaultCN) becomes the username
Kerberos/SPNEGO Authentication
Section titled “Kerberos/SPNEGO Authentication”Enterprise SSO using Kerberos with SPNEGO (HTTP Negotiate).
auth: providers: - name: kerberos type: kerberos enabled: true priority: 1 config: keytab_file: "/etc/krb5.keytab" service_principal: "HTTP/proxy.example.com" realm: "EXAMPLE.COM" strip_realm: true username_to_lowercase: trueHTTP browser SSO requires the Negotiate middleware. A bare
kerberosprovider validates username/password credentials. For transparent SPNEGO SSO over the HTTP proxy, also enable theauth.negotiateblock and pointkerberos_providerat this provider (see the Advanced Authentication guide).
Kerberos Setup
Section titled “Kerberos Setup”- Create a service principal for the proxy
- Export the keytab file
- Configure the client to use Kerberos (kinit)
# Create service principal (on KDC)kadmin -q "addprinc -randkey HTTP/proxy.example.com"kadmin -q "ktadd -k /etc/krb5.keytab HTTP/proxy.example.com"NTLM Authentication
Section titled “NTLM Authentication”Not functional — fails closed by design. The NTLM plugin cannot verify NTLM responses: Bifrost has no credential source (password/NT-hash store or domain-controller pass-through) against which to recompute and compare the client’s response. Accepting the attacker-supplied Type 3 message without cryptographic verification would be an authentication bypass, so the plugin rejects every login. It is retained only so a misconfiguration does not silently fall through to another provider. Do not rely on NTLM for access; use Kerberos,
native,ldap, oroauthinstead.
There is no working NTLM configuration. Any type: ntlm provider will fail
every authentication attempt with an “NTLM response verification is not
supported” error.
MFA Wrapper (Two-Factor Authentication)
Section titled “MFA Wrapper (Two-Factor Authentication)”Combine a primary authentication provider with an OTP provider for two-factor
authentication. The wrapper itself is a provider whose config embeds the
primary and secondary provider definitions.
auth: providers: - name: mfa type: mfa_wrapper enabled: true priority: 1 config: # Primary authentication (username/password) primary: mode: native config: users: - username: admin password_hash: "$2a$10$..." - username: user1 password_hash: "$2a$10$..."
# Secondary authentication (OTP) secondary: mode: totp config: issuer: "Bifrost" secrets: - username: admin secret: "JBSWY3DPEHPK3PXP" - username: user1 secret: "GEZDGNBVGY3TQOJQ"
# How the OTP is carried inside the password field password_format: separated # "separated" or "concatenated" separator: ":" # password:123456 mfa_code_length: 6 # Used by "concatenated" to split the tail mfa_required: always # always | per_user | group_basedBoth primary and secondary blocks are mandatory. If either is missing the
plugin falls back to resolving providers by name — which it cannot do — and the
server refuses to start with “referencing auth providers by name … is not
supported”. Each block takes its own mode (the plugin type) and config, and
the nested config must satisfy that plugin’s own schema (note the list-shaped
secrets above).
MFA Configuration
Section titled “MFA Configuration”| Field | Default | Description |
|---|---|---|
primary |
required | Inline first-factor authenticator (mode + config) |
secondary |
required | Inline OTP authenticator; mode is totp or hotp |
password_format |
concatenated |
How the OTP is embedded in the password |
separator |
: |
Separator for password_format: separated |
mfa_code_length |
6 |
OTP length, used to split a concatenated password |
mfa_required |
always |
always, per_user or group_based |
mfa_users |
- | Usernames requiring MFA (per_user mode) |
mfa_groups |
- | Groups requiring MFA (group_based mode) |
otp_separator is accepted as a legacy alias for separator (it also implies
password_format: separated).
MFA Authentication Flow
Section titled “MFA Authentication Flow”- The client sends the username and a password that carries the OTP
- The wrapper splits the OTP off the password using
password_format(password:123456when separated, or the trailingmfa_code_lengthdigits when concatenated) - The primary provider validates the password half, the secondary provider validates the OTP
- Both factors must pass for authentication to succeed
There is no OTP header. The code is read only out of the password field —
X-OTPand similar headers are not consulted, so the OTP has to be appended to the proxy password.
Session Management
Section titled “Session Management”Sessions for the Web UI / API can be stored in memory (default) or in Redis for persistence across restarts and sharing across replicas.
session: store: redis # "memory" (default) or "redis" duration: "24h" # Session lifetime (default 8h) max_sessions_per_user: 0 # 0 = unlimited cleanup_interval: "5m" # Memory store reap interval (ignored by Redis) redis: addr: "127.0.0.1:6379" # Required when store: redis password: "${REDIS_PASSWORD}" db: 0 key_prefix: "bifrost:session:" op_timeout: "5s"Session Stores
Section titled “Session Stores”| Store | Persistence | Scaling | Use Case |
|---|---|---|---|
memory |
No | Single instance | Development, simple deployments |
redis |
Yes | Multi-instance | Production, HA deployments |
Note: When
store: redisis selected,redis.addris required and the server fails to start if it is empty.
Authentication Plugin System
Section titled “Authentication Plugin System”The authentication system uses a plugin architecture. A plugin is a factory: it
validates a config map and builds an auth.Authenticator, which is the object
that actually checks credentials. Register the plugin from an init() function
so it is available by the time the config is loaded.
import ( "context" "fmt"
"github.com/rennerdo30/bifrost-proxy/internal/auth")
func init() { auth.RegisterPlugin("custom", &customPlugin{})}
// customPlugin implements auth.Plugin.type customPlugin struct{}
func (p *customPlugin) Type() string { return "custom" }func (p *customPlugin) Description() string { return "Example custom authenticator" }
func (p *customPlugin) ValidateConfig(config map[string]any) error { if _, ok := config["realm"].(string); !ok { return fmt.Errorf("custom auth config: 'realm' is required") } return nil}
func (p *customPlugin) Create(config map[string]any) (auth.Authenticator, error) { if err := p.ValidateConfig(config); err != nil { return nil, err } realm, _ := config["realm"].(string) return &customAuthenticator{realm: realm}, nil}
func (p *customPlugin) DefaultConfig() map[string]any { return map[string]any{"realm": "example"}}
func (p *customPlugin) ConfigSchema() string { return "" }
// customAuthenticator implements auth.Authenticator.type customAuthenticator struct{ realm string }
func (a *customAuthenticator) Name() string { return "custom" }func (a *customAuthenticator) Type() string { return "custom" }
func (a *customAuthenticator) Authenticate(ctx context.Context, username, password string) (*auth.UserInfo, error) { // Validate the credentials; return an error to reject. return &auth.UserInfo{Username: username, Metadata: map[string]string{"realm": a.realm}}, nil}Once registered, select it from the config like any built-in plugin:
auth: providers: - name: custom type: custom enabled: true priority: 1 config: realm: example