Configuration
SwitchTender is one binary with subcommands. This page lists every command, flag, and environment variable.
Licensed features
Almost everything on this page runs on Community, which needs no license. Six features are licensed, and separately an install is capped on how many approval policies it holds at once. A flag or request that turns on a licensed feature is refused outright rather than quietly ignored, so an install never believes it has a control it does not have.
| Capability | Tier | Turned on by |
|---|---|---|
| Directory sign-in (OIDC, SAML, LDAP, JWT) | Pro | Any --oidc-*, --saml-*, --ldap-*, or --jwt-* flag. serve refuses to start with one set and no license. |
| The full policy engine: deny rules, risk floors, actor scoping, distinct-approver separation of duties | Team | Creating a policy that uses one. A single require-approval policy stays Community. |
| More approval policies at once | Pro holds five, Team is uncapped | Creating policies. Community holds one. |
| The period change register | Team | audit report, and GET /v1/audit/register. |
| Distributed workers | Team | Every switchtender worker, whether it shares the database or reaches the server over the mesh relay with --server. |
| Initializing a new PostgreSQL database | Team | The first serve against a postgres:// DSN. Opening a schema that already exists is never gated. |
| One-click drift reconcile | Team | POST /v1/drift/reconcile. Drift detection itself is free. |
license status prints the tier this install runs and when a license lapses. Tiers and prices are at
https://switchtender.com/pricing.
Environment variables
| Variable | Used by | Purpose |
|---|---|---|
SWITCHTENDER_ENCRYPTION_KEY |
serve, worker | Passphrase that seals stored credentials with AES-256-GCM. Credentials are disabled when unset. |
SWITCHTENDER_ENCRYPTION_SALT |
serve, worker | Per-deployment salt for argon2id key derivation. Must be set alongside the key and stay stable across restarts, or stored credentials cannot be decrypted. Credentials are disabled when unset. |
SWITCHTENDER_AUDIT_KEY |
serve | Hex-encoded ed25519 seed for the install's signing identity, which signs the LoomSeal bundles it emits and binds every audit entry to this install. Unset beside a local database, the install mints and stores its own key there. Unset against a shared database it mints nothing, since every process must sign as the same install, and the chain is recorded unattributed and unbound until a seed is supplied to all of them. A malformed value stops startup. |
SWITCHTENDER_IDENTITY_DIR |
serve, audit, receipt | Directory holding the install's producer signing identity. A SQLite install keeps it beside the database and needs no setting. A postgres install has no filesystem home, so it uses a per-user configuration directory; when the account has no home, as in a container, there is nowhere durable to put a key and startup refuses rather than choosing a path a restart would empty. Point this at a durable path the server owns. |
SWITCHTENDER_PASSWORD |
user new | Initial account password, read instead of prompting so it never lands on the command line. |
SWITCHTENDER_SMTP_PASSWORD |
serve | Password for SMTP authentication when --smtp-username is set. |
SWITCHTENDER_AI_KEY |
serve | API key for a cloud AI provider such as Anthropic or an OpenAI-compatible endpoint. A local Ollama needs none. |
SWITCHTENDER_OIDC_CLIENT_SECRET |
serve | OpenID Connect client secret, paired with --oidc-client-id. Read from the environment so it stays off the command line. |
SWITCHTENDER_LDAP_PASSWORD |
serve | Password for the --ldap-bind-dn service account. |
SWITCHTENDER_WORKER_TOKEN |
serve, worker | Mesh relay bearer token. The server reads it when --worker-token is unset, and a relay worker started with --server presents it on every call. |
SWITCHTENDER_GALAXY_SERVER |
serve, worker | Default for --galaxy-server, a private Ansible Galaxy or Automation Hub URL. |
SWITCHTENDER_GALAXY_TOKEN |
serve, worker | Token for the --galaxy-server URL, read from the environment so it never lands on the command line. |
SWITCHTENDER_PLUGINS_DIR |
serve, worker | Directory of extension plugin binaries, read when --plugins-dir is unset. |
SWITCHTENDER_ADMIN_PASSWORD |
init | Password for the first admin account. When unset, init generates one and prints it once. |
SWITCHTENDER_DESKTOP_NO_BROWSER |
desktop | Set to any value to skip opening the browser, for a headless or remote run. |
init
Bootstraps a new deployment. It creates the database and the first admin account, writes an
environment file, and optionally a systemd unit. Run it once, then start serve.
| Flag | Default | Purpose |
|---|---|---|
--db |
switchtender.db |
SQLite database path. |
--config |
switchtender.env |
Environment file to write. |
--addr |
127.0.0.1:8080 |
Address the server listens on. Loopback by default. |
--admin |
admin |
Username for the first admin account. |
--systemd |
none | Path to write a systemd unit to, empty to skip. |
--force |
false |
Overwrite an existing config file. |
The admin password comes from SWITCHTENDER_ADMIN_PASSWORD, or is generated and printed once when
that variable is unset.
serve
Runs the HTTP API, the in-process executor, the scheduler, the retention sweeper, and the web UI.
| Flag | Default | Purpose |
|---|---|---|
--addr |
127.0.0.1:8080 |
Address the server listens on. Loopback by default. Set 0.0.0.0:8080 to expose it on the network. |
--db |
switchtender.db |
SQLite file path, or a postgres:// DSN for the PostgreSQL backend. |
--tls-cert |
none | TLS certificate file, to serve HTTPS directly with no reverse proxy. Requires --tls-key. |
--tls-key |
none | TLS private key file. Requires --tls-cert. |
--oidc-issuer |
none | OpenID Connect issuer URL to enable single sign-on. Empty leaves SSO off. |
--oidc-client-id |
none | OIDC client id. |
--oidc-redirect-url |
none | OIDC redirect URL, for example https://host/auth/oidc/callback. |
--oidc-default-role |
viewer |
Role granted to an account created on first SSO sign-in: admin, operator, or viewer. |
--ldap-url |
none | LDAP directory URL to enable directory sign-in, for example ldaps://ldap.example.com:636. |
--ldap-bind-dn |
none | Service account DN used to search for a user, empty for an anonymous search. |
--ldap-base-dn |
none | Search base for finding a user. |
--ldap-user-filter |
(uid=%s) |
Search filter with one %s for the username. |
--ldap-default-role |
viewer |
Role for an account created on first directory sign-in. |
--ldap-role-map |
none | Map a directory group to a role as groupDN=role. A matched group sets the role on every sign-in. Repeatable. |
--saml-idp-metadata-url |
none | SAML IdP metadata URL to enable SAML sign-in. Empty leaves SAML off. |
--saml-base-url |
none | Public base URL of this server, used to build the SAML entity id and ACS endpoint. |
--saml-cert |
none | Path to the service provider certificate, PEM. |
--saml-key |
none | Path to the service provider RSA private key, PEM. |
--saml-username-attr |
NameID | Assertion attribute used as the username. Empty uses the subject NameID. |
--saml-groups-attr |
groups |
Assertion attribute holding the user's groups, used with --saml-role-map. |
--saml-default-role |
viewer |
Role granted to an account created on first SAML sign-in. |
--saml-role-map |
none | Map an asserted group to a role as group=role. A matched group sets the role on every sign-in. Repeatable. |
--jwt-jwks-url |
none | JWKS URL to enable bearer JWT sign-in, so a service can present a JWT minted elsewhere. |
--jwt-issuer |
none | Expected token issuer, the iss claim. |
--jwt-audience |
none | Expected token audience. Left empty the audience is not checked, so every token the issuer signs is accepted, including one minted for a different application at the same issuer. Set it unless the issuer serves this install alone. |
--jwt-username-claim |
sub |
Claim naming the account. |
--jwt-groups-claim |
none | Claim holding the user's groups, used with --jwt-role-map. |
--jwt-role-map |
none | Map a token group to a role as group=role. Repeatable. |
--jwt-default-role |
viewer |
Role granted to an account created on first JWT sign-in. |
--ai-provider |
none | Enable advisory AI features with a provider: ollama, anthropic, or openai. Empty leaves AI off. |
--ai-model |
provider default | Model name for the AI provider. Required for openai, which has no universal default. |
--ai-url |
provider default | Base URL for the AI provider, for a self-hosted Ollama, an OpenAI-compatible server, or a proxy. |
--schedule-interval |
15s |
How often the scheduler checks for due schedules. |
--workers |
4 |
Concurrent runs this process executes at once. At least 1: the server executes its own runs, so there is no value that makes it execute none. |
--max-shards |
512 |
Most groups a split fans out into. A split is always bounded by the host count. |
--run-timeout |
0 |
Default cap on how long a run may execute before it is canceled and failed, for example 1h. A run may set a shorter timeout. Zero leaves runs uncapped. |
--notify-webhook |
none | URL that receives a JSON notification when a run finishes. Repeatable. |
--notify-slack |
none | Slack incoming webhook URL that receives a message when a run finishes. Repeatable. |
--notify-mattermost |
none | Mattermost incoming webhook URL that receives a message when a run finishes. Repeatable. |
--notify-rocketchat |
none | Rocket.Chat incoming webhook URL that receives a message when a run finishes. Repeatable. |
--notify-discord |
none | Discord incoming webhook URL that receives a message when a run finishes. Repeatable. |
--notify-teams |
none | Microsoft Teams incoming webhook URL that receives an Adaptive Card when a run finishes. Repeatable. |
--notify-ntfy |
none | ntfy topic URL that receives a notification when a run finishes, such as https://ntfy.sh/my-topic. Repeatable. |
--notify-ntfy-token |
none | Optional bearer token for a protected ntfy topic, applied to every --notify-ntfy URL. |
--notify-pagerduty |
none | PagerDuty Events API routing key that triggers an incident when a run fails. Repeatable. |
--notify-grafana |
none | Grafana base URL that receives an annotation when a run finishes. Repeatable. |
--notify-grafana-token |
none | Bearer token for the Grafana annotations API, applied to every --notify-grafana URL. |
--notify-twilio-sid |
none | Twilio Account SID for SMS notifications on a failed run. |
--notify-twilio-token |
none | Twilio Auth Token, paired with --notify-twilio-sid. |
--notify-twilio-from |
none | Twilio sender phone number that texts run failures. |
--notify-twilio-to |
none | Phone number that receives an SMS when a run fails. Repeatable. |
--allow-container-ee |
false |
Allow runs whose project pins a container image to execute inside it. Needs Docker on the executor. |
--default-image |
none | Fallback execution image for runs that pin none at the run, template, or project level. Empty leaves an unpinned run on the host. |
--require-image-digest |
false |
Reject a container run whose image is not pinned to an @sha256: digest. |
--container-memory |
2g |
Memory cap for containerized runs, as docker --memory. Empty removes the cap. |
--container-cpus |
2 |
CPU cap for containerized runs, as docker --cpus. Empty removes the cap. |
--container-pids-limit |
2048 |
Process cap for containerized runs, as docker --pids-limit. Zero removes the cap. |
--container-network |
bridge |
Network mode for containerized runs, as docker --network, for example bridge or none. A run with network access can reach the host's cloud metadata service and read the instance identity, which on a cloud host is a credential. Use none for runs that do not need the network, and block the link-local metadata address at the host firewall where they do. |
--container-runtime |
docker |
Container CLI for containerized runs: docker or podman. |
--container-pull-policy |
missing |
Image pull policy for containerized runs, as docker --pull: always, missing, or never. |
--galaxy-server |
none | Private Ansible Galaxy or Automation Hub URL for project collection installs. Token from SWITCHTENDER_GALAXY_TOKEN. |
--strict-grants |
false |
Deny non-admins access to an object that has no grants, instead of deferring to the global role. Off by default, which means separation between organizations is not enforced until you turn it on: see below. |
--read-only |
false |
Reject every mutating request, for a safely exposable instance. |
--matrix-cap |
50000 |
Largest host matrix, in cells, the UI draws before showing a notice. 0 means no limit. |
--plugins-dir |
none | Directory of extension plugin binaries loaded at startup. Also SWITCHTENDER_PLUGINS_DIR. See Extend in Go. |
--worker-token |
none | Bearer token that authenticates mesh relay workers and enables the relay endpoints. Also SWITCHTENDER_WORKER_TOKEN. Keep it secret. On its own, every worker holding it may lease from every queue. |
--worker-pools |
none | YAML file binding each worker token to the queues it may lease from, so a queue is a boundary rather than a routing hint. |
--retain-runs |
none | Delete terminal runs older than this, for example 90d. Empty keeps them forever. Deleting a run does not make what it left behind unreadable: summaries, drift, and host state history outlive it, and the purge retains the record of who could read the run so those rows stay readable to exactly the same people. That record is dropped automatically once nothing references it. |
--retain-events |
none | Drop run events and logs older than this, for example 30d. Empty keeps them forever. |
--retain-history |
none | Keep only this many per-host and per-task summaries for each host and each task, for example 500. Summaries outlive the runs they came from, so this is the only bound on them. Zero keeps every summary forever. A smaller value is raised to 500, the deepest window the fleet views will answer. |
--facts-interval |
24h |
Minimum spacing between retained host state snapshots. The estate history keeps the newest gather in each period, which is what answers what a host looked like on a date. Zero keeps every gather, at roughly a hundred times the disk. |
--retain-facts |
400 |
Keep only this many host state snapshots for each host. A fact set is hundreds of kilobytes, so unlike summaries this is bounded by default. Zero keeps every snapshot forever. |
--retention-interval |
1h |
How often the retention sweeper runs. |
--evidence-dir |
none | Directory for periodic change registers. Set together with --evidence-cadence. Team: the server refuses to start with a cadence set on a Community license. |
--evidence-cadence |
none | How long each change register covers and how often one is written, for example 2160h for a quarter. Minimum 1h. Zero writes none. Progress is read from the archive, so a restart resumes from the newest pack rather than starting the period again. |
--forward-url |
none | HTTP endpoint audit events stream to as NDJSON, one JSON object per line, each carrying its seq:link receipt. Splunk HEC raw, Elastic, and log routers ingest it directly. |
--forward-header |
none | Header set on every forwarded batch, as Name: value, for example an HEC token. Repeatable. |
--forward-syslog |
none | TCP syslog collector (host:port) audit events stream to as RFC 5424, octet-counted, one message per event with the JSON event as the body. |
--forward-syslog-tls |
false |
Wrap the syslog connection in TLS. |
--forward-state |
switchtender-forward.json |
Durable cursor recording the last position every sink accepted. The cursor advances only on delivery, so an outage delays events rather than dropping them, and a restart resumes without restreaming. |
--forward-interval |
5s |
How often the forwarder polls the chain when caught up. Minimum 1s. |
--smtp-addr |
none | SMTP server host:port for run notification emails. Empty disables email. |
--smtp-from |
none | Sender address for notification emails. |
--smtp-to |
none | Recipient address for notification emails. Repeatable. |
--smtp-username |
none | SMTP username. The password comes from SWITCHTENDER_SMTP_PASSWORD. |
--notify-on |
failure |
When to email: failure for failed runs only, or finish for every terminal run. |
Retention windows accept a whole number of days with a d suffix, such as 30d, or Go duration
syntax such as 720h.
AI providers
The advisory AI features run against one provider, chosen with --ai-provider: ollama for a local
model, or anthropic and openai for a cloud model with SWITCHTENDER_AI_KEY. The provider is off
until set, and no feature ever executes anything the provider suggests.
Cloud models see automation content: commands, playbook names, failed-run logs, and host drift.
Because that content is security-adjacent, a model with strict safety classifiers can decline a
benign request as a false positive. When the Anthropic model is a Fable or Mythos model, SwitchTender
opts into server-side fallbacks, so a declined request is retried on claude-opus-4-8 in the same
call and the feature keeps working. A Fable model also requires that the account keep 30-day data
retention, or the API rejects every request.
desktop
Runs SwitchTender as a local desktop application. It serves on a private loopback port, stores its
data in a per-user directory, and opens the web UI in the default browser. It takes no flags. Set
SWITCHTENDER_DESKTOP_NO_BROWSER to skip opening a browser. See Desktop for packaging a
macOS app or a Windows installer.
worker
Leases pending runs from the shared store and executes them. Point it and a server at the same
database and they compete for work. Or start it with --server and it leases runs from the control
node over the mesh relay, with no database access of its own.
| Flag | Default | Purpose |
|---|---|---|
--db |
switchtender.db |
SQLite file path, or a postgres:// DSN. Ignored with --server. |
--server |
none | Control node base URL to lease runs from over the mesh relay, for example https://switchtender.example.com. When set, the worker needs no database and dials one outbound connection. Token from SWITCHTENDER_WORKER_TOKEN. |
--name |
host and pid | Worker name stamped on the runs it executes. |
--queue |
none | Queue this worker serves. Repeatable. Without any, it serves the default pool. |
--workers |
4 |
Concurrent runs this process executes at once. At least 1: a worker with no slots would lease nothing and sit idle. |
--run-timeout |
0 |
Default cap on how long a run may execute before it is canceled and failed, for example 1h. Zero leaves runs uncapped. |
--facts-interval |
24h |
Minimum spacing between retained host state snapshots. Applies only with --db: a worker using --server reports what it gathered to the control node, which spaces the history with its own setting. |
--retain-facts |
400 |
Snapshots kept per host. Applies only with --db, for the same reason. |
--allow-container-ee |
false |
Allow container execution environments on this worker. Needs Docker. |
--default-image |
none | Fallback execution image for runs that pin none at the run, template, or project level. |
--require-image-digest |
false |
Reject a container run whose image is not pinned to an @sha256: digest. |
--plugins-dir |
none | Directory of extension plugin binaries loaded at startup. Also SWITCHTENDER_PLUGINS_DIR. |
--container-memory |
2g |
Memory cap for containerized runs, as docker --memory. Empty removes the cap. |
--container-cpus |
2 |
CPU cap for containerized runs, as docker --cpus. Empty removes the cap. |
--container-pids-limit |
2048 |
Process cap for containerized runs, as docker --pids-limit. Zero removes the cap. |
--container-network |
bridge |
Network mode for containerized runs, as docker --network. |
--container-runtime |
docker |
Container CLI for containerized runs: docker or podman. |
--container-pull-policy |
missing |
Image pull policy for containerized runs, as docker --pull: always, missing, or never. |
--galaxy-server |
none | Private Ansible Galaxy or Automation Hub URL for project collection installs. Token from SWITCHTENDER_GALAXY_TOKEN. |
token
Manages API tokens. A public bind on an empty database mints an initial admin token at startup.
token new --name <label> [--user <username>] [--ttl <duration>] [--agent]mints a token, printed once. A zero TTL never expires.--userbinds the token to an account, and the token carries that account's role. A token minted without--useris unscoped and acts as admin, so bind every token you hand to a person, a service, or an AI agent. See running an agent.--agentmarks the token as held by an AI agent rather than a person. It caps the token at operator whatever role its account holds, so an agent can launch and propose work but can never manage identity, access, or secrets, and can never approve its own held run. Every action it takes is recorded in the chain asactor_type: agentwith the account it acts for beside it, which is whatactor_kind: agentpolicy rules match on. It requires--user, so the chain always records the human the agent acts for; without one the command refuses. Mint every agent token with it: a token without--agentis indistinguishable from a person's in the record.token listlists tokens without their secrets.token revoke <id>deletes a token.
All token subcommands take --db and the global --pretty flag for indented JSON.
user
Manages accounts with roles: admin, operator, and viewer.
user new <username> --role <role>creates an account. The password comes fromSWITCHTENDER_PASSWORDor a prompt, never an argument.user listlists accounts.user delete <id>deletes an account. Its tokens stop working.
All user subcommands take --db.
license
Shows or installs this install's license. No license is Community, which is complete in itself. What each tier covers is listed at https://switchtender.com/pricing.
license statusshows the tier this install runs and when a license lapses.license install <file>verifies a license file and installs it beside the database.
A license is read from SWITCHTENDER_LICENSE, or from switchtender-license.json in the same
directory as the database. A license this install cannot parse or that has lapsed reads as
Community rather than failing the server, so an expiry never takes an install down.
assess
Reports what an automation export holds and what governing it would change, without writing
anything and without a database. It reads an export the same way import does, then grades every
template it found through the same risk and reversibility graders the product uses at run time, so
a number in an assessment cannot disagree with what a run would later say.
switchtender assess awx awx-export.json
switchtender assess chef chef-nodes.json
Formats are awx, semaphore, chef, and puppet. An AWX-format export also covers Ansible
Automation Platform, Tower, and Ascender.
The output has three parts: what is in the export, what survives the move, and what changes about how it is governed. The third part names the templates that cannot be undone, the ones carrying a destructive signal, the credentials more than one template shares, and the templates targeting no stored inventory.
Grades are a floor rather than a measurement. An Ansible template keeps its work in a playbook, and at assessment time that playbook is in a repository nothing has fetched, so reading one can only ever raise a grade. The report says how many templates that applies to rather than leaving the numbers looking more settled than they are.
import
Migrates from AWX, Semaphore, Chef, Puppet, Rundeck, Jenkins, or cron. Which objects each one carries across is in what each source brings over.
import awx <export.json> [--apply]brings projects, inventories static and dynamic, credential shells, job templates and workflows, surveys, and schedules.import semaphore <export.json> [--apply]brings the same kinds from a Semaphore export, apart from the dynamic inventory sources AWX alone carries.import chef <nodes.json> [--apply]brings one inventory of the fleet: every node a host, grouped by itschef_environmentand by everyrole[...]in its run list, carrying the ohai facts that identify a machine, withipaddressalso set asansible_host. Accepts an array of node documents, a single node, or an object keyed by node name. Cookbooks and recipes are not imported and the recipes seen are named in the report.import puppet <nodes.json|facts.json|nodes.txt> [--apply]brings one inventory of the fleet, grouped by environment, from a PuppetDB nodes query, a PuppetDB facts query, or the plain certname listpuppet node listprints. Deactivated and expired nodes are left out and counted. Manifests and modules are not imported.import rundeck <jobs.yaml|project-archive.zip> [--inventory <name>] [--apply]brings templates, surveys, and schedules from either a job export or a project archive, told apart by content. An archive brings one project as well, but only when its source control configuration names a repository this can reach. Neither artifact carries a node definition, so no inventory is imported from either and--inventorynames the hosts its jobs target.import jenkins <JENKINS_HOME|jobs-dir|config.xml> [--inventory <name>] [--apply]brings templates, surveys, and schedules from freestyle jobs. Jenkins picks an agent by label, so--inventorynames the machines.import cron <crontab-file> [--inventory <name>] [--system] [--apply]brings schedules alone, one per crontab line, each carrying its own one-step bash pipeline rather than a template.--systemparses the six-field/etc/crontabform, whose user column sits before the command. That step runs on the SwitchTender host, not on the machine the crontab came from.
All five take --db for the target database. Without --apply the command only reports what it
would create. Rundeck, Jenkins, and cron import no credentials, so a job that needed a login needs
one built by hand afterward.
audit
Audit trail tools.
audit bundleemits the chain as a signed LoomSeal bundle. Anyone verifies it offline with the openloomsealverifier, or in a browser, without trusting the server that produced it.audit anchorhas a public timestamp authority sign the current head, so a chain that has lost its tail no longer reaches its anchor.audit receipt <seq:link>redeems a receipt the server issued, proving that entry is still in the chain.audit reportrenders the period's change register as a self-contained HTML evidence report.audit run <id>emits one run's evidence dossier as a self-contained HTML document.
receipt
Writes a signed receipt for one finished run, which a third party verifies offline with verify. A
receipt is the chain segment from the request that created the run through the entry recording what
it did, signed with this install's key and carrying any anchors that fix its position.
| Flag | Default | Purpose |
|---|---|---|
--db |
switchtender.db |
Database holding the run and its chain. |
--out |
stdout | File to write the receipt to. |
--sparse |
off | Disclose only this run's own chain entries, proving each belongs to the log without carrying the entries around it. |
--append-only-from |
unset | With --sparse, prove the log only appended since this size. Use a size a reader already saw, such as an anchored head. |
Publish the key fingerprint the command prints so a verifier can pin it.
verify
Verifies a receipt written by receipt. It trusts nothing this server says: it recomputes every
chain link from the receipt's own claims, checks the signature covers the exact bytes, and confirms
any anchors name an entry the receipt holds. It reads only the file, reaches no database and no
network, and does not run the server, so a relying party can check a receipt on a machine that has
never seen this install.
switchtender verify run.receipt --pubkey sha256:...
Pass --pubkey with the fingerprint the producer published to tie the result to a key obtained out
of band. Without it the receipt is checked against the key it names, which proves it was not altered
but not who signed it.
witness
Watches a server's span beat feed from outside it. A chain proves what it holds was not altered, but not that nothing was removed from the end, because the process running the chain also decides what gets written down. A witness on another machine remembers what the feed served, keeps that memory in a signed checkpoint, and raises a finding when a beat goes missing, an already-witnessed beat comes back rewritten, or the head regresses. Run it where the server's operator has no hand.
| Flag | Default | Purpose |
|---|---|---|
--server |
required | Base URL of the server to watch. |
--state |
switchtender-witness.json |
Signed checkpoint holding what this witness has seen. |
--interval |
1m |
How often to poll the feed. |
--key-dir |
the state file's directory | Directory holding the witness signing key. |
--once |
off | Run one check and exit nonzero on findings, for cron. |
--webhook |
unset | URL that receives each finding as a JSON POST. |
witness servewatches many servers from one process and answers auditors with countersigned attestations.witness verify-attestationverifies an attestation offline against a pinned witness key.
mcp
Serves the Model Context Protocol over stdio, so an agent can list templates, propose a run, and read what happened. Every tool call is an ordinary authenticated API request carrying the token given here, so it passes the same authorization, the same approval policy, and the same fail-closed audit append as a request from a person. See Agents.
export SWITCHTENDER_MCP_TOKEN=swt_...
switchtender mcp --server https://switchtender.internal
The token is read from SWITCHTENDER_MCP_TOKEN, falling back to SWITCHTENDER_TOKEN. Prefer the
environment variable to a flag, whose value is visible in the host's process list. The command
refuses to start on an admin token. There is deliberately no approve tool, so an agent cannot release
its own work, and no credential, account, token, grant, or policy tool, so it cannot widen its own
reach.
| Flag | Default | Purpose |
|---|---|---|
--allow-adhoc |
off | Also expose the ad-hoc run tool, letting the agent compose a run rather than launch a template an operator defined. Approval policy still applies. |
demo
Seeds a fresh database with sample data and real runs, then serves it read-only, so a public instance is safe to expose. It needs ansible on the PATH to run the sample playbooks.
| Flag | Default | Purpose |
|---|---|---|
--addr |
127.0.0.1:8080 |
Address the demo listens on. Loopback by default. |
--db |
temporary file | Database to seed and serve. Empty uses a fresh temporary SQLite file. |
--seed-only |
off | Seed the database and exit without serving. |
--no-seed |
off | Serve the database as it already stands instead of seeding it. |
Seeding runs real playbooks and takes a couple of minutes, which is a visible gap if a public demo reseeds in place. The two flags split that work in half so it can happen off to the side:
switchtender demo --db next.db --seed-only # build the next database, serving continues
mv next.db demo.db # swap it in
switchtender demo --db demo.db --no-seed # serves the prepared data in under a second
A host wired this way reseeds without a visible outage, since the running instance keeps answering the whole time the replacement is being built.
examples
Seeds a handful of starter templates, so a first launch works on the spot rather than opening on an empty list. They use the Bash tool and print or read something local, needing no project, inventory, or credential. Delete them once you have your own.
Run it against the same database serve uses. It skips a template whose name is already present, so
it is safe to run twice. Takes --db.
backup
Writes an encrypted, portable backup of the control-plane configuration and secrets. The whole file is sealed with the deployment encryption key, so it stays confidential and tamper-evident, and it restores into either the SQLite or the PostgreSQL backend. See Backup and restore.
| Flag | Default | Purpose |
|---|---|---|
--db |
switchtender.db |
SQLite file path, or a postgres:// DSN, to back up. |
--out |
stdout | File to write the backup to. |
Run history and the audit chain are not included. The audit chain has its own signed export, through
audit bundle.
restore
Reads a backup and upserts its objects into the store by id. It needs the same encryption key the backup was written with, and it never deletes objects absent from the file.
| Flag | Default | Purpose |
|---|---|---|
--db |
switchtender.db |
SQLite file path, or a postgres:// DSN, to restore into. |
--in |
stdin | Backup file to read. |
version
Prints the SwitchTender version.
help and completion
Both are the standard Cobra built-ins. help prints usage for any command, and completion emits a
shell completion script for bash, zsh, fish, or PowerShell.
Output flags
JSON goes to stdout compact by default. Three commands take --pretty to indent it; it is not a
global flag, so passing it elsewhere is an error rather than a no-op.
| Flag | Where | Purpose |
|---|---|---|
--pretty |
token and its subcommands, audit anchor, audit receipt |
Indent JSON output instead of the compact default. |
Separation between organizations is opt-in
Organizations, teams, and grants exist, but with --strict-grants off an object nobody has granted
falls back to the caller's global role. On that default any operator may use any project, inventory,
or credential in the install, whichever organization it belongs to. That is deliberate: a small team
should not have to grant every object before anything works, and an upgrade should not lock people
out of what they were already using.
It does mean an install with several organizations on it is not separated until --strict-grants is
on. If you are running work for more than one team, more than one customer, or anything where one
group must not reach another's credentials, turn it on and grant deliberately. Objects created
before you do carry no grants, so plan to assign them.
Directory sign-in and existing accounts
An account records what created it: an administrator, or the directory that provisioned it. A directory identity signs in to an account only when that account is its own, so an identity provider asserting the username of a local administrator is refused rather than handed that administrator's account and role.
This matters most when the username does not come from a stable subject. With
--jwt-username-claim email or a SAML username attribute pointing at an email, against an issuer
that lets a user set their own address, matching on username alone was account takeover. OIDC has
always refused the equivalent when the provider does not vouch for the address.
An account created before this was recorded carries no source, so it cannot be told apart from a local one of the same name. Those are still signed in to, because refusing them would lock out every directory user on upgrade, and each is logged so an administrator can set the source and remove the ambiguity.
Confining relay workers to their queues
A relay worker runs in a segment the control node cannot reach, which means the least trusted machine
in the estate holds a worker token. With a single --worker-token, that machine may name any queue
it likes and lease from it, so a compromised host in a DMZ can take a production run and execute it
with production credentials.
--worker-pools binds each token to the queues it may serve. The file stores the SHA-256 of each
token, never the token itself, the same way a webhook secret is stored:
workers:
- name: dmz
token_sha256: 9f2c... # sha256 of that pool's bearer token
queues: [dmz]
- name: production
token_sha256: 41ab...
queues: [prod, canary]
A pool that declares no queues may lease from all of them, which is the single-token shape stated out loud. A pool that declares queues is refused anything else, including the default queue when it names none, so confinement cannot be escaped by omission.
Generate a digest with printf %s "$TOKEN" | shasum -a 256. A malformed file stops the server rather
than falling back to no confinement, because an install that believes it is segmented and is not is
worse than one that refuses to start.
That confines the lease side. The submit side is confined by granting the queue, which is a grantable
object named queue:<name>:
curl -X POST localhost:8080/v1/grants \
-H "Authorization: Bearer $ST_TOKEN" \
-d '{"subject": "team_sre", "object": "queue:prod", "access": "use"}'
A queue nobody has granted follows the same rule every other object does: the global role decides,
unless --strict-grants is on, in which case an ungranted queue is refused. Granting a queue makes
it access-controlled, so only the subjects named may route work to it. The grant is checked wherever
a queue is chosen: on a run, on a template, on an inventory, and at launch against the template's own
queue.
An install that has not turned strict grants on can gate a queue with a rule instead, since a policy
matches on queue:
policies:
- name: hold anything headed for production
queue: prod
require_distinct_approver: true
A policy with queue holds only what is routed to that queue, and a run on a named queue executes
only when a worker serving that queue claims it; the control node's own runner takes unqueued runs
alone. On an install without relay workers, a queued run therefore waits as pending indefinitely,
even after approval. To gate runs the server itself executes, write the policy without queue.
What a relay worker writes into the audit trail
A relay worker runs where the control node cannot see it, so two moments are recorded: the run leaving for that machine, and the outcome coming back.
RELAY /relay/claim/run_4f21a9 worker:build-dmz-01
RELAY /relay/finished/run_4f21a9/succeeded
Captured output, structured events, per-host and per-task summaries, and heartbeats are not recorded. They are the content and liveness of a run that is already stored on the run itself, they arrive several times a second, and writing each into a hash chain would drown the record it exists to make readable.
This append does not fail closed, unlike every mutation through the API. Refusing a worker's report because the audit store is unhealthy does not un-finish the run; it loses the outcome of work that already ran on real hosts. A failure to record is logged loudly instead.
