Server configuration
The UniSSH server is configured in layers: defaults → config.toml → environment. Environment keys use the form UNISSH__SECTION__KEY=... (double-underscore nesting). Secrets (TLS key, Postgres URL, and the optional ops token) should come from the environment or Docker secrets, never the committed file.
Start from the shipped template:
cp config.example.toml config.tomlSections
Section titled “Sections”[server]
Section titled “[server]”[server]bind = "0.0.0.0:8443"public_url = "" # external base URL for links/redirects; # empty → derived from the requesttls_cert = "/secrets/cert.pem" # in-process TLS 1.3 (rustls)tls_key = "/secrets/key.pem"trust_proxy = falseacme = falseSet tls_cert/tls_key for in-process rustls (TLS 1.3 only), or leave them empty and terminate TLS at a reverse proxy with trust_proxy = true. public_url is the externally reachable base URL (used when the server builds links); leave it empty to derive it from the request. A cors_allowed_origins key (a list, empty by default) adds extra browser origins for CORS — unneeded in the same-origin Compose deployment, where CORS stays off.
[db]backend = "sqlite" # "sqlite" | "postgres"url = "/app/data/unissh.db" # sqlite: file path (or ":memory:") # postgres: postgres://user:pass@host/dbmax_connections = 16[limits]
Section titled “[limits]”Request and object bounds, plus a per-IP rate limit.
[limits]max_body_bytes = 16777216 # 16 MiBmax_object_bytes = 1048576 # 1 MiBmax_objects_per_push = 1000delta_page_size = 500delta_max_page_size = 1000rate_limit_per_ip_rps = 20rate_limit_burst = 40[sync]
Section titled “[sync]”[sync]freshness_window_seconds = 30 # window for online-only live-grantsvalidate_signatures = true # defense-in-depth record-signature checksmin_instance_generation = 0 # anti-rollback floor (Σ next_seq); 0 = offvalidate_signatures(on by default) re-verifies each Vault/Item/Manifest/Grant record’s Ed25519 signature on write, byte-exact with the core, dropping forged/tampered objects early. This is defense-in-depth, not the security boundary — the client still re-verifies on read.min_instance_generationis an operator-anchored, out-of-band floor for the instance-wide sequence (next_seq). The server refuses to boot if a restored snapshot is below it, closing the new-client/TOFU rollback gap. Anchor this value outside the database. See Backups & anti-rollback restore and the sync model.
[session]
Section titled “[session]”Token and lifecycle TTLs (seconds):
[session]access_ttl_seconds = 900refresh_ttl_seconds = 2592000nonce_ttl_seconds = 120invite_default_ttl_seconds = 86400relay_ttl_seconds = 120janitor_interval_seconds = 300idempotency_ttl_seconds = 86400[obs]log_format = "json" # "json" | "text"otel_endpoint = "" # OTLP export is NOT compiled in: a value here # only warns at startup. Metrics: /metrics.metrics_bind = "127.0.0.1:9090"[setup]
Section titled “[setup]”Controls the one-time setup code that the first user presents to claim the instance and become its owner. There is no bootstrap token.
[setup]code = "" # empty → a random setup code is printed to the # server log on first boot (while unclaimed); # set a value to pin a deterministic code for IaC. # Env: UNISSH__SETUP__CODEThe server stores only sha256(code), and the code is valid only while the instance is unclaimed — a second claim is refused. Read the printed code with docker compose logs server 2>&1 | grep -i "setup code". Because only the hash is kept, a generated code cannot be printed a second time; if the log is gone, unissh-server setup-code --rotate --config <your config> issues a new one without touching any data (point it at the config you serve with — the default database path is relative, so a different working directory silently creates an empty one) (a pinned code is never printed at all — it came from you). See the Docker Compose deployment.
[oidc]
Section titled “[oidc]”Optional SSO (OpenID Connect). Disabled by default; when enabled, the login screen offers “Sign in with SSO”, and IdP groups map to space memberships (reconciled on every login).
[oidc]enabled = falseissuer = "" # IdP issuer URLclient_id = ""audience = "" # expected id_token `aud`; empty → client_idjwks_url = "" # empty → {issuer}/.well-known/jwks.jsongroups_claim = "groups" # id_token claim holding the group listmax_reassertion_age_seconds = 604800 # 7 days before a full OIDC re-auth# [[oidc.group_map]] # repeat per mapping: IdP group → space membership# group = "engineering"# space_id = "<space-id>"# role = "member"SSO asserts identity + space memberships only — never vault keys: the id_token is verified against the issuer JWKS and bound to the presented keyset via an OIDC nonce, and dropping an IdP group removes that space on the next login.
An optional break-glass operator surface (/v1/ops/*, header X-UniSSH-Ops-Token), off by default:
[ops]token = "" # empty → ops surface DISABLED (the default) # set via UNISSH__OPS__TOKENThis is server-trusted infrastructure access (overview / instance / seq-bump), not a keyset and never decryption. It is not how the admin panel normally signs in — the panel authenticates by escrow or SSO; the ops token is a last-resort infrastructure lever.
Environment overrides
Section titled “Environment overrides”Any key maps to an environment variable by uppercasing and joining with double underscores:
UNISSH__SERVER__BIND=0.0.0.0:8443UNISSH__SERVER__TRUST_PROXY=trueUNISSH__DB__BACKEND=postgresUNISSH__DB__URL=postgres://unissh:secret@postgres:5432/unisshUNISSH__SETUP__CODE=my-pinned-setup-code # optional — pin instead of a random oneUNISSH__OPS__TOKEN=$(openssl rand -hex 32) # optional — only if you enable break-glass opsGenerate strong tokens with openssl rand -hex 32. In the Compose stack these live in a gitignored .env; nothing secret is baked into images. See Docker Compose deployment.