Skip to content

Security Guide

This guide covers security best practices for deploying and operating Bifrost in production environments.

server:
http:
listen: ":8443"
tls:
enabled: true
cert_file: "/etc/bifrost/certs/server.crt"
key_file: "/etc/bifrost/certs/server.key"

For testing only:

Terminal window
# Generate private key
openssl genrsa -out server.key 4096
# Generate certificate
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365 \
-subj "/CN=bifrost.example.com"

For production, use certificates from Let’s Encrypt:

Terminal window
# Install certbot
sudo apt install certbot
# Obtain certificate
sudo certbot certonly --standalone -d bifrost.example.com
# Certificate location
# /etc/letsencrypt/live/bifrost.example.com/fullchain.pem
# /etc/letsencrypt/live/bifrost.example.com/privkey.pem
server:
http:
tls:
enabled: true
cert_file: "/etc/letsencrypt/live/bifrost.example.com/fullchain.pem"
key_file: "/etc/letsencrypt/live/bifrost.example.com/privkey.pem"
Terminal window
# Secure key file
chmod 600 /etc/bifrost/certs/server.key
chown bifrost:bifrost /etc/bifrost/certs/server.key

Authentication is configured as an ordered list under auth.providers[], each entry naming a plugin type. (The legacy auth.mode key is rejected at startup — see the Authentication Guide.)

type Use Case Security Level
none Internal networks only Low
native Small deployments Medium
system Unix/PAM integration Medium-High — needs a custom build on Linux, see below
apikey Service-to-service access Medium-High
ldap Enterprise/AD integration High
oauth SSO / bearer-token validation High
jwt Signed-token validation (JWKS or pinned key) High
kerberos Active Directory SSO (with auth.negotiate) High
mtls Certificate / smart-card authentication High
totp, hotp, mfa_wrapper Second factor on top of a primary provider High
ntlm Non-functional — rejects every login n/a

Warning: system (PAM) fails closed in the stock builds. On Linux the PAM backend is compiled in only with CGO_ENABLED=1 and the pam build tag. The release binaries and the Docker image are built without both, so type: system rejects every login there (it logs a warning and returns false). Build from source with CGO_ENABLED=1 go build -tags pam ./... (needs libpam headers) if you need it, or use native/ldap/oauth instead. macOS uses dscl -authonly and needs no special build; Windows is not supported.

Warning: ntlm can never succeed. Bifrost has no credential source against which to verify an NTLM Type 3 response, so the plugin rejects every attempt by design rather than accepting unverified credentials. Use kerberos for Windows domain SSO.

Generate secure password hashes using bcrypt:

Terminal window
# Using htpasswd (Apache utils)
htpasswd -nbBC 12 "" "your-password" | cut -d: -f2
# Using Python
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(rounds=12)).decode())"

Warning: Password Hash Security

  • Always use bcrypt cost factor of 12 or higher
  • Never store plaintext passwords in config files
  • Rotate passwords regularly
auth:
providers:
- name: native
type: native
enabled: true
priority: 1
config:
users:
- username: admin
password_hash: "$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.gS6T9I2P9z8K2G"
auth:
providers:
- name: ldap
type: ldap
enabled: true
priority: 1
config:
url: "ldaps://ldap.example.com:636" # Use LDAPS
base_dn: "dc=example,dc=com"
tls: true
insecure_skip_verify: false # Always verify in production
bind_dn: "cn=bifrost-svc,ou=services,dc=example,dc=com"
bind_password: "${LDAP_BIND_PASSWORD}" # Use environment variable
group_lookup_fail_closed: true # Reject the login if the group lookup fails

url and base_dn are both required — the provider refuses to start without them.

Tip: LDAP Best Practices

  • Use a dedicated service account with minimal permissions
  • Store bind password in environment variable
  • Use LDAPS (port 636) instead of StartTLS
  • Verify server certificates
auth:
providers:
- name: oauth
type: oauth
enabled: true
priority: 1
config:
client_id: "${OAUTH_CLIENT_ID}"
client_secret: "${OAUTH_CLIENT_SECRET}" # Never commit to git
issuer_url: "https://auth.example.com"
scopes:
- openid
- profile

Bifrost validates a bearer token the client already holds (supplied as the proxy password), so there is no browser redirect and no redirect_url setting. Obtain the token from your identity provider over TLS and keep the proxy listener on HTTPS so the token is never sent in the clear.


Always set an API token for production:

api:
enabled: true
listen: ":7082"
token: "${BIFROST_API_TOKEN}"

Generate a secure token:

Terminal window
# Generate random token
openssl rand -hex 32
Terminal window
# Header authentication (recommended)
curl -H "Authorization: Bearer your-token" http://localhost:7082/api/v1/status
# Query parameter (less secure, avoid in production)
curl "http://localhost:7082/api/v1/status?token=your-token"

The web dashboard authenticates with the same api.token, but it does not keep the token in the browser when a session store is configured. Signing in POSTs the token once to /api/v1/login, which returns an HttpOnly session cookie:

api:
enabled: true
token: "${BIFROST_API_TOKEN}"
session:
store: memory # or redis
duration: "12h"

With that block present:

  • The token is sent once and not written to localStorage. The cookie is HttpOnly, so injected script cannot read it — a stored bearer token can be read by any XSS on the page.
  • The cookie also authenticates the WebSocket handshake, so no credential is placed in a URL. A browser cannot set headers on a WS upgrade, which is why the ?token= form exists at all.
  • Sign out (in the dashboard’s credential dialog) calls /api/v1/logout, which destroys the session server-side rather than only clearing browser state.
  • When the session expires the dashboard prompts for the token again instead of showing failures on every page.

Without a session: block, /api/v1/login answers 503 and the dashboard falls back to storing the token in localStorage and sending Authorization: Bearer. That still works, and the dialog says plainly that it is the weaker posture. Configuring a session store is the recommended production setup.

Bind the API to localhost if only local access is needed:

api:
listen: "127.0.0.1:7082" # Only localhost

Terminal window
# Allow proxy ports from specific networks
sudo ufw allow from 10.0.0.0/8 to any port 8080 proto tcp
sudo ufw allow from 10.0.0.0/8 to any port 1080 proto tcp
# Allow Web UI from admin network only
sudo ufw allow from 192.168.1.0/24 to any port 8081 proto tcp
# Block API from external access
sudo ufw deny 8082/tcp
Terminal window
# Allow proxy from internal network
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

Restrict which client IPs may use the proxy with two CIDR lists, whitelist and blacklist:

access_control:
# If non-empty, ONLY IPs matching one of these CIDRs are allowed.
# Leave empty to allow all IPs that are not blacklisted.
whitelist:
- "10.0.0.0/8"
- "192.168.0.0/16"
# IPs matching any of these CIDRs are always denied.
blacklist:
- "203.0.113.0/24"

Evaluation order:

  1. The blacklist is checked first — a match is always denied (blacklist takes precedence over the whitelist).
  2. If the whitelist is non-empty, the IP must match an entry in it; otherwise the request is denied.
  3. If the whitelist is empty, any IP not on the blacklist is allowed.

Both lists accept individual addresses (e.g. 192.168.1.10) as well as CIDR ranges. Access control is enforced only when at least one list is non-empty, and changes are hot-reloaded.

There is no enabled, default_action, or rules key — the presence of entries in the lists is what activates enforcement.


Protect against abuse with rate limiting:

rate_limit:
enabled: true
requests_per_second: 100
burst_size: 200
per_ip: true
per_user: true

Prevent bandwidth abuse:

rate_limit:
bandwidth:
enabled: true
upload: "10Mbps"
download: "100Mbps"

Never commit secrets to version control. Use environment variables:

# config.yaml
auth:
providers:
- name: ldap
type: ldap
enabled: true
priority: 1
config:
url: "ldaps://ldap.example.com:636"
base_dn: "dc=example,dc=com"
bind_dn: "cn=bifrost-svc,ou=services,dc=example,dc=com"
bind_password: "${LDAP_PASSWORD}"
api:
token: "${API_TOKEN}"
Terminal window
# Set environment variables
export LDAP_PASSWORD="secret"
export API_TOKEN="your-secure-token"
# Run with environment
bifrost-server -c config.yaml

Create /etc/bifrost/env:

Terminal window
LDAP_PASSWORD=secret
API_TOKEN=your-secure-token

Add to service file:

[Service]
EnvironmentFile=/etc/bifrost/env

Secure the file:

Terminal window
chmod 600 /etc/bifrost/env
chown root:bifrost /etc/bifrost/env
# docker-compose.yml
services:
bifrost-server:
environment:
- API_TOKEN_FILE=/run/secrets/api_token
secrets:
- api_token
secrets:
api_token:
file: ./secrets/api_token.txt

Bifrost automatically redacts sensitive data from logs, but review your configuration:

logging:
level: info # Avoid 'debug' in production
format: json

Warning: Log Review

  • Regularly review logs for sensitive data leaks
  • Don’t log full request/response bodies in production
  • Secure log files with appropriate permissions
Terminal window
chmod 640 /var/log/bifrost/*.log
chown bifrost:bifrost /var/log/bifrost/*.log

  • TLS enabled on all public listeners
  • Valid certificates (not self-signed)
  • Authentication enabled
  • API token set
  • Secrets in environment variables (not config)
  • Config file permissions restricted (600)
  • Firewall rules configured
  • Rate limiting enabled
  • Rotate API tokens quarterly
  • Update TLS certificates before expiry
  • Review access logs for anomalies
  • Update to latest Bifrost version
  • Audit user accounts
  • Test backup and recovery procedures
  • Document API endpoints and access
  • Have a process for revoking tokens
  • Know how to disable authentication temporarily
  • Have backup configurations ready
  • Know how to review logs quickly

If you discover a security vulnerability, please report it responsibly:

  1. Do not open a public GitHub issue
  2. Email the maintainers directly
  3. Include:
    • Description of the vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if any)

Subscribe to releases to stay informed about security updates:

Terminal window
# Watch the repository on GitHub
# Or check releases periodically
curl -s https://api.github.com/repos/rennerdo30/bifrost-proxy/releases/latest | jq -r .tag_name