In this section
Policy / strict schema

Configuration

TOML, YAML, and JSON enter the same strict schema. Heimdall rejects ambiguity before a command starts: unknown fields, duplicate objects, invalid references, unsupported capabilities, and contradictory DNS choices.

One file, one source of truth

Heimdall reads exactly one /etc/heimdall/config.<format> file. The extension selects the parser; it does not select a different product model.

Do not put passwords in config. Use an absolute password_file readable only by the invoking user. The foreground session removes one trailing newline and enforces the SOCKS5 length limit.

Offline schema and examples

heimdall config schema --version v1
heimdall config example --format toml
heimdall config example --format yaml
heimdall config example --format json

The JSON Schema is generated from the canonical Rust/Serde model, and each example is the same complete starter used by heimdall init. Both commands are read-only and work without a config file. Schema validation covers structure; finish with heimdall config validate --json for references and runtime capability checks.

Minimal configuration

config.toml
version = 1

[proxy]
default_policy = "default"

[proxy.outbounds.default]
type = "socks5"
server = "127.0.0.1"
server_port = 1080
network = ["tcp"]

[proxy.policies.default.dns]
mode = "fake"

[proxy.policies.default.final]
tcp = { type = "route", outbound = "default" }
udp = { type = "reject", method = "refused" }

[capture]
mode = "off"

[decrypt]
mode = "off"

Outbounds

The data plane supports SOCKS5 TCP CONNECT and UDP ASSOCIATE. Declare the network capability explicitly; a route action is rejected when its protocol is not declared by the selected outbound.

[proxy.outbounds.corp]
type = "socks5"
server = "127.0.0.1"
server_port = 1081
network = ["tcp", "udp"]
connect_timeout = "10s"

[proxy.outbounds.corp.auth]
username = "alice"
password_file = "/etc/heimdall/secrets/corp-password"

Ordered rules

Rules are evaluated in declaration order. The first matching rule wins. Fields inside one list are OR; different fields are AND.

[[proxy.policies.default.rules]]
name = "corp-domains"
action = { type = "route", outbound = "corp" }

[proxy.policies.default.rules.match]
network = ["tcp", "udp"]
domain_suffix = ["internal.example.com"]
port = [443]

[[proxy.policies.default.rules]]
name = "deny-smtp"
action = { type = "reject", method = "refused" }

[proxy.policies.default.rules.match]
network = ["tcp"]
port = [25]

[proxy.policies.default.final]
tcp = { type = "route", outbound = "default" }
udp = { type = "reject", method = "refused" }

There is no implicit first outbound and no fallback to direct egress. direct is an explicit authorization, and reject fails the connection or datagram.

DNS modes

ModeBehaviorPolicy identity
fakeRedirects DNS to Heimdall and preserves the hostname through the relay.Domain rules allowed.
systemAllows the host resolver to answer; the relay sees resolved IPs.Use IP, port, or protocol rules.

Use heimdall config explain --network udp ... --json to inspect a UDP decision. Without --network, the command explains TCP.

Capture and decrypt

Proxying decides where traffic goes. Capture decides whether bytes are retained. Decrypt decides what those retained bytes represent. These are separate opt-in choices.

[capture]
mode = "on"
max_bytes_per_flow = 1048576
block_max_bytes = 65536
flush_interval_ms = 100
boundaries = ["tls_plaintext.runtime"]
directions = ["client_to_remote", "remote_to_client"]
redact_env = ["API_TOKEN"]

[decrypt]
mode = "runtime" # off | runtime | relay

Payload bytes are stored below the private run directory as SHA-256 blobs. heimdall.event/v1 JSONL contains references and boundaries, never inline base64 or a second capture log.

Boundary and direction lists are payload allowlists. Values named by redact_env are read from the inherited environment and masked before hashing or publication, including matches split across observed reads. heimdall agent fails readiness when a named value is unavailable. Exact matching does not cover encoded or transformed variants.

Reads are coalesced per flow and direction into bounded blocks. Every payload event reports its block index, configured size/latency limits, and whether size, interval, or close caused the flush.