Secrets & Encryption
Strøm supports two complementary approaches to secret management: SOPS-encrypted workflow files and vals secret resolution from external secret stores.
SOPS encrypted files
Section titled “SOPS encrypted files”Workflow files can be encrypted with SOPS. Name encrypted files with a .sops.yaml or .sops.yml suffix (e.g., secrets.sops.yaml). The server and CLI automatically detect these files and decrypt them before loading.
Requirements
Section titled “Requirements”sopsmust be installed and available on PATH- Decryption keys must be configured (age, AWS KMS, GCP KMS, etc.)
- The
.sops.yamlconfiguration file (if present) is ignored by the loader
Example
Section titled “Example”# Encrypt a secrets file with agesops -e --age age1... secrets.yaml > secrets.sops.yamlThe decrypted content is merged with other workflow files normally. Values from SOPS-encrypted files are already plaintext after decryption and don’t need | vals.
Secret resolution with | vals
Section titled “Secret resolution with | vals”The | vals filter resolves ref+ secret references at template render time using the vals CLI.
Prerequisites
Section titled “Prerequisites”The vals binary must be installed and available on PATH on the server (where templates are rendered).
Workspace-level secrets
Section titled “Workspace-level secrets”The secrets: section is rendered through Tera at workspace load time. Secrets are resolved once and cached in memory:
secrets: DB_PASSWORD: "{{ 'ref+awsssm:///prod/db/password' | vals }}" API_TOKEN: "{{ 'ref+vault://secret/data/api#token' | vals }}" SLACK_WEBHOOK: "{{ 'ref+gcpsecrets://my-project/slack-webhook' | vals }}"Then use the resolved values in templates — no | vals needed:
env: DB_PASSWORD: "{{ secret.DB_PASSWORD }}"script: "deploy --token {{ secret.API_TOKEN }}"Secrets are re-resolved when the workspace reloads (on config change or git poll), so rotated secrets are picked up automatically.
Inline resolution
Section titled “Inline resolution”You can also use | vals inline in any template expression (step input:, action env:, script:, source:, hook input:) without going through secrets::
env: DB_PASSWORD: "{{ 'ref+awsssm:///prod/db/password' | vals }}"This resolves at step claim time rather than workspace load time.
Full example
Section titled “Full example”secrets: DB_PASSWORD: "{{ 'ref+awsssm:///prod/db/password' | vals }}" API_TOKEN: "{{ 'ref+vault://secret/data/api#token' | vals }}" SLACK_WEBHOOK: "{{ 'ref+gcpsecrets://my-project/slack-webhook' | vals }}"
actions: deploy: type: script script: "deploy --token {{ secret.API_TOKEN }}" env: DB_PASSWORD: "{{ secret.DB_PASSWORD }}" input: env: { type: string }
tasks: deploy: flow: deploy: action: deploy input: env: "{{ input.env }}" on_success: - action: notify-slack input: webhook_url: "{{ secret.SLACK_WEBHOOK }}" message: "Deploy succeeded"Supported backends
Section titled “Supported backends”Any backend supported by vals works:
ref+awsssm://— AWS SSM Parameter Storeref+vault://— HashiCorp Vaultref+gcpsecrets://— Google Cloud Secret Managerref+azurekeyvault://— Azure Key Vaultref+sops://— SOPS encrypted files- And many more
Behavior
Section titled “Behavior”- Plain strings (not starting with
ref+) pass through unchanged —{{ "hello" | vals }}returns"hello" - Non-string values (numbers, booleans) pass through unchanged
- If
valsis not installed and aref+value is encountered, the template render fails with a clear error - Each
| valsusage invokes the vals CLI once
API redaction
Section titled “API redaction”Secret values are automatically redacted from API responses. When you view a job via GET /api/jobs/:id, any field (job input/output, step input/output) that contains a known secret value will have it replaced with ••••••. Substring matches are also redacted. Additionally, unresolved ref+ references are redacted to avoid leaking secret-manager paths.