Configuration reference
This page documents the layered configuration as implemented in the current
v0.5.x series (internal/config/). Every key below exists in the code
today.
Layering
Section titled “Layering”Precedence, highest to lowest (SRS FR-003, verified in config.LoadFor):
- Command-line flags — see the CLI reference.
TOBBY_*environment variables.- The YAML configuration file —
/etc/tobby/config.yamlby default, or the--configpath. The default location is optional; an explicitly given path must exist. - Built-in defaults.
Validation runs on the merged result, per command scope: what is set must always be coherent, what is absent is only an error when the command needs it. A contradictory configuration (a base path without a destination, credentials without a proxy, half of a TLS pair) is refused at startup rather than silently ignored — a setting that reads like it works must work.
Environment variable mapping
Section titled “Environment variable mapping”The rule is mechanical: TOBBY_ plus the configuration path in upper snake
case — network.proxy.url becomes TOBBY_NETWORK_PROXY_URL. Each
supported variable is declared explicitly in the code (internal/config/env.go);
the full list appears in the key tables below.
Two TOBBY_* variables are not configuration keys and appear in no
table below: TOBBY_INSTANCE_URL and TOBBY_API_TOKEN, which tell a
command which running instance to drive and with what credential — see
tobby sync. They configure a client,
not an instance, so an instance never reads them.
Value syntax in the environment:
- Booleans:
true/1andfalse/0. - Durations: Go syntax —
30s,15m,12h. - Sizes: a unit suffix (
64MiB,512kB, case-insensitive) or a bare byte count. - Lists: comma-separated (
TOBBY_REGISTRIES_INSECURE=reg1:5000,reg2). Only three list keys have an environment form; every other structured or list-valued key (trust roots, scopes, substitutions, filesets…) is configuration file only.
Top level and directories
Section titled “Top level and directories”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
mode |
passthrough | mirror |
(none — required to serve) | TOBBY_MODE |
The operating mode. There is deliberately no default: an instance must state what it is. |
zone |
string | (none) | TOBBY_ZONE |
This instance’s zone identity — the metadata.name of the Retriever that serves the zone. A source-side instance reads it from the Retriever it resolves and needs nothing here; a destination-side instance has no Retriever, its content arrives on a medium, and without this key it cannot tell whether a medium is addressed to it. tobby media verify and tobby media import refuse to run without it. |
storage.root |
path | (none — required to serve) | TOBBY_STORAGE_ROOT |
The self-contained store: artifacts, recipes, task history and logs all live under it. |
storage.basePrefix |
repository path | (none) | TOBBY_STORAGE_BASE_PREFIX |
Optional relocation base prefix, applied identically to every ingredient of the instance. |
storage.occupancyThreshold |
size | (none — unmonitored) | TOBBY_STORAGE_OCCUPANCY_THRESHOLD |
On-disk footprint past which the store raises a persistent warning on every UI page, reports exceeded on the API, and moves the tobby_store_occupancy_exceeded metric. Both crossings are visible: coming back under the threshold retracts the warning and the metric. Unset means unmonitored, never “within limits” — Tobby cannot guess the size of the volume it was given. |
state.root |
path | (none — required unless auth is disabled) | TOBBY_STATE_ROOT |
The instance state: accounts, tokens, trust-root cache, partial downloads. Strictly outside the store — secrets never travel on the media, and this directory is the single backup target. A state root inside the storage root (or the reverse) is refused. |
Server
Section titled “Server”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
server.addr |
host:port | :8080 |
TOBBY_SERVER_ADDR |
Listen address of the single listener (UI, API, registry, probes, metrics). |
server.secureCookies |
bool | false |
TOBBY_SERVER_SECURE_COOKIES |
Marks UI cookies Secure when a reverse proxy terminates TLS in front of a plain-HTTP listener. Stated explicitly by the operator: Tobby refuses to trust spoofable forwarding headers for security attributes. When the listener serves TLS itself, cookies are Secure regardless. |
server.tls.enabled |
bool | false |
TOBBY_SERVER_TLS_ENABLED |
Serves TLS with a self-signed fallback certificate (fingerprint logged at startup). Supplying a certificate pair implies TLS without this flag. |
server.tls.certFile |
path | (none) | TOBBY_SERVER_TLS_CERT_FILE |
Administrator-supplied PEM certificate. Both or neither of the pair. Re-read when the files change on disk: replacing them replaces the served certificate without a restart. |
server.tls.keyFile |
path | (none) | TOBBY_SERVER_TLS_KEY_FILE |
The matching PEM private key. |
server.tls.hosts |
list | (none) | (file only) | Extra subject alternative names for the generated self-signed certificate. Refused when a certificate is supplied — it would silently do nothing. |
Authentication
Section titled “Authentication”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
auth.disabled |
bool | false |
TOBBY_AUTH_DISABLED |
Switches authentication off for every surface. A deliberate opt-in — settable in the file or the environment, never by flag — audited at startup and bannered permanently in the UI. See Authentication, accounts and RBAC. |
auth.sessionTTL |
duration | 12h |
TOBBY_AUTH_SESSION_TTL |
UI session lifetime. Sessions live in memory: a restart signs everyone out. |
auth.session.idleTimeout |
duration | 30m |
(file only) | Ends a UI session after this long without user activity: navigations and actions renew it, the page’s own background polls do not. Must be positive. auth.sessionTTL still bounds the session’s whole life. Each expiry is audited as session.expire (R-14). |
auth.tokens.defaultLifetime |
duration | (none) | (file only) | Gives API tokens created without an explicit expiry one this far in the future. Unset keeps the long-lived token. An expired token is refused with TBY-AUTH-013 and the refusal is audited as token.expire (R-14). |
auth.oidc.enabled |
bool | false |
(file only) | OpenID Connect sign-in (FR-070), beside the local methods. See Enterprise identity. |
auth.oidc.issuer |
URL | (none) | (file only) | The provider’s issuer, https:// only; discovery happens from it and nowhere else, and the tokens’ iss must match it exactly. |
auth.oidc.clientID |
string | (none) | (file only) | This instance’s client identifier — the audience its ID tokens must name. |
auth.oidc.clientSecretFile |
path | (none) | (file only) | The client secret, in a file outside the store (NFR-020); never the value itself. |
auth.oidc.redirectURL |
URL | (none) | (file only) | The callback registered at the provider; its path must be /auth/oidc/callback. |
auth.oidc.scopes |
list | (none) | (file only) | Scopes requested beyond openid. |
auth.oidc.groupsClaim |
string | groups |
(file only) | The ID token claim carrying the groups. |
auth.oidc.nameClaim |
string | preferred_username |
(file only) | The claim shown as the person’s name, on screen and as actor_name in logs and audit. Display only: the identity key is always sub. |
auth.oidc.signingAlgorithms |
list | every asymmetric algorithm | (file only) | Allow-list of ID token signatures. none and HS* are refused. |
auth.oidc.displayName |
string | OpenID Connect |
(file only) | Label of the sign-in button. |
auth.oidc.roles |
map | (none) | (file only) | viewer, operator, admin: the groups granting each role. Highest wins; no match is a refusal. Required non-empty when enabled. |
auth.saml.enabled |
bool | false |
(file only) | SAML 2.0 sign-in, SP-initiated only (FR-071). |
auth.saml.entityID |
string | (none) | (file only) | This service provider’s entity ID — the audience assertions must name. |
auth.saml.acsURL |
URL | (none) | (file only) | The assertion consumer service; its path must be /auth/saml/acs. Responses must name it as their Destination. |
auth.saml.idpMetadataFile |
path | (none) | (file only) | The identity provider’s metadata, pinned. Exactly one of this and idpMetadataURL. |
auth.saml.idpMetadataURL |
URL | (none) | (file only) | Fetch the metadata over https:// instead — an explicit outbound connection (NFR-019). Requires idpCertificateFile. |
auth.saml.idpCertificateFile |
path | (none) | (file only) | The provider’s signing certificate (PEM), pinned. Required with idpMetadataURL; with a metadata file, replaces the certificates it carries. |
auth.saml.certFile, auth.saml.keyFile |
paths | (none) | (file only) | The service provider’s key pair, which signs its requests. The key is a secret path (NFR-020). |
auth.saml.groupsAttribute |
string | groups |
(file only) | The assertion attribute (Name or FriendlyName) carrying the groups. |
auth.saml.nameAttribute |
string | (none) | (file only) | An attribute shown as the person’s name, on screen and as actor_name in logs and audit. Display only: the identity key is always the persistent NameID. Without it the NameID is shown. |
auth.saml.clockSkew |
duration | 90s |
(file only) | Tolerated clock difference with the provider, both directions; at most 5m. |
auth.saml.displayName |
string | SAML |
(file only) | Label of the sign-in button. |
auth.saml.roles |
map | (none) | (file only) | As auth.oidc.roles. |
Outbound network
Section titled “Outbound network”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
network.proxy.url |
URL | (none) | TOBBY_NETWORK_PROXY_URL |
Forward proxy for all outbound traffic — one setting, one transport, every path. https:// proxies are accepted. Credentials in the URL are refused: they have their own fields. |
network.proxy.httpsURL |
URL | (none) | TOBBY_NETWORK_PROXY_HTTPS_URL |
Separate proxy for https:// destinations when they take a different route. Empty means url serves both. |
network.proxy.noProxy |
list | (none) | TOBBY_NETWORK_PROXY_NO_PROXY |
Destinations reached directly: a host, a .suffix, a CIDR block, or *. |
network.proxy.username |
string | (none) | TOBBY_NETWORK_PROXY_USERNAME |
Proxy credential. |
network.proxy.password |
secret | (none) | TOBBY_NETWORK_PROXY_PASSWORD |
Proxy credential. Never a flag (visible in the process table); redacted by construction in logs, errors and config dump. |
network.tls.caFiles |
list of paths | (none) | TOBBY_NETWORK_TLS_CA_FILES |
PEM bundles of private certificate authorities trusted in addition to the public ones — for registries, Helm repositories, the retriever, trust-root URLs and the proxy hop alike. |
network.tls.ca |
PEM string | (none) | (file only) | Inline PEM bundle, for deployments that inject configuration but cannot mount a file. |
network.tls.exclusiveTrust |
bool | false |
(file only) | Drops the host’s public root store, leaving only the configured authorities. Only ever narrows trust. There is no setting anywhere that disables TLS verification. |
Registries
Section titled “Registries”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
registries.insecure |
list of hosts | (none) | TOBBY_REGISTRIES_INSECURE |
Hosts reached over plain HTTP. Per-host and explicit, never a global switch. Not the answer to a private PKI — that is network.tls. |
registries.substitutions |
map host → base | (none) | (file only) | Fetch-time source substitution: a downstream zone fetches docker.io/… from its upstream zone registry without modifying the recipes. Changes only the endpoint contacted, never the relocated path. The base is a host, plus at most the upstream’s base path; the full canonical path, origin host included, is appended to it — see what the substitute registry must hold. |
registries.allowlist |
list of host patterns | (absent = unrestricted) | (file only) | Bounds which registries the instance may contact at all, evaluated on the host actually reached. Absent and [] are different statements: absent means no restriction (reported as undeclared), [] means nothing is allowed. Globs: * within a DNS label, ** across labels. |
registries.credentialsFile |
path | (none) | (file only) | A kubernetes.io/dockerconfigjson payload; credentials are looked up by the effective host contacted, whichever direction the bytes travel. Must sit outside the transportable store. |
Content flow
Section titled “Content flow”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
retriever.source |
file path, HTTPS URL or OCI ref | (none) | TOBBY_RETRIEVER_SOURCE |
Where the desired-state document comes from. See Zone Retriever and cascading. |
destination.registry |
bare host | (none — empty promotes nothing) | TOBBY_DESTINATION_REGISTRY |
The zone registry this instance promotes into. A bare host, never a URL and never a path: the scheme follows registries.insecure, the path is computed by the relocation convention. |
destination.basePath |
repository path | (none) | TOBBY_DESTINATION_BASE_PATH |
Path prefix under which relocated ingredients land on the destination. Distinct from storage.basePrefix on purpose. Refused without a destination. |
destination.cookbook |
repository path | cookbook |
TOBBY_DESTINATION_COOKBOOK |
Where the zone’s recipes are re-published with their signatures. |
sync.parallelism |
int | 3 |
TOBBY_SYNC_PARALLELISM |
Caps concurrent ingredient transfers. |
sync.retries |
int | 3 |
TOBBY_SYNC_RETRIES |
Per-ingredient retry attempts on transient failures (bounded backoff). |
sync.interval |
duration | 15m |
TOBBY_SYNC_INTERVAL |
Reconciliation cadence, passthrough mode only (mirror-mode synchronization is manual by requirement). 0 disables the loop, reported at startup. An operator override persisted in the state directory wins over this value and is audited. |
sync.prune |
bool | false |
TOBBY_SYNC_PRUNE |
Passthrough only. Removes, at the end of every reconciliation cycle, the recipe-managed content the resolved Retriever no longer references (FR-045). Off by default: a transit store is not a delivery unit, and refreshing it never implies shrinking it. Unit imports, the offline vulnerability database and content seeded through /v2/ are never eligible. Every removal is listed in the run log. In mirror mode the setting is refused — prune is on by default there and confirmed at trigger time, against the projected list and total size. |
import.inspectTimeout |
duration | 20s |
TOBBY_IMPORT_INSPECT_TIMEOUT |
Deadline of one remote inspection on the unit-import screens; a hit maps to the dedicated TBY-REG-004. |
transfer.resumeThreshold |
size | 64MiB |
TOBBY_TRANSFER_RESUME_THRESHOLD |
Blob size from which a download becomes resumable inside the blob (spooled in the state directory, resumed by HTTP Range). 0 disables in-blob resumption and streams every blob straight to the store. |
preflight.safetyMarginPercent |
int 0–99 | 10 |
TOBBY_PREFLIGHT_SAFETY_MARGIN_PERCENT |
Share of the target’s free space held back by the pre-flight check: a synchronization is refused before any transfer when the projection exceeds free space minus this margin, stating the shortfall (TBY-STO-004). The margin exists because the store is never the only writer on its volume. 0 restores the default — an absent key must not silently mean “fill the volume”. |
preflight.disabled |
bool | false |
TOBBY_PREFLIGHT_DISABLED |
Turns the pre-flight gate into a report: volumes and filesystem verdicts are still computed and still shown, and they no longer refuse a synchronization. An explicit, announced removal of a safety check — logged at startup and again every time it lets a refusal through. |
tasks.keepFinished |
int | 500 |
TOBBY_TASKS_KEEP_FINISHED |
Finished tasks retained (newest first); older ones are purged with their log files. Pending and running tasks are never purged. 0 keeps the whole history. |
Configuration file only. Verification is on by default for every recipe; relaxation exists only as explicitly declared scopes, never a global bypass. The full model is on Signatures, trust roots and allowlist.
| Key | Type | Purpose |
|---|---|---|
trust.roots[] |
list | Trusted public keys (cosign, key-based). Each root has a name and exactly one of key (inline PEM), keyFile, keyURL (https:// only, fetched and cached at configuration time — never at verification time). Multiple roots enable rotation by overlap. |
trust.scopes[] |
list | Declared relaxation perimeters, evaluated in order, first match wins. Each scope has a name, repositories glob patterns on the recipe’s canonical nominal cookbook path (* within a segment, ** across; a port’s : is written _), and must change something: allowUnsigned: true and/or a roots restriction. Relaxed scopes are reported on every surface, never silent. |
Vulnerability scanning
Section titled “Vulnerability scanning”Every image is scanned at retrieval — a declared container image, and any
ingredient whose content is one, whatever its declared kind (ADR-0017) —
in passthrough as in mirror mode (FR-031, ADR-0008). There is deliberately no enabled switch:
the scan always runs and its results are always visible — the default
advisory mode reports violations on every surface and lets the content
through, with a permanent banner saying so. The Retriever’s
spec.scanPolicy and the recipe annotation recipe.tobby.dev/scan-threshold
refine the instance level; the model is on
Vulnerability scanning.
| Key | Type | Default | Environment | Purpose |
|---|---|---|---|---|
scan.mode |
string | warn |
TOBBY_SCAN_MODE |
warn (advisory) or block: a violating item is then refused — never pushed, never written on a medium — and so is an item that could not be scanned. A threshold posed by the Retriever or a recipe makes the policy blocking for its perimeter even under warn. |
scan.threshold |
number | 7.0 |
TOBBY_SCAN_THRESHOLD |
CVSS base score from which a finding violates the policy, inclusive, a finite number from 0 to 10 (NaN and infinities are refused, in the file as in the variable). A finding without a CVSS score sits at the lower bound of its severity band (LOW 0.1, MEDIUM 4.0, HIGH 7.0, CRITICAL 9.0); an UNKNOWN one is reported as unscored, never compared. |
scan.exceptions[] |
list | none | — | CVE identifiers tolerated beyond the threshold: id, optional justification, optional expires (RFC 3339). Added to the Retriever’s own list. An expired exception no longer applies and is reported as expired. Configuration file only. |
scan.maxDbAge |
duration | 168h |
TOBBY_SCAN_MAX_DB_AGE |
Maximum age of the vulnerability database, measured from the build timestamp it records (R-13). Must be positive. |
scan.maxDbAgeAction |
string | warn |
TOBBY_SCAN_MAX_DB_AGE_ACTION |
Beyond maxDbAge: warn reports staleness on the report, the UI and the metrics; block refuses items scanned with a stale database (TBY-SCN-004), whatever scan.mode says. |
scan.timeout |
duration | 10m |
TOBBY_SCAN_TIMEOUT |
Bound of one scan, the scanner process included. A timed-out item is reported not scanned. |
scan.dbRepository |
string | relocation of ghcr.io/aquasecurity/trivy-db |
TOBBY_SCAN_DB_REPOSITORY |
Repository of this store holding the database, when it is not the relocation of the upstream one the tobby-system recipe carries. |
scan.trivy.path |
string | none | TOBBY_SCAN_TRIVY_PATH |
An explicit scanner binary, absolute. Without scan.trivy.digest it is accepted only when it is the release this build pins (Trivy 0.76.2); on macOS any trivy is accepted, unpinned. |
scan.trivy.digest |
string | none | TOBBY_SCAN_TRIVY_DIGEST |
sha256:<hex> pin of scan.trivy.path, verified before every execution. |
scan.trivy.ref |
string | ghcr.io/aquasecurity/trivy |
TOBBY_SCAN_TRIVY_REF |
Nominal reference of the tobby-system FileSet carrying the binary on Linux and Windows hosts. Exclusive with scan.trivy.path. |
Where the binary comes from, per platform: the container image bundles
the pinned release at /usr/libexec/tobby/trivy; a Linux or Windows host
binary takes it from the tobby-system channel; macOS takes the first
trivy on the PATH. Missing or unverifiable, it is the degraded
mode: a startup log record, a danger banner, tobby_scan_scanner_available 0, and every item reported not scanned with the reason — refused under a
blocking policy.
FileSets
Section titled “FileSets”Configuration file only. Serving is disabled by default: only FileSets
listed here are served under /files/<name>/.
| Key | Type | Purpose |
|---|---|---|
files.packRoots |
list of absolute paths | Confines FileSet packing as reached from the web interface and the API (POST /filesets/pack, POST /api/v1/filesets/pack): those surfaces may pack a directory only if it sits under one of these paths. The default — no entry — refuses every path and hides the form, because reading an arbitrary host directory on request is a capability an instance should be given, not one it should have. A relative path is refused at startup. tobby fileset pack on the host itself is unaffected: whoever runs it already holds the filesystem’s own rights. |
files.filesets[].name |
URL segment | Serves under /files/<name>/. |
files.filesets[].ref |
host + repository | The nominal ingredient reference of the FileSet — no tag or digest; the served content is whatever verified digest the store holds. |
files.filesets[].version |
tag | Pins the served tag; empty serves the highest semver tag present locally. |
files.filesets[].platform |
e.g. linux/amd64 |
Selects the platform manifest of a multi-platform FileSet; an ambiguous index without it is refused. |
files.filesets[].anonymous |
bool | Opts this FileSet into unauthenticated reads (bare-host bootstrap) — reported, never silent. |
UI, logging, shutdown
Section titled “UI, logging, shutdown”| Key | Type | Default | Env | Purpose |
|---|---|---|---|---|
ui.themeOverride |
path | (none) | TOBBY_UI_THEME_OVERRIDE |
Operator stylesheet served after the embedded design tokens: rebranding without rebuild. The default tokens pass WCAG AA; overrides carry that responsibility. |
ui.showUpcoming |
bool | false |
TOBBY_UI_SHOW_UPCOMING |
Renders future-milestone navigation entries as inert, labeled placeholders (demo mode). Production navigation shows only what works. |
logging.media.file |
store-relative path | _tobby/logs/operations.log |
(file only) | Where the operation log is written on the transport medium (mirror mode only): the return audit channel, so whoever receives the medium can read what was done with it. The path must lie outside the media manifest’s coverage — under _tobby/ — and the instance refuses to start otherwise: a log inside coverage invalidates, line by line, the inventory the destination verifies. |
logging.media.maxSize |
size | 10MiB |
(file only) | Size-based rotation threshold of that log. |
logging.media.keep |
int | 3 |
(file only) | How many rotated generations to keep. Bounds the log at maxSize × (keep + 1) on a medium whose whole point is to carry gigabytes of content. |
logging.media.disabled |
bool | false |
(file only) | Turns the medium’s log off. Explicit and never a default: a medium arriving without one cannot be audited by whoever receives it. |
logging.level |
string | info |
TOBBY_LOGGING_LEVEL |
debug, info, warn, or error. |
shutdown.gracePeriod |
duration | 30s |
TOBBY_SHUTDOWN_GRACE_PERIOD |
Drain budget after SIGTERM/SIGINT. |
There is no configuration key for the UI language: the CLI follows the host locale, the web UI negotiates its language per user.
Checking the effective configuration
Section titled “Checking the effective configuration”tobby config dump prints the merged result of all four layers as YAML on
stdout — the exact configuration the instance would run with. Secrets are
redacted by construction: sensitive values are carried in a type that
cannot serialize its content, so a credential reaching the dump (or a log
line, or an error message) is impossible rather than reviewed against.
A configured secret dumps as REDACTED; an absent one as an empty string.
Use it as the control tool: after any change, diff
tobby config dump against what you intended, then restart.
Complete production example
Section titled “Complete production example”An annotated passthrough instance promoting into a zone registry, behind a corporate proxy, with a private CA and a bounded registry perimeter:
# /etc/tobby/config.yaml — passthrough instance of the "prod" zone.mode: passthrough
storage: root: /var/lib/tobby/storage # the self-contained storestate: root: /var/lib/tobby/state # accounts, tokens — the backup target, # never inside the store
server: addr: :8080 secureCookies: true # TLS terminates on the ingress in front # of this listener
network: proxy: url: http://proxy.example.com:3128 noProxy: [registry.zone.example.com, .cluster.local] username: tobby-egress # password: set through TOBBY_NETWORK_PROXY_PASSWORD — never in a flag, # never printed anywhere. tls: caFiles: [/etc/tobby/pki/corporate-ca.pem] # private CA, verification stays on
registries: credentialsFile: /etc/tobby/dockerconfig.json # kubernetes.io/dockerconfigjson allowlist: # everything else is refused pre-transfer - registry.upstream.example.com - registry.zone.example.com substitutions: # fetch docker.io content from upstream zone docker.io: registry.upstream.example.com # a host: the origin path is appended
retriever: source: https://registry.upstream.example.com/zone/retriever.yaml
destination: registry: registry.zone.example.com # bare host — path comes from relocation cookbook: cookbook # where recipes are re-published, signed
trust: roots: - name: qualification-2026 keyFile: /etc/tobby/pki/cosign-qualification-2026.pub - name: qualification-2025 # rotation by overlap keyFile: /etc/tobby/pki/cosign-qualification-2025.pub
sync: parallelism: 3 retries: 3 interval: 15m
preflight: safetyMarginPercent: 10 # refuse before filling the volume (FR-055)
scan: mode: block # refuse what violates the policy (FR-031) threshold: 7.0 # CVSS, inclusive exceptions: - id: CVE-2024-12345 justification: vulnerable parser disabled in our build expires: 2027-03-31T00:00:00Z maxDbAge: 168h # the database must have crossed this week maxDbAgeAction: warn
logging: level: infoshutdown: gracePeriod: 30sDeployment-specific variants (Helm values, systemd unit, offline packages) live in Deploy: Kubernetes and VM.