Skip to content

Secrets & Encryption

Strøm supports two complementary approaches to secret management: SOPS-encrypted workflow files and vals secret resolution from external secret stores.

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.

  • sops must be installed and available on PATH
  • Decryption keys must be configured (age, AWS KMS, GCP KMS, etc.)
  • The .sops.yaml configuration file (if present) is ignored by the loader
Terminal window
# Encrypt a secrets file with age
sops -e --age age1... secrets.yaml > secrets.sops.yaml

The decrypted content is merged with other workflow files normally. Values from SOPS-encrypted files are already plaintext after decryption and don’t need | vals.

The | vals filter resolves ref+ secret references at template render time using the vals CLI.

The vals binary must be installed and available on PATH on the server (where templates are rendered).

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.

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.

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"

Any backend supported by vals works:

  • ref+awsssm:// — AWS SSM Parameter Store
  • ref+vault:// — HashiCorp Vault
  • ref+gcpsecrets:// — Google Cloud Secret Manager
  • ref+azurekeyvault:// — Azure Key Vault
  • ref+sops:// — SOPS encrypted files
  • And many more
  • Plain strings (not starting with ref+) pass through unchanged — {{ "hello" | vals }} returns "hello"
  • Non-string values (numbers, booleans) pass through unchanged
  • If vals is not installed and a ref+ value is encountered, the template render fails with a clear error
  • Each | vals usage invokes the vals CLI once

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.