Skip to content

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.mode field 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. Use auth.providers[] with an explicit type and a nested config map, as shown below.

Default behaviour - allows all connections.

auth:
providers:
- name: open
type: none
enabled: true
priority: 1

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: false

Using the bcrypt tool:

Terminal window
# Using htpasswd
htpasswd -bnBC 10 "" password | tr -d ':\n'
# Using Python
python3 -c "import bcrypt; print(bcrypt.hashpw(b'password', bcrypt.gensalt()).decode())"
# Using Go
go run -mod=mod github.com/rennerdo30/bifrost-proxy/tools/hashpw password

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: false
FieldDescription
urlLDAP server URL
base_dnBase DN for searches
bind_dnDN for service account
bind_passwordService account password
user_filterFilter to find users (%s = username)
group_filterFilter to find user groups
require_groupOnly allow users in this group

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, or oauth instead.

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
- staff
PlatformSupportMethod
Linux⚠️ Requires custom buildPAM (only in CGO_ENABLED=1 -tags pam builds; the default/Docker build fails closed)
macOS✅ SupportedDirectory Services (dscl)
Windows❌ Not SupportedUse native/ldap/oauth instead

Warning: The default (and Docker) binaries are built without the pam tag and with cgo disabled, so Linux system authentication fails closed — every login is rejected. To enable it, build from source with CGO_ENABLED=1 go build -tags pam ./... (requires libpam headers, e.g. libpam0g-dev / pam-devel). macOS does not need a special build.

FieldDescription
servicePAM service name (default: login) - Linux only
allowed_usersOptional list of allowed usernames
allowed_groupsOptional list of allowed groups (user must be in at least one)
  • Linux: Requires a binary built with CGO_ENABLED=1 -tags pam and the PAM development headers. The process must be able to authenticate against the configured PAM service (often needs root or a suitable /etc/pam.d entry). Without the pam build tag the plugin fails closed.
  • macOS: Requires access to Directory Services via dscl

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}"
client_secret: "${OAUTH_CLIENT_SECRET}"
issuer_url: "https://auth.example.com"
redirect_url: "http://localhost:7081/callback"
scopes:
- openid
- profile
- email

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)"

Clients authenticate using Proxy-Authorization header:

Terminal window
curl -x http://user:pass@localhost:7080 http://example.com
Terminal window
curl --socks5 user:pass@localhost:7180 http://example.com

The client can authenticate with the server:

server:
address: "proxy.example.com:7080"
username: "myuser"
password: "mypass"

Authenticate using API keys passed in headers.

auth:
providers:
- name: apikey
type: apikey
enabled: true
priority: 1
config:
header: "X-API-Key" # Header name to check
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"]
Terminal window
# Generate a random API key
openssl rand -base64 32
# Hash the key for storage
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-api-key', bcrypt.gensalt()).decode())"

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 use static key:
# signing_key: "${JWT_SIGNING_KEY}"
username_claim: "sub"
groups_claim: "groups"
allowed_algorithms:
- RS256
- ES256
FieldDescription
issuerExpected issuer (iss claim)
audienceExpected audience (aud claim)
jwks_urlURL to fetch JSON Web Key Set
signing_keyStatic signing key (alternative to JWKS)
username_claimClaim to use as username
groups_claimClaim containing user groups

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
period: 30
algorithm: "SHA1"
secrets:
user1: "JBSWY3DPEHPK3PXP" # Base32-encoded secret
user2: "GEZDGNBVGY3TQOJQ"
  1. Generate a secret for each user
  2. Share the secret with the user via QR code or manual entry
  3. User scans with authenticator app (Google Authenticator, Authy, etc.)
Terminal window
# Generate a random TOTP secret
python3 -c "import secrets; import base64; print(base64.b32encode(secrets.token_bytes(20)).decode())"

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
algorithm: "SHA1"
secrets:
user1:
secret: "JBSWY3DPEHPK3PXP"
counter: 0
user2:
secret: "GEZDGNBVGY3TQOJQ"
counter: 100
look_ahead: 10 # Accept codes within this window

Client certificate authentication for mutual TLS.

auth:
providers:
- name: mtls
type: mtls
enabled: true
priority: 1
config:
ca_cert: "/path/to/ca.crt"
# Optional: require specific certificate attributes
require_cn: true
allowed_cns:
- "client1.example.com"
- "*.internal.example.com"
# Map certificate subject to user
username_from: "cn" # cn, email, or custom OID
  • Client must present a valid certificate signed by the configured CA
  • Certificate must not be expired or revoked
  • Subject CN or email is used as username

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: true

HTTP browser SSO requires the Negotiate middleware. A bare kerberos provider validates username/password credentials. For transparent SPNEGO SSO over the HTTP proxy, also enable the auth.negotiate block and point kerberos_provider at this provider (see the Advanced Authentication guide).

  1. Create a service principal for the proxy
  2. Export the keytab file
  3. Configure the client to use Kerberos (kinit)
Terminal window
# Create service principal (on KDC)
kadmin -q "addprinc -randkey HTTP/proxy.example.com"
kadmin -q "ktadd -k /etc/krb5.keytab HTTP/proxy.example.com"

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, or oauth instead.

There is no working NTLM configuration. Any type: ntlm provider will fail every authentication attempt with an “NTLM response verification is not supported” error.

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:
admin: "JBSWY3DPEHPK3PXP"
user1: "GEZDGNBVGY3TQOJQ"
# How to submit OTP
otp_header: "X-OTP" # Or append to password with separator
otp_separator: ":" # password:123456
  1. Client sends username and password
  2. If OTP header is present, validate both
  3. If OTP is appended to password (password:123456), split and validate
  4. Both factors must pass for authentication to succeed

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"
StorePersistenceScalingUse Case
memoryNoSingle instanceDevelopment, simple deployments
redisYesMulti-instanceProduction, HA deployments

Note: When store: redis is selected, redis.addr is required and the server fails to start if it is empty.

The authentication system uses a plugin architecture. Custom authentication providers can be registered:

import "github.com/rennerdo30/bifrost-proxy/internal/auth"
func init() {
auth.RegisterPlugin("custom", &CustomAuthPlugin{})
}
type CustomAuthPlugin struct{}
func (p *CustomAuthPlugin) Name() string { return "custom" }
func (p *CustomAuthPlugin) Init(config map[string]interface{}) error {
// Initialize plugin
return nil
}
func (p *CustomAuthPlugin) Authenticate(ctx context.Context, creds auth.Credentials) (*auth.User, error) {
// Validate credentials
return &auth.User{Username: creds.Username}, nil
}
func (p *CustomAuthPlugin) Close() error {
return nil
}