Documentation for v2.11.0. Read the latest, v2.12.0.

Operator

Configuration

Everything the service takes from its environment is in one table below, one row per variable. What an administrator changes at runtime (cleanup ages, the cache size, the authentication mode, the webhook allow-list) lives in the database instead and is listed at the end.

What changed in 2.6.0

  • New: WEBHOOKS_ENABLED, WEBHOOK_WORKER_INTERVAL_SECS, WEBHOOK_DELIVERY_TIMEOUT_SECS and PUBLIC_BASE_URL for webhooks; LDAP_OPERATION_TIMEOUT_SECS for the LDAP stall timeout.
  • The data cache's 10 s entry lifetime is fixed, not a setting.

Environment variables#

A variable that is unset takes its default. A variable set to something the service cannot use stops the start (below); the service never falls back silently.

Variable Default Effect
DATABASE_URL sqlite:sanshain.db?mode=rwc The database. postgres://user:pass@host:5432/db (or postgresql://) selects PostgreSQL, anything else SQLite. The image sets sqlite:/data/sanshain.db?mode=rwc. Set but empty counts as unset.
BIND_ADDRESS 0.0.0.0:3000 Address and port to listen on. Binding all interfaces logs SECURITY WARNING: binding to all interfaces; 127.0.0.1:3000 restricts to localhost.
MAX_POSTGRES_CONNECTIONS 20 Pool size on PostgreSQL.
MAX_SQLITE_CONNECTIONS 5 Pool size on SQLite (WAL mode: readers beside one writer).
SQLITE_BUSY_TIMEOUT_MS 5000 How long a SQLite writer waits for the lock before failing.
CACHE_MEMORY_MB 256 Memory bound of the in-memory data cache; 0 disables it. The admin page can change it at runtime (below).
CLEANUP_INTERVAL_SECS 3600 Period of the background pass that expires snapshots, prunes stale dependencies, closes stale trunk data, removes expired sessions and tokens, settled webhook deliveries and old CSRF tokens. See Administration. Must be at least 1; 0 is refused at startup.
CSRF_MAX_AGE_HOURS 24 Age after which an unused CSRF token is pruned by that pass.
LOG_BUFFER_SIZE 100 Lines kept per level (error, warn, info, debug) in the in-memory log buffer the observability page shows.
SPEC_UPDATED_CHANNEL_SIZE 100 Capacity of the broadcast channel behind the SSE and WebSocket update streams; a slow subscriber lags past it.
MAX_SPEC_BODY_BYTES 4194304 Largest request body (4 MiB); a larger one is refused with 413 before parsing. 0 is refused at startup.
RUST_LOG sanshain_service=info,tower_http=warn The filter for the log written to stdout, in tracing_subscriber::EnvFilter syntax.
CAPTURE_LOG_FILTER sanshain_service=debug,tower_http=debug The filter for the in-memory buffer, independent of stdout.
LOG_FORMAT text json writes one JSON object per line to stdout.
OTEL_ENABLED false true exports traces over OTLP/gRPC; a failure to build the exporter stops the start. See Observability.
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 The collector the traces go to.
OTEL_EXPORTER_OTLP_HEADERS unset Read by the OpenTelemetry SDK, not by the service. Headers sent with every export, key=value,… (an auth token for the collector, for example).
OTEL_EXPORTER_OTLP_COMPRESSION unset Read by the OpenTelemetry SDK, not by the service. gzip or zstd compresses the export.
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Read by the OpenTelemetry SDK, not by the service. Export timeout in milliseconds.
OTEL_EXPORTER_OTLP_PROTOCOL grpc Read by the OpenTelemetry SDK, not by the service. The exporter is built for gRPC; another value is ignored by this build.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT unset Read by the OpenTelemetry SDK, not by the service. Trace-specific endpoint, overrides OTEL_EXPORTER_OTLP_ENDPOINT.
OTEL_EXPORTER_OTLP_TRACES_HEADERS unset Read by the OpenTelemetry SDK, not by the service. Trace-specific headers, override OTEL_EXPORTER_OTLP_HEADERS.
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION unset Read by the OpenTelemetry SDK, not by the service. Trace-specific compression, overrides OTEL_EXPORTER_OTLP_COMPRESSION.
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT unset Read by the OpenTelemetry SDK, not by the service. Trace-specific timeout, overrides OTEL_EXPORTER_OTLP_TIMEOUT.
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL unset Read by the OpenTelemetry SDK, not by the service. Trace-specific protocol, overrides OTEL_EXPORTER_OTLP_PROTOCOL.
STATIC_DIR static Directory of the web UI's files, served as the fallback route.
INSTANCE_ID a random UUID Identifies this process in the log and in GET /version.
INITIAL_ADMIN_USERNAME root Name of the account created on the first start, when no user exists. Also the fallback for SANSHAIN_ROOT_USERS.
INITIAL_ADMIN_PASSWORD random, printed to stderr Password of that account, read on the first start only. Set but empty counts as unset: the random password is generated and printed.
INITIAL_AUTH_MODE Off The auth mode of the first start, when no user exists: off, dev, local, ldap or oidc. Unset, a fresh instance starts in Off, where only the admin signs in and picks the mode. dev needs ALLOW_INSECURE_DEV_MODE=true. See Off.
SANSHAIN_ROOT_USERS INITIAL_ADMIN_USERNAME, else root Comma-separated usernames that hold every permission by configuration. See Root users.
SANSHAIN_DIRECTORY_GROUP_TTL_SECS 300 How long a user's directory group membership is cached; 0 re-reads it on every check. See Directory group caching.
SANSHAIN_DEV_MODE unset Ignored since 2.10; startup logs a warning while it is set. Developer mode is the Dev auth mode.
ALLOW_INSECURE_DEV_MODE false The safety gate: developer mode only takes effect when this is true; without it a requested dev mode is refused and logged. Never set in production.
EXTRA_CA_CERTS_DIR unset Directory of PEM certificates added to the trust store for LDAPS. See Additional CA certificates.
LDAP_OPERATION_TIMEOUT_SECS 10 How long one LDAP exchange (TLS or StartTLS handshake, bind, search, unbind) may take once the TCP connection is up. A value that is not a positive whole number is ignored and the default used.
MCP_ENABLED true Mounts the MCP endpoint at /mcp; false leaves it unmounted (404).
MCP_CALLS_PER_MINUTE 60 Tool calls one API token may make per minute: a token bucket of that size, refilled continuously, so a burst of 60 is allowed. 0 is refused at startup.
MCP_MAX_RESULT_BYTES 65536 A tool answer above this size is replaced by a summary. 0 is refused at startup.
WEBHOOKS_ENABLED true Writes webhook events to the outbox and runs the delivery worker. false writes and sends nothing; hooks can still be managed, and a test event is refused with 409.
WEBHOOK_WORKER_INTERVAL_SECS 5 How often the webhook worker looks for due deliveries. Must be greater than 0.
WEBHOOK_DELIVERY_TIMEOUT_SECS 10 How long one webhook POST may take before it counts as a failed attempt. Must be greater than 0.
PUBLIC_BASE_URL unset The URL people open Sanshain at (https://sanshain.corp), for the deep links in webhook messages and for the admin page named in the first-start message; a trailing slash is dropped and it must start with http:// or https://. Unset, the deep links use the origin of the OIDC redirect URL (with neither, they are omitted) and the first-start message names the bind address.

There is no variable for the metrics path or the session length: metrics are always at /metrics, a session always lasts 7 days.

Invalid values#

A numeric variable that is not a number, or a fixed-vocabulary variable (LOG_FORMAT, OTEL_ENABLED, MCP_ENABLED, WEBHOOKS_ENABLED) with an unrecognised value, makes the service exit before it binds, with the variable named on stderr:

CONFIGURATION ERROR: LOG_BUFFER_SIZE="1OO" is not a valid number. Correct it or unset it to use the default (100).
CONFIGURATION ERROR: LOG_FORMAT="yaml" is not recognised. Valid values: json, text. Unset it to use the default (text).

The same for values that would make the service useless: MAX_SPEC_BODY_BYTES=0, MCP_CALLS_PER_MINUTE=0, MCP_MAX_RESULT_BYTES=0, WEBHOOK_WORKER_INTERVAL_SECS=0, WEBHOOK_DELIVERY_TIMEOUT_SECS=0, CLEANUP_INTERVAL_SECS=0, a PUBLIC_BASE_URL without a scheme, and an unparsable BIND_ADDRESS or DATABASE_URL. Two variables are the exception and fall back quietly: LDAP_OPERATION_TIMEOUT_SECS and SANSHAIN_DIRECTORY_GROUP_TTL_SECS use their default when the value is not a whole number (and, for the LDAP timeout, when it is zero). Boolean-like variables read with a plain comparison (ALLOW_INSECURE_DEV_MODE) are true only for the exact string true.

Root users#

SANSHAIN_ROOT_USERS names the accounts that hold every permission, including ones a later release adds. Root is deliberately not stored: its authority comes from configuration, so no UI action, role change, migration or SQL update can revoke it. That makes it the recovery path when an administrator locks themselves out.

The list is read once at startup; changing it means a restart. Unset or empty, it falls back to INITIAL_ADMIN_USERNAME, then to root, so an existing deployment keeps a root account without being reconfigured. The resolved set is logged: Root users (configuration-held, cannot be revoked in-app): root.

Naming an account grants full authority whether or not it exists, is approved or holds a role, so keep the list short. Because the grant is a pure username match, an unclaimed root name would be a standing invitation: startup therefore creates a placeholder account for every configured name that has none, and registration refuses reserved names with 409 This username is reserved. The placeholder has no usable local password; the first LDAP or OIDC login under that name signs in as it, and it becomes that directory user's shadow account. The log says so: Claimed root username 'breakglass' with a placeholder account: ….

SANSHAIN_ROOT_USERS=root,breakglass

Directory group caching#

With LDAP, a user's group membership is read from the directory through the configured service account on every authorisation check and cached for SANSHAIN_DIRECTORY_GROUP_TTL_SECS. Sanshain stores which roles a directory group confers, never who is in it. The TTL is the dial between load on the directory and how long a revocation takes to bite; 0 disables caching, so every check reads the directory.

When the directory is unreachable, directory-derived roles are omitted, and the empty answer is cached for the TTL too, so an outage does not cost a directory timeout per request. Directory roles are purely additive, so leaving them out can only remove privilege. An outage degrades a directory-backed administrator to whatever Sanshain itself granted them; SANSHAIN_ROOT_USERS, held in configuration, still works. The alternative, a stale answer for the duration of the outage, would keep a revoked administrator in exactly that long.

The data cache#

Every data cache (resolved endpoints, endpoint lists per version, spec content, reports, Producer and Consumer lists, version metadata) holds an entry for at most 10 seconds. The value is fixed. A write invalidates the caches at once, and Sanshain runs as one instance, so the limit is never noticed; it is also why a second instance would serve stale data. See Scaling.

CACHE_MEMORY_MB is the initial memory bound. The admin page's In-Memory Cache section shows hit rate, entries and memory used, changes the bound at runtime (POST /admin/settings/cache, which rebuilds the caches) and clears them (POST /admin/cache/clear). The runtime value is not persisted; a restart returns to the variable.

Additional CA certificates#

Set EXTRA_CA_CERTS_DIR to a directory of PEM files when Sanshain must trust a private or corporate certificate authority, typically for LDAPS against an internal directory. Every *.pem, *.crt and *.cer file in the directory is read at startup and added to the platform trust store, never substituted for it. A file may hold one certificate or a chain; .crt is accepted because a Kubernetes Secret is conventionally keyed ca.crt. Files with any other name are ignored, so the entries a projected volume creates (..data and friends) do not interfere.

Startup fails rather than continuing with an incomplete store when:

On success the log says Trusting N additional CA certificate(s) from M file(s) in …; certificate contents are never logged. Certificates are read once, so rotating them means a restart. Unset, nothing changes: the platform bundle, which the image ships, is used.

This applies to LDAP connections. The database and the OTLP exporter have their own TLS configuration and do not consult this directory. Sanshain builds the LDAP trust store itself in every case, configured or not: left to its own, the LDAP library substitutes an empty root store when reading the platform certificates fails, which rejects every certificate and reports nothing.

Logging#

Two filters, two sinks. RUST_LOG governs stdout (LOG_FORMAT=json for machine-readable lines); CAPTURE_LOG_FILTER governs the in-memory buffer of LOG_BUFFER_SIZE lines per level that the observability page shows, so the buffer can hold debug lines while stdout stays at info. Two debug flags (business logic, admin/user activity) widen the buffer further at runtime without a restart. Secrets, tokens and passwords are never logged.

Settings in the database#

These are changed on the admin page or through /admin/settings/* and survive a restart. Each route needs a signed-in session; API tokens are not accepted on administrative routes.

Setting Default Route and permission
auth_mode Off on the first start, or INITIAL_AUTH_MODE GET/PUT /admin/auth-config, manage_auth_config. With the LDAP and OIDC configurations. See Authentication.
dev_mode off Follows the Dev mode of the same form; gated by the environment.
auth_mode_before_off none Written when the mode becomes Off: the sign-in source Off keeps for administrators. Not set directly.
registration_enabled false POST /admin/settings/registration, manage_settings. Applies in Local mode only.
auto_approve_users false GET/POST /admin/settings/auto-approve, manage_settings.
snapshot_max_age_days 30 GET/POST /admin/settings/snapshot-max-age, manage_settings. 0 disables.
dependency_max_age_days 30 GET/POST /admin/settings/dependency-max-age, manage_settings. Must be greater than 0.
trunk_max_age_days 90 GET/POST /admin/settings/trunk-max-age, manage_settings. 0 disables.
webhook_allowed_hosts [] GET/POST /admin/settings/webhook-allowed-hosts, manage_settings. See Webhooks.

Every *_max_age_days accepts at most 36500 days; a larger value answers 400 days must be at most 36500.

Checked against v2.6.1.