Skip to content

Secrets Management

LatchGate uses SOPS (Secrets OPerationS) for encrypted secret storage. Secrets are decrypted just-in-time at each action execution and injected at the host I/O layer — they never enter the WASM sandbox, never appear in logs, and never reach the agent process.

Action manifest SOPS-encrypted file
declares: contains:
secrets: GITHUB_TOKEN: ENC[...]
- name: GITHUB_TOKEN SLACK_BOT_TOKEN: ENC[...]
required: true STRIPE_KEY: ENC[...]
DB_PASSWORD: ENC[...]
│ │
▼ ▼
┌──────────────────────────────────────────────────┐
│ SecretsManager.decrypt_secrets() │
│ │
│ 1. Invoke `sops -d secrets.enc.yaml` │
│ 2. Parse decrypted JSON │
│ 3. Filter: only GITHUB_TOKEN returned │
│ (SLACK_BOT_TOKEN, STRIPE_KEY, DB_PASSWORD │
│ are in the SOPS file but NOT in this │
│ action's manifest — they are discarded) │
│ 4. Validate: GITHUB_TOKEN is required => present │
└───────────────────────┬──────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Host I/O layer │
│ │
│ Injects GITHUB_TOKEN into the outgoing HTTP │
│ Authorization header at the transport level. │
│ The WASM provider never sees the token value. │
└──────────────────────────────────────────────────┘

This is the least-privilege guarantee: even if the SOPS file contains 20 secrets, an action that declares only GITHUB_TOKEN receives only GITHUB_TOKEN. Undeclared keys are discarded before execution.

Install SOPS and age (recommended encryption backend):

Terminal window
# macOS
brew install sops age
# Linux (Debian/Ubuntu)
sudo apt install age
# SOPS: download from https://github.com/getsops/sops/releases

Verify:

Terminal window
sops --version # >= 3.8.0
age --version # >= 1.0.0

Switch to the Setup screen, Secrets sub-tab (65):

Terminal window
latchgate tui
  • i — initialize the encrypted secrets store. Generates an age keypair at .latchgate/sops-age.key (mode 0600), creates an empty encrypted file, and sets sops_secrets_file + sops_key_file in latchgate.toml.
  • s — set a secret (available once the store is initialized). Each set decrypts, updates, and re-encrypts atomically; plaintext lives only in a temporary file with mode 0600, auto-deleted on drop.
  • r — remove the selected secret.
  • / (or k/j) — move the selection.

The sub-tab shows coverage status — which secrets are required by action manifests but missing.

CLI equivalent

The latchgate secrets command handles key generation, encryption, and config wiring in one flow:

Terminal window
# 1. Initialize — generates age key, creates encrypted file, updates latchgate.toml
latchgate secrets init
# 2. Add secrets
latchgate secrets set GITHUB_TOKEN ghp_xxxxxxxxxxxx
latchgate secrets set SLACK_BOT_TOKEN xoxb-xxxxxxxxxxxx
latchgate secrets set STRIPE_KEY sk_live_xxxxxxxxx
# 3. Verify coverage
latchgate secrets list
# 4. Validate
latchgate doctor

secrets init creates an age keypair at .latchgate/sops-age.key (mode 0600), an empty encrypted secrets file at secrets.enc.yaml, and sets sops_secrets_file + sops_key_file in latchgate.toml.

Each secrets set decrypts the file, updates the key, and re-encrypts atomically. Plaintext exists only in a temporary file with mode 0600, auto-deleted on drop.

secrets list shows all stored secrets with coverage status — which ones are required by action manifests but missing.

If you need custom paths, a different encryption backend, or an existing age key, set up manually:

Terminal window
mkdir -p /etc/latchgate
age-keygen -o /etc/latchgate/sops-age.key

Note the public key from the output (starts with age1...). Protect the key file:

Terminal window
chmod 600 /etc/latchgate/sops-age.key

In the directory where your secrets file will live, create .sops.yaml:

creation_rules:
- path_regex: \.enc\.(yaml|json)$
age: "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3lk..." # your public key

Create a plaintext YAML file with all your secrets:

# secrets.yaml (plaintext — encrypt immediately, then delete)
GITHUB_TOKEN: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
SLACK_BOT_TOKEN: "xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxx"
STRIPE_KEY: "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxx"
SENDGRID_API_KEY: "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Encrypt it:

Terminal window
sops -e secrets.yaml > secrets.enc.yaml
rm secrets.yaml # delete the plaintext immediately

Verify you can decrypt:

Terminal window
SOPS_AGE_KEY_FILE=/etc/latchgate/sops-age.key sops -d secrets.enc.yaml

Add to latchgate.toml:

# Path to the SOPS-encrypted secrets file. When set, the Gate decrypts this
# file at each action execution and injects only the secrets declared in
# the action manifest (least privilege). When unset, actions that declare
# secrets receive an empty env map and required secrets cause execution failure.
[secrets]
sops_secrets_file = "/etc/latchgate/secrets.enc.yaml"
# Optional: path to an age key file. When set, exported as SOPS_AGE_KEY_FILE
# when invoking SOPS. When unset, SOPS uses its default key discovery
# (SOPS_AGE_KEY_FILE env var, or ~/.config/sops/age/keys.txt).
sops_key_file = "/etc/latchgate/sops-age.key"

The SOPS binary name (sops) is a compile-time constant — ensure sops is on $PATH.

Terminal window
latchgate doctor

The doctor check validates that the sops binary is on PATH when sops_secrets_file is configured. If the binary is missing, the check fails and the gate will not be able to decrypt secrets at runtime.

Each action manifest declares which secrets it needs:

definitions/manifests/github_create_issue.yaml
action_id: "github_create_issue"
# ...
secrets:
- name: "GITHUB_TOKEN"
required: true # execution fails if this secret is missing from the SOPS file
- name: "BACKUP_TOKEN"
required: false # execution proceeds without this secret

The name field must match a top-level key in the SOPS-encrypted file exactly (case-sensitive). If required: true and the key is not present in the SOPS file, the action execution fails with SecretsError::RequiredSecretMissing before the WASM provider is loaded.

If required is omitted, it defaults to false.

What happens when sops_secrets_file is not configured

Section titled “What happens when sops_secrets_file is not configured”

When no sops_secrets_file is set in latchgate.toml:

  • Actions that declare no secrets execute normally.
  • Actions that declare secrets with required: false execute with an empty secrets map — the host I/O layer has no credentials to inject, so authenticated requests will fail at the external system (e.g. HTTP 401).
  • Actions that declare secrets with required: true — LatchGate logs a warning ("action has policy-approved secrets but no sops_secrets_file is configured") but does not block execution at the secret-injection step. The failure surfaces when the host I/O layer attempts an authenticated request without credentials.

Secrets never enter the WASM sandbox. The SecretsManager decrypts secrets and passes them to the host I/O layer, which injects credentials into outgoing requests at the transport level (e.g., Authorization header for HTTP, connection string for SQL). The WASM provider code never receives secret values.

Secrets are never logged. The redact_env() function replaces all values with ***REDACTED*** before any structured log or audit event. Only secret names (not values) appear in trace logs to confirm which secrets were injected.

Secrets are never included in policy input. The OPA policy input does not contain secret values. Policy receives only the list of secret names declared in the manifest.

Secrets do not persist beyond the cache TTL. After execution, the RunTask.env map containing decrypted secrets is dropped with the task struct. Decrypted secrets may be held in an in-memory cache (keyed by file mtime + inode) for up to 30 seconds to avoid forking sops -d on every request. The cache is invalidated immediately when the file’s mtime or inode changes (e.g., after rotation). The cache TTL is a compile-time constant (30 s).

Secrets are decrypted on demand with caching. Decrypted secrets are cached in memory for 30 seconds keyed by file mtime + inode. Secret rotation takes effect within this window — or immediately if the file’s mtime changes (e.g., sops re-encrypts on save). No restart is needed for rotation.

Least privilege is enforced at the filter step. Even if the SOPS file contains secrets for every action in the system, each action receives only the secrets listed in its own manifest. The SecretsManager.decrypt_secrets() function discards all keys not in the needed list before returning.

To rotate a secret:

  1. Decrypt, edit, and re-encrypt:
Terminal window
SOPS_AGE_KEY_FILE=/etc/latchgate/sops-age.key sops secrets.enc.yaml
# This opens the file in your editor. Change the value, save, exit.
# SOPS re-encrypts automatically.
  1. No restart required. The next action execution picks up the new value. The cache is invalidated when the file’s mtime changes (SOPS updates mtime on re-encrypt), so updates are typically immediate.

To rotate the age encryption key:

  1. Generate a new age key.
  2. Update .sops.yaml to include both the old and new public keys.
  3. Re-encrypt the secrets file with sops updatekeys secrets.enc.yaml.
  4. Update sops_key_file in latchgate.toml to point to the new key.
  5. Once confirmed working, remove the old key from .sops.yaml and run updatekeys again.

SOPS supports multiple backends: age (recommended), AWS KMS, GCP KMS, Azure Key Vault, and HashiCorp Vault. LatchGate invokes sops -d <file> and reads the JSON output — any backend that SOPS supports works transparently. Configure the backend in .sops.yaml per SOPS documentation.

For AWS KMS:

creation_rules:
- path_regex: \.enc\.(yaml|json)$
kms: "arn:aws:kms:us-east-1:123456789:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

For GCP KMS:

creation_rules:
- path_regex: \.enc\.(yaml|json)$
gcp_kms: "projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key"
ErrorCauseResolution
sops binary not foundsops is not on $PATHInstall SOPS and ensure sops is on $PATH
sops decryption failedWrong key, corrupted file, or backend auth failureVerify sops_key_file, check sops -d manually
failed to parse sops output as JSONSOPS file is not valid YAML/JSON after decryptionRe-encrypt a valid file
key file not foundsops_key_file path does not existCheck the path in latchgate.toml
required secret 'X' not found in sops fileManifest declares required: true but key is missingAdd the key to the SOPS file or set required: false

All secret-related errors cause the action execution to fail (deny) when sops_secrets_file is configured. Secrets management is fail-closed at the decryption and filtering stages. See the caveat above for behavior when sops_secrets_file is not configured.

  • sops_secrets_file is set and points to an encrypted file
  • sops_key_file is set (or the SOPS backend is configured via environment)
  • The age key file has chmod 600 permissions
  • latchgate doctor passes the SOPS check
  • Every action with required: true secrets has matching keys in the SOPS file
  • The SOPS file is backed up (encrypted, so safe to store in version control)
  • Plaintext secrets are not committed to version control

For the full configuration reference, see Configuration. For writing actions that use secrets, see Custom Actions. For the execution pipeline that invokes secret injection, see Core Concepts.