SwitchTenderSwitchTender Docs

Secrets

Secrets live in credentials. Each is sealed at rest with AES-256-GCM, is never returned by the API, and reaches a run only while it executes, in the environment or a temporary file created mode 0600 and deleted when the run ends. If a tool prints a secret, SwitchTender redacts it from the run's log, live stream, and events, so the output shows *** instead of the value.

Kinds

A credential's kind decides how its value reaches a run.

Kind What it is
ssh_key An SSH private key, to reach hosts and clone private git projects. A passphrase protected key is unlocked in memory at run time from a passphrase sealed alongside it, so no prompt blocks the run.
ssh_password A machine login, injected as ansible_user and ansible_password through a file, so the password stays off the command line.
vault_password An Ansible Vault password. An optional vault ID label passes it as --vault-id label@file, so several vault credentials on one run each unlock the secrets encrypted for their label; without a label it is the classic --vault-password-file.
become_password A privilege escalation password, kept off the command line.
become Privilege escalation with an optional method and user, injected as the ansible_become_* variables through a file.
network A network device login, injected as the ansible_user, ansible_password, ansible_network_os, and ansible_connection variables.
env KEY=VALUE lines injected into the environment, how cloud SDK tokens reach a tool.
token A single API token or JWT, exposed to the run as SWITCHTENDER_TOKEN.
registry A container registry login, to pull a pinned execution image.
aws An AWS access key, injected as the standard AWS_* environment variables.
azure An Azure service principal, injected as the ARM_* variables Terraform reads and the AZURE_* variables the Ansible azure collection reads.
gcp A Google Cloud service account JSON, written to a private file bound to GOOGLE_APPLICATION_CREDENTIALS.
vmware A vCenter login, injected as the VMWARE_* environment variables the community.vmware modules read.
openstack An OpenStack login, injected as the OS_* environment variables openstacksdk and the openstack.cloud collection read. Fields: auth_url, username, password, project_name required; user_domain_name and project_domain_name default to Default; region_name optional.

Sources

A credential's source decides where its value comes from at run time.

Source Where the value comes from
Stored The sealed value you pasted. This is the default.
Command A command SwitchTender runs at launch, whose standard output is the secret. Works with any CLI, so Vault, AWS, GCP, or 1Password all resolve with no extra integration.
Vault A Vault address, path, and field, read over Vault's HTTP API at launch. Handles KV v2 and KV v1.
Vault dynamic A Vault dynamic secrets path. A fresh, short-lived credential is minted for each run and revoked when the run ends.
Google Secret Manager A project, secret, and version, read at launch. On GCP it reads as the attached service account with no stored key.
AWS Secrets Manager A secret id, region, and AWS credentials, read over a Signature Version 4 signed request at launch. Credentials fall back to the standard AWS environment, so an instance role needs no stored key.
AWS STS An IAM role to assume. A fresh set of short-lived role credentials is minted for each run and injected as AWS_* environment variables. The credentials expire on their own STS lifetime, so nothing long-lived is stored.
Azure Key Vault A vault name and secret, read over the Key Vault REST API at launch. Authenticates with a bearer token, a service principal, or, on Azure, the attached managed identity with no stored key.
CyberArk Conjur A Conjur URL, account, and variable, read over the Conjur REST API at launch. Authenticates with an access token or by exchanging an API key, so no long-lived credential is stored once a token is issued.
CyberArk CCP A Central Credential Provider URL, app id, and account locator, read over the AIMWebService REST API at launch. Authenticates the application with a client certificate or a CCP allowed-machine rule, so no long-lived credential is stored.
1Password A 1Password Connect URL, token, vault, item, and field, read over the Connect REST API at launch. The vault and item may be names or ids, and the field defaults to the item's password.

A source that authenticates with an attached managed identity holds no stored key, which is the point of using one, but it does mean the host's identity is a credential reachable from anything running on that host. A containerized run with network access can read it from the cloud metadata service the same way this server does. Run with --container-network none where a run does not need the network, and block the link-local metadata address at the host firewall where it does. This bounds what the identity can reach as well: give it access to the secrets this install reads and nothing more.

Ephemeral secrets

A Vault dynamic source mints a new credential for each run and revokes it the moment the run ends, so a leaked value is useless minutes later. If the process dies before it can revoke, the credential still expires on the lease's own TTL. This is a control-plane capability neither incumbent offers.

Scope

Attach a credential to a run, a template, a project, or a stored inventory. A credential attached to an inventory reaches every run that targets it, so a fleet carries its own secret variables in one place.

Settings

A credential can carry non-secret fields beside its sealed secret: the user to connect as, a become method, a region. Unlike the secret, settings return from the API and show in the interface, so an operator can see and edit them and an AWX import lands them automatically. On the connection kinds, ssh_key, ssh_password, become, and network, settings inject as the matching Ansible variables, merged beneath the sealed fields so a sealed value wins on a shared name; a machine credential whose settings carry become_method or become_user injects the matching ansible_become_* variables. On every other kind, including env, settings are reference metadata only and are not injected, so a non-secret pair can never shadow another credential's sealed value. Settings values are never added to the run's mask list, which is the point: masking a username like deploy would black out ordinary output everywhere it appears.

What masking does not cover

Masking redacts known values: everything a credential injects, and any inventory variable whose name looks secret, such as ansible_password, ansible_become_pass, or api_token. Two things sit outside that. A dynamic inventory source saves whatever its script emits into the inventory it maintains, because the synced host list is the data later runs depend on, so anyone who can read that inventory reads it as stored. And a secret under an ordinary name, say a connection_string holding a password, is invisible to the name heuristic. Keep real secrets in credentials or an external source and let inventories carry references; a credential attached to the inventory reaches every run that targets it, sealed at rest and masked in output.

While a run executes, its working material lives in files created mode 0600 and removed when the run ends: materialized credentials, the inventory, and, for Ansible, the structured event sidecar the callback writes before the server masks it at read time. The exposure window is the run and the reader is the executing user, but a hard crash can leave those files until the temp directory clears. On a shared or long-lived executor, point TMPDIR at a tmpfs so run scratch lives in memory and dies with the machine instead of landing on disk.

What a run can reach on the host

A run without an execution image runs as the SwitchTender server's own user. That user owns the scratch files of every other run on that host, so the 0600 mode above stops other accounts, not other runs: while two runs overlap, either one's code can read the other's materialized credentials, and a run can write to the directory the Ansible callback plugin lives in. SwitchTender checks that plugin against its embedded copy before every Ansible run and restores it when it differs, so nothing a run leaves behind survives to be imported by later runs, but that is repair, not isolation.

Treat a run's content as trusted code, at the level of the host it runs on. Where that is not true, separate them:

Encryption

Sealing needs a key. Set SWITCHTENDER_ENCRYPTION_KEY and a stable SWITCHTENDER_ENCRYPTION_SALT before storing an externally sourced credential or inventory. The sealed value never leaves SwitchTender.

Back up the key and salt the way you back up any root secret. Whoever holds them can decrypt every stored credential, and losing them makes the sealed values unrecoverable, so every credential has to be re-entered. Keep the salt stable for the life of an install; changing it has the same effect as losing the key. There is no in-place key rotation yet: rotating the key means re-provisioning each stored credential under the new key, so plan a rotation as a re-entry pass rather than a background re-seal. For a compromised key, treat every credential it sealed as exposed, rotate those upstream secrets at their source, and re-enter them under a fresh key and salt.

See Set a secret for the step-by-step, including the API calls.

Custom credential types

The built-in kinds cover the common providers. For one they do not, define a custom type: the fields it collects and how those fields are injected into a run. This is what AWX calls a custom credential type, and it needs no code change.

A type is data, not code. An injector splices a field's value into a string literally, and nothing in it is executed, so a type cannot become a way to run something on the executor.

Define a type, admin only:

POST /v1/credential-types
{
  "name": "Datadog API",
  "fields": [
    {"name": "host",    "label": "API host"},
    {"name": "api_key", "label": "API key", "secret": true}
  ],
  "env": {
    "DD_HOST":    "{{host}}",
    "DD_API_KEY": "{{api_key}}"
  },
  "extra_vars": {
    "datadog_host": "{{host}}"
  }
}

A field marked secret is masked out of run output; a field left plain, such as a host or a region, is treated as configuration and is not masked. An injector value is literal text with {{field}} references, so "Bearer {{api_key}}" becomes the header with the key spliced in. A reference to a field the type does not declare is refused when the type is created, not left to expand to nothing at run time.

Create a credential of that type:

POST /v1/credentials
{
  "name": "prod-datadog",
  "type_id": "ctype_...",
  "fields": {"host": "api.datadoghq.com", "api_key": "the-secret-key"}
}

The field values are sealed together as one encrypted object, the same as any other secret. At run time the type's injectors add the environment variables, and any extra vars go through a private file so they never reach the process argument list. A field the type does not declare is refused.

A typed credential is recreated rather than updated, so its fields are changed by deleting it and creating a new one. This keeps the update path, which speaks a single secret, from reinterpreting the sealed field object.