Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration reference

Every SithBit binary follows the same contract: an empty or missing config file is a runnable dev instance. Every setting has an in-code default — loopback binds, unprivileged ports, a local SQLite store — so configuration is only ever overriding a default, never satisfying a required field. The one exception is called out below (TLS certificate paths, which have no sensible default).

The annotated example files are the canonical per-key documentation and ship with every default shown commented out:

  • mail_spooler/sithbitd.example.toml
  • account_api/account_api.toml
  • domain_sithbit/domain_sithbit.example.toml
  • ipfs_daemon/sithbit_ipfsd.example.toml
  • ipfs_gateway/ipfs_gateway.example.toml
  • mail_grpc/mail_grpc.example.toml
  • pop_server/pop_server.toml
  • imap_server/imap_server.toml
  • smtp_server/smtp_server.toml
  • mail_migrate/sithbit_migrate.example.toml
  • mail_console/sithbit_console.toml

The last three ship under the config file’s own name rather than an .example copy, and are the one place a setting is left live instead of commented out — see Standalone protocol servers for the two dev-only exceptions they make.

How a setting resolves

Eleven binaries take an annotated TOML file of their own — sithbitd, account-api, domain-sithbit, sithbit-ipfsd, sithbit-gateway, mail-grpc, the three standalone protocol servers pop-server, imap-server and smtp-server, the store-migration tool sithbit-migrate, and the admin TUI sithbit-console. Every one of them layers each setting the same way, lowest to highest precedence:

  1. the in-code default,
  2. the TOML file (its own file name in the working directory, or the path in its *_CONFIG environment variable — both named in the table below),
  3. an optional cloud app-config source — AWS AppConfig or Azure App Configuration, opted into per binary by a bootstrap env var; skipped entirely when neither var is set,
  4. ./.env,
  5. ./.env.$APP_ENV (APP_ENV comes from the environment or ./.env),
  6. the real environment.

Environment variables address individual settings as {PREFIX}_{PATH}, with __ descending one TOML nesting level:

SITHBITD_STORE__KIND=aws                       # [store] kind
SITHBITD_SMTP__SERVER__BIND_ADDR=0.0.0.0:2525  # [smtp.server] bind_addr
SITHBITD_STORE__BLOBS__KIND=azure              # [store.blobs] kind (tagged enum)

Each binary’s file, config-path variable, and prefix:

BinaryConfig fileConfig-path variableEnv prefix
sithbitdsithbitd.tomlSITHBITD_CONFIGSITHBITD
account-apiaccount_api.tomlACCOUNT_API_CONFIGACCOUNT_API
domain-sithbitdomain_sithbit.tomlDOMAIN_SITHBIT_CONFIGDOMAIN_SITHBIT
sithbit-ipfsdsithbit_ipfsd.tomlSITHBIT_IPFSD_CONFIGSITHBIT_IPFSD
sithbit-gatewayipfs_gateway.tomlIPFS_GATEWAY_CONFIGIPFS_GATEWAY
mail-grpcmail_grpc.tomlMAIL_GRPC_CONFIGMAIL_GRPC
pop-serverpop_server.tomlPOP_SERVER_CONFIGPOP_SERVER
imap-serverimap_server.tomlIMAP_SERVER_CONFIGIMAP_SERVER
smtp-serversmtp_server.tomlSMTP_SERVER_CONFIGSMTP_SERVER
sithbit-migratesithbit_migrate.tomlSITHBIT_MIGRATE_CONFIGSITHBIT_MIGRATE
sithbit-consolesithbit_console.tomlSITHBIT_CONSOLE_CONFIGSITHBIT_CONSOLE

Env files are read from the working directory only, and their values are exported to the process environment without overriding variables that are already set.

Key sources: files or cloud secret managers

Six file-loaded secrets take a key source rather than a bare path: the account-api JWT signing key (jwt.key_file), the DKIM signing key(s) ([spooler.dkim] / [mail.dkim] key_file), the credential-sealing key (credential_key_file), every server’s TLS certificate and key (certs / key, the account-api’s [tls] included — it shares the same section shape), domain-sithbit’s delegate signing key (delegate_key_file), and mail-grpc’s signing/fee-payer keypair (keypair). A key source is either a local file (the default) or a cloud secret manager’s secret — Azure Key Vault (akv), AWS Secrets Manager (asm), or Google Secret Manager (gsm) — in one of these TOML forms:

key_file = "jwt.key"                    # 1. bare path string  -> a local file
key_file = { path = "jwt.key" }         # 2. table, kind omitted -> a local file
key_file = { kind = "akv",              # 3. an Azure Key Vault secret
             vault_uri = "https://<vault>.vault.azure.net/",
             secret_name = "jwt-signing-key" }
key_file = { kind = "asm",              # 4. an AWS Secrets Manager secret
             secret_id = "sithbit/jwt-signing-key" }
key_file = { kind = "gsm",              # 5. a Google Secret Manager secret
             project = "my-project",
             secret = "jwt-signing-key" }

The representation defaults to a file, so a config that names a plain path — or omits kind — keeps the historical, zero-config file behaviour byte-for-byte; only an explicit cloud kind opts into a secret manager. This holds for every key-source field whatever it is named (key_file, credential_key_file, delegate_key_file, keypair, certs / key).

A cloud secret’s value holds exactly what the file would have — the raw key bytes, or the PEM. The TLS pair therefore fetches two secrets — the certificate-chain PEM and the private-key PEM — one per certs/key entry. No secret material lives in the config file itself; it carries only the secret’s coordinates. Per cloud:

  • akv names the vault_uri and secret_name. Authentication reuses the same managed-identity credential chain as the Azure storage backend (azure_identity’s ManagedIdentity): no new auth to configure.
  • asm names the secret_id — a secret name or a full ARN — with optional region (defaults to the ambient AWS configuration’s) and endpoint_url (an emulator such as LocalStack, mirroring the AWS storage backend’s setting). Authentication is the ambient AWS credential chain (environment, profile, IAM role). A string secret’s UTF-8 bytes are used; a binary secret’s raw bytes serve as the fallback when no string value is present.
  • gsm names the project and secret, with optional version (default "latest"). Authentication is Application Default Credentials (workload identity, GOOGLE_APPLICATION_CREDENTIALS, or a gcloud user login).

A table carries only its own kind’s fields. A field belonging to a different kind — vault_uri under kind = "asm", project under kind = "akv" — is refused by name when the config loads, rather than parsed and silently discarded. A half-finished migration between secret managers therefore fails loudly at startup instead of quietly reading from the source you believed you had left behind. The likeliest shape is a stale environment override: {PREFIX}_..._KIND switched to a new kind while the old kind’s ..._VAULT_URI (or ..._SECRET_ID) is still exported, since those layer on top of the TOML. Each kind’s own optional fields — asm’s region and endpoint_url, gsm’s version — are unaffected.

A key belonging to no kind is refused the same way, naming it: key = { kind = "asm", secret_id = "…", regoin = "us-east-1" } fails with table has an unknown field `regoin` . That is the message a plain typo produces, and it is worth knowing which mistakes it now catches that previously passed silently — a misspelled optional field, as above, which used to leave the setting at its default, and a stray key alongside an otherwise-valid table. A typo in a required field always failed, but blamed the absence (requires `path` ) rather than the cause; it now names the typo instead.

Two mistakes that used to escape by name no longer do. A kind whose value is unknown fails with kind = "avk" is not a known kind; expected one of "file", "akv", "asm", "gsm" — the list quoted in the message is the same list that accepts a spelling, so the two cannot drift apart. A field of the right name but the wrong type names both the field and what it found: path = 5 fails with table field `path` must be a string, found integer. Spellings stay exact and case-sensitive, so kind = "AKV" is refused rather than quietly accepted.

Both used to fail while the table was still being parsed, where the untagged form retried its other shapes and reported only “data did not match any variant”. Neither the config that is accepted nor the config that is refused has changed — only which of them tells you why. One gap remains: a key that is neither a string nor a table (key = 5) still reports the untagged message, because it matches no shape at all.

Every kind parses in every build. The clouds themselves are cargo features of the key-source crate (akv / asm / gsm, all on by default); a build that compiles one out still accepts the config but fails at load with an error naming the feature to enable, so a slimmed operator build can drop the SDKs it never uses (see Slim-build features for per-binary slim build commands).

(Cloudflare has no equivalent backend by design: its Secrets Store / Workers secrets are write-only over the API — only a deployed Worker binding can read a value — so a fetch-style key source cannot exist.)

There is no local Key Vault emulator, so the live AKV fetch is exercised only by an #[ignore]d probe pointed at a real vault; the ASM and GSM twins have the same shape, except that an ASM probe can target LocalStack instead of the real cloud. Each probe runs only when its environment variables are set, and the roster of those variables lives in key_source/README.md’s Tests section (outside this book) rather than being restated here — a test in that crate scans its sources and fails if a gate variable is missing from that section, so the crate’s list is the one that cannot fall behind. The per-kind dispatch is unit-tested against a fake fetcher, so CI covers the dispatch and a real cloud covers the round trip.

Cloud app-config sources: AWS AppConfig or Azure App Configuration

When mounting a TOML file into every container or VM is the awkward part of a deployment — orchestrated fleets, serverless units, config that several instances must share — a binary can pull its settings from a managed configuration store instead: AWS AppConfig or Azure App Configuration. The cloud tier slots into the resolution chain above directly after the TOML file: it overrides the file, and is itself overridden by ./.env, ./.env.$APP_ENV, and the real environment — so a {PREFIX}_{PATH} override still beats a cloud value, exactly as it beats the file. This tier carries settings, not secrets: key material keeps going through key sources, and a cloud config value holds at most a secret’s coordinates, never the secret.

Opting in is pure environment — no TOML key, no code change. Each binary derives six bootstrap variables from its prefix (SITHBITD, ACCOUNT_API, DOMAIN_SITHBIT, MAIL_GRPC, SITHBIT_IPFSD, IPFS_GATEWAY):

VariableMeaning
{PREFIX}_AWSAPPCONFIGSelect AWS AppConfig: application/environment/profile (exactly three non-empty segments)
{PREFIX}_AWSAPPCONFIG_REGIONOptional region override (default: the ambient AWS configuration’s)
{PREFIX}_AWSAPPCONFIG_ENDPOINTOptional endpoint URL, for emulators/local fakes
{PREFIX}_AZAPPCONFIGSelect Azure App Configuration: the store’s https://<name>.azconfig.io endpoint
{PREFIX}_AZAPPCONFIG_LABELOptional label filter; unset reads the NULL label only, never every label
{PREFIX}_AZAPPCONFIG_PREFIXOptional key prefix, server-filtered and stripped before mapping (e.g. sithbitd:)

With neither primary variable set the tier is skipped entirely — the zero-config contract is untouched. Setting both is a load error (pick one provider per binary). The bootstrap variables are control inputs, not settings, and may themselves come from a .env file.

The payload idiom differs per provider:

  • AWS AppConfig holds one whole TOML document in a freeform configuration profile. It deep-merges over the config file: nested tables merge key-wise, so one cloud document can override a section without erasing its siblings; scalars and arrays replace wholesale. Each load opens a fresh AppConfigData session and makes a single GetLatestConfiguration call — configuration is read once at startup, so picking up a new deployment means restarting the binary.
  • Azure App Configuration holds per-key values: : in a key descends one TOML nesting level (store:kind sets store.kind), and values get the same TOML-scalar parsing as env overrides. Keys are case-sensitive — spell them exactly like the TOML keys. A scalar-vs-table collision between keys fails the load rather than letting listing order decide.

Both providers authenticate ambiently — the AWS credential chain, the Azure managed identity — the same posture as the key sources above. The backends are cargo features of the app-config crate (awsconf / azconf, both on by default); a build that compiles one out fails at load with an error naming the feature to rebuild with when its provider is selected, so a slimmed operator build can drop the SDK it never uses (see Slim-build features for per-binary slim build commands).

The mapping logic (bootstrap parsing, TOML merge, key nesting) is unit-tested against injected fake fetches, so CI covers it without credentials. The live round trips are #[ignore]d probes — the AWS one needs ambient AWS credentials and can be aimed at an emulator instead of the real service, the Azure one needs an ambient managed identity — and each runs only when its environment variables are set. The roster of those variables lives in app_config/README.md’s Tests section (outside this book) rather than being restated here — a test in that crate scans its sources and fails if a gate variable is missing from that section, so the crate’s list is the one that cannot fall behind.

You do not have to author the store content from scratch: the repository ships ready-to-import production documents for both providers — a complete six-service deployment shape with dummy secret coordinates — under iac/appconfig/, together with the az appconfig kv import / aws appconfig runbooks and the generator that keeps the two flavors in lock-step. See iac/README.md and the deployment chapter.

Going public: which settings must change

Every per-binary page below marks the rows an operator has to revisit before a listener leaves loopback. The same three-way legend applies on every page, in the example TOMLs, and in the iac/appconfig/ production documents:

  • REQUIRED (public) — a reachable deployment is unsafe or non-functional until the operator sets this deliberately: a public bind address, the TLS certificate behind it, the hostname clients and peers are told, a store shared across instances, a secret, or the gateway’s mutual-TLS table once the gateway enforces [auth].
  • RECOMMENDED (public) — the in-code default is safe, but a public deployment should choose the value consciously: rate limits and budgets, blocklists, sender-authentication policy, quotas, telemetry.
  • unmarked — the default is right as shipped; going public changes nothing.

A marker is an operator obligation, not a startup check. Two binaries couple a non-loopback bind to a credential and refuse to start without it — mail-grpc to its [auth] section, sithbit-ipfsd to its auth_token — and no other binary validates its configuration against the address it binds. Where a row’s setting is enforced at startup for some other reason (a missing field, an empty allow-list, a mismatched scheme) the row says so — otherwise assume a misconfigured public instance starts cleanly and serves.

[health] and [observability] — every binary

Two sections shared by every TOML binary — mail-grpc included, since it moved onto the same layering:

KeyDefaultMeaning
health.bind_addr127.0.0.1:<per-binary port>The /healthz + /readyz listener; each binary defaults its own port (8190–8198, table in Monitoring)
health.enabledtrueDisable to serve no health endpoints (--health-probe then exits 1)
observability.otlp(absent — no export)Presence of the section enables OTLP push of traces + metrics
observability.otlp.endpoint"http://127.0.0.1:4317"Collector gRPC endpoint. The standard OTEL_EXPORTER_OTLP_*ENDPOINT env vars silently override this — leave them unset
observability.otlp.metrics_interval_seconds60Metric push cadence

Every binary also accepts a --health-probe argument: load the same config, GET the health listener’s /readyz, exit 0/1 — this is what the compose healthcheck: entries run inside the distroless images. See Monitoring and backups for the endpoints, the metric list, and the docker-compose.otel.yml collector overlay.

What’s on each page

The settings above — resolution order, key sources, cloud app-config sources, and the two sections every binary shares — apply across the whole reference. Each binary’s own settings are on its own page: