Configuration Guide
Configuration Guide
Section titled “Configuration Guide”Bifrost uses YAML configuration files. Environment variables can be used with ${VAR_NAME} syntax.
Server Configuration
Section titled “Server Configuration”Full Example
Section titled “Full Example”server: http: listen: ":7080" read_timeout: "30s" write_timeout: "30s" idle_timeout: "60s" tls: enabled: false cert_file: "/path/to/cert.pem" key_file: "/path/to/key.pem" socks5: listen: ":7180" graceful_period: "30s"
backends: - name: direct type: direct enabled: true
- name: wireguard type: wireguard enabled: true config: private_key: "${WG_PRIVATE_KEY}" address: "10.0.0.2/24" dns: ["1.1.1.1"] mtu: 1420 peer: public_key: "${WG_PEER_PUBLIC_KEY}" endpoint: "vpn.example.com:51820" allowed_ips: ["0.0.0.0/0"] persistent_keepalive: 25
routes: - domains: ["*.internal.com"] backend: wireguard priority: 100 - domains: ["*"] backend: direct priority: 1
auth: providers: - name: native type: native enabled: true priority: 1 config: users: - username: admin password_hash: "$2a$10$..."
rate_limit: enabled: true requests_per_second: 100 burst_size: 200 per_ip: true
access_log: enabled: true format: json output: "/var/log/bifrost/access.log"
metrics: enabled: true listen: ":7090" path: "/metrics"
logging: level: info format: json output: stdoutServer Settings
Section titled “Server Settings”| Field | Type | Default | Description |
|---|---|---|---|
server.http.listen |
string | :7080 |
HTTP proxy listen address |
server.socks5.listen |
string | :7180 |
SOCKS5 proxy listen address |
server.<listener>.read_timeout |
duration | 30s |
Inbound request read deadline (see below) |
server.<listener>.write_timeout |
duration | 30s |
No-progress bound on writes back to the client |
server.<listener>.idle_timeout |
duration | 60s |
Bound on a connection with nothing in flight |
server.<listener>.tunnel_idle_timeout |
duration | unset | Opt-in reaper for established tunnels quiet in both directions |
server.<listener>.max_connections |
int | 0 |
Per-listener ceiling; 0 = unlimited |
server.graceful_period |
duration | 30s |
Graceful shutdown period |
A listener is active when its listen address is set; there is no per-listener
enabled flag. At least one of the two listeners must be configured.
HTTP proxy protocol support
Section titled “HTTP proxy protocol support”The plain-HTTP forward-proxy path speaks HTTP/1.1 with one request per
connection — the proxy now says so on the wire (Connection: close on both
hops) instead of implying persistence and closing. CONNECT tunnels are
opaque, so HTTP/2 and WebSocket-over-TLS work end-to-end through them; a
plain-HTTP Upgrade (e.g. ws://) is honored by switching the connection to
an opaque tunnel after the origin’s 101. An h2c prior-knowledge request is
rejected with 505 HTTP Version Not Supported — HTTP/2 to the proxy itself is
not supported. Hop-by-hop headers (RFC 7230 §6.1) are stripped in both
directions and the proxy appends itself to Via.
Listener timeouts
Section titled “Listener timeouts”All three timeouts describe the inbound client connection. None of them
affects outbound dials to a backend — see
Network Settings for network.dial_timeout. Set any of
them to 0 to disable that deadline and leave the connection bounded only by
its peer.
read_timeout— the complete inbound request must arrive within it, measured from the client’s first byte: the request line and header block for HTTP, the whole handshake (method negotiation, optional username/password exchange, connect request) for SOCKS5. This is an absolute bound, which is what makes it effective against a client that trickles a request one byte at a time. It applies to TLS-terminated listeners too — the TLS handshake must complete within it — and to every decrypted request on a MITM-intercepted tunnel. Once the headers are in it applies per read instead, so a large upload may take arbitrarily long overall as long as it keeps making progress.write_timeout— a no-progress bound on writes back to the client: each window ofwrite_timeoutmust deliver at least one byte. A streaming response (server-sent events, a chunked feed, a large download) is never truncated while it keeps moving — even to a very slow receiver — but a client that has stopped reading entirely is timed out within one window. (On a TLS-terminated listener a stalled window is fatal to the connection, a constraint of Go’s TLS implementation; a progressing response is still never cut off.)idle_timeout— bounds a connection with no request in flight: one that has been accepted but has sent nothing, or the wait between exchanges on a kept-alive loop, including a MITM-intercepted tunnel. It deliberately does not apply to an established opaqueCONNECTtunnel or SOCKS5 relay.tunnel_idle_timeout— off by default. When set, an established tunnel or relay in which neither direction has carried data for the period is closed. An actively transferring tunnel is never interrupted, even to a very slow receiver — each window only requires progress, not completion.
The same three listener timeouts exist on the client’s proxy.http and
proxy.socks5 blocks and are applied the same way; they default to 0
(disabled) there, since the client’s listeners are typically loopback-only.
SOCKS5 protocol support
Section titled “SOCKS5 protocol support”The SOCKS5 listener implements the CONNECT command only. BIND (0x02) and
UDP ASSOCIATE (0x03) are answered with reply code 0x07
(“command not supported”), so inbound-connection and UDP relaying — QUIC/HTTP-3,
DNS-over-UDP, WebRTC media, some BitTorrent and FTP-active flows — cannot be
proxied over SOCKS5. Use the HTTP listener, or VPN mode for a TUN
device that carries UDP. Supported authentication methods are no-auth (0x00) and
username/password (0x02); GSSAPI (0x01) is not offered.
Logging Settings
Section titled “Logging Settings”| Field | Type | Default | Description |
|---|---|---|---|
logging.level |
string | info |
debug, info, warn or error |
logging.format |
string | text |
json or text |
logging.output |
string | stdout |
stdout, stderr or a file path |
logging.time_format |
string | RFC3339 with ms | Go time layout for the timestamp |
logging.max_size_mb |
int | 0 |
File outputs only: rotate above this size; <= 0 disables built-in rotation |
logging.max_backups |
int | 0 |
Rotated files to retain; <= 0 keeps all |
See Log Rotation for the rotation behaviour and the
external-logrotate alternative.
Backend Types
Section titled “Backend Types”direct- Direct connectionwireguard- WireGuard tunnelopenvpn- OpenVPN tunnelhttp_proxy- Upstream HTTP proxysocks5_proxy- Upstream SOCKS5 proxynordvpn,mullvad,pia,protonvpn- Commercial VPN providers (see VPN Providers)
Route Configuration
Section titled “Route Configuration”Routes match domains to backends. Higher priority routes are evaluated first.
routes: - name: "internal" # Optional name domains: # Domain patterns - "*.internal.com" - "internal.com" backend: wireguard # Backend name priority: 100 # Higher = evaluated firstPattern formats:
example.com- Exact match*.example.com- Wildcard subdomain.example.com- Suffix match (matches domain and all subdomains)*- Match all
Load balancing across several backends
Section titled “Load balancing across several backends”A route may name several backends instead of one. With load_balance: weighted,
weights sets the relative share each receives; a backend absent from the map
gets a weight of 1.
routes: - name: "internal" domains: ["*.internal.com"] backends: ["wireguard-a", "wireguard-b"] load_balance: weighted # round_robin | least_conn | random | weighted weights: wireguard-a: 3 # receives 3 of every 4 requests wireguard-b: 1Web UI Settings
Section titled “Web UI Settings”web_ui: enabled: true listen: "127.0.0.1:7081" base_path: "/bifrost" # serve the dashboard under a sub-pathbase_path is optional in every setup: the dashboard derives its prefix from
the browser URL at runtime, so serving it under a sub-path behind a reverse
proxy works with or without this setting (the Go server does not consume the
value). If the proxy also rewrites Host, add the browser-visible origin to
api.allowed_origins, or the dashboard’s WebSocket will be refused.
Network Settings
Section titled “Network Settings”Process-wide tuning for outbound dials. Every field is optional; omitted values keep the Go and OS defaults. This section requires a restart.
network: ipv6: true # false restricts outbound dials to IPv4 prefer_ipv6: false # prefer AAAA when a host resolves to both keepalive: 30s # TCP keep-alive on outbound dials; negative disables max_connections: 0 # ceiling across all listeners; 0 = unlimitedipv6 is deliberately three-state: leaving it unset means dual-stack, exactly
as true does, so an absent value never narrows the address family. Only an
explicit ipv6: false restricts dialling to IPv4.
dial_timeout is the default timeout for outbound connections to a backend.
It applies only where the backend does not already derive a timeout from its own
configuration — it will not override a backend-specific value. It has no bearing
on inbound connections; those timeouts come from the per-listener
server.http and server.socks5 settings.
API Settings
Section titled “API Settings”api: enabled: true listen: "127.0.0.1:7082" token: "..." # optional; when unset the API has no auth allowed_origins: # extra browser origins allowed to open a WebSocket - "https://bifrost.example.com"allowed_origins matters when a reverse proxy rewrites Host, and it also
grants the Host check that blocks DNS rebinding. Both rules, the matching
semantics, and the ["*"] opt-out are documented in
the WebSocket API reference.
MITM Inspection
Section titled “MITM Inspection”Off by default. When enabled, the proxy terminates TLS with a certificate it mints per host from a CA you supply, so request bodies become visible to the traffic debugger. Both files are required, and the CA private key lets the proxy impersonate any host — treat it accordingly.
mitm: enabled: false ca_cert_file: "/etc/bifrost/mitm-ca.pem" ca_key_file: "/etc/bifrost/mitm-ca.key" leaf_ttl: 24h # lifetime of each minted host certificate max_cached_certs: 100 # LRU bound on minted certificates held in memoryClients must trust ca_cert_file, or every intercepted connection fails its
certificate check. Changing this section requires a restart.
Client Configuration
Section titled “Client Configuration”Full Example
Section titled “Full Example”proxy: http: listen: "127.0.0.1:7380" socks5: listen: "127.0.0.1:7381"
server: address: "proxy.example.com:7080" protocol: http username: "${PROXY_USER}" password: "${PROXY_PASS}" timeout: "30s" retry_count: 3 retry_delay: "1s"
routes: - domains: ["localhost", "127.0.0.1", "*.local"] action: direct priority: 100 - domains: ["*"] action: server priority: 1
debug: enabled: true max_entries: 1000 capture_body: false
web_ui: enabled: true listen: "127.0.0.1:7382"
tray: enabled: true start_minimized: falseClient Route Actions
Section titled “Client Route Actions”server- Route through Bifrost serverdirect- Connect directly
Environment Variables
Section titled “Environment Variables”Use ${VAR_NAME} syntax for environment variable expansion:
server: http: listen: ":${HTTP_PORT}"auth: providers: - name: native type: native enabled: true priority: 1 config: users: - username: admin password_hash: "${ADMIN_PASSWORD_HASH}"Expansion syntax
Section titled “Expansion syntax”The whole grammar is three forms. Everything else is literal text.
| Form | Meaning |
|---|---|
${NAME} |
Value of NAME. If NAME is not set the reference expands to the empty string and the loader logs a warning naming the variable and the line. |
${NAME:-fallback} |
Value of NAME when it is set and non-empty, otherwise the literal fallback. The fallback is used verbatim and is not itself expanded. |
$$ |
A literal $. Only needed in front of a { or another $. |
server: http: # Defaults to :8080 when HTTP_PORT is unset or empty listen: ":${HTTP_PORT:-8080}"Literal dollar signs are safe
Section titled “Literal dollar signs are safe”A $ that is not followed by { or $ is literal, so passwords, API keys and
bcrypt hashes pass through untouched:
auth: providers: - name: native type: native enabled: true priority: 1 config: users: - username: admin # Kept exactly as written password_hash: "$2a$10$N9qo8uLOickgx2ZMRZoMye"Write $$ only where a value must literally contain ${, for example
token: "a$${not_expanded}b", which loads as a${not_expanded}b.
Bare
$NAMEis not expanded. Only the braced forms above are. This is what makes a literal$in a value safe — expansion runs on the raw file before YAML parsing, so quoting cannot protect a value. If a bare$NAMEnames a variable that is set in the environment, the loader warns with the line number so the mismatch is visible; add the braces to expand it.
Expansion is a single pass. A value substituted from the environment is never expanded again, so a secret that itself contains
${is safe.
Unknown Keys Are Rejected
Section titled “Unknown Keys Are Rejected”A key that does not correspond to a setting is an error, not a silent no-op. The loader reports every offending key in one pass, with its line and the config block it appeared in:
Error: config file /etc/bifrost/config.yaml contains 2 unknown key(s):line 3: "listem" (in ListenerConfig); line 12: "max_conns" (in ListenerConfig);remove or correct the key, or set BIFROST_CONFIG_ALLOW_UNKNOWN_KEYS=1 todowngrade unknown keys to warnings while migratingThis applies everywhere a config file is read: server and client startup,
bifrost-server config validate, hot reload, and the dashboard’s config API. A
rejected hot reload leaves the running configuration in place.
The check reaches into the dynamic sections too: backends[].config and
auth.providers[].config are validated against the exact keys their backend
type or provider plugin reads — including nested blocks such as a WireGuard
peer, a native users entry, or an inline mfa_wrapper authenticator. A
config file must also contain exactly one YAML document; content after a ---
separator used to be silently ignored and is now an error.
Every shipped example config and both init templates pass this check, so a file
that follows this guide is unaffected. A hand-edited file carrying a typo, or a
key from an older release, will now fail to start — which is the point: a
misspelled key used to look exactly like a setting that does not work.
Escape hatch
Section titled “Escape hatch”If a deployment has to come up before the file can be cleaned, set
BIFROST_CONFIG_ALLOW_UNKNOWN_KEYS=1 (or true, yes, on). Unknown keys are
then logged as warnings and ignored:
WARN ignoring unknown configuration key config=/etc/bifrost/config.yaml line=3 key=listem section=ListenerConfig hint="this key has no effect; ..."Treat this as temporary. A key that is ignored is a setting that is not applied, and the variable is documented as a migration aid rather than a supported mode.
Genuine type errors — a string where a number belongs, for instance — always fail, with or without the variable set.