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.
How secrets flow
Section titled “How secrets flow”Action manifest SOPS-encrypted filedeclares: 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.
Prerequisites
Section titled “Prerequisites”Install SOPS and age (recommended encryption backend):
# macOSbrew install sops age
# Linux (Debian/Ubuntu)sudo apt install age# SOPS: download from https://github.com/getsops/sops/releasesVerify:
sops --version # >= 3.8.0age --version # >= 1.0.0Via the operator TUI (recommended)
Section titled “Via the operator TUI (recommended)”Switch to the Setup screen, Secrets sub-tab (6 → 5):
latchgate tuii— initialize the encrypted secrets store. Generates an age keypair at.latchgate/sops-age.key(mode 0600), creates an empty encrypted file, and setssops_secrets_file+sops_key_fileinlatchgate.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.↑/↓(ork/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:
# 1. Initialize — generates age key, creates encrypted file, updates latchgate.tomllatchgate secrets init
# 2. Add secretslatchgate secrets set GITHUB_TOKEN ghp_xxxxxxxxxxxxlatchgate secrets set SLACK_BOT_TOKEN xoxb-xxxxxxxxxxxxlatchgate secrets set STRIPE_KEY sk_live_xxxxxxxxx
# 3. Verify coveragelatchgate secrets list
# 4. Validatelatchgate doctorsecrets 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.
Manual setup (alternative)
Section titled “Manual setup (alternative)”If you need custom paths, a different encryption backend, or an existing age key, set up manually:
1. Generate an age key
Section titled “1. Generate an age key”mkdir -p /etc/latchgateage-keygen -o /etc/latchgate/sops-age.keyNote the public key from the output (starts with age1...). Protect the key file:
chmod 600 /etc/latchgate/sops-age.key2. Create a .sops.yaml configuration
Section titled “2. Create a .sops.yaml configuration”In the directory where your secrets file will live, create .sops.yaml:
creation_rules: - path_regex: \.enc\.(yaml|json)$ age: "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3lk..." # your public key3. Create and encrypt the secrets file
Section titled “3. Create and encrypt the secrets file”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:
sops -e secrets.yaml > secrets.enc.yamlrm secrets.yaml # delete the plaintext immediatelyVerify you can decrypt:
SOPS_AGE_KEY_FILE=/etc/latchgate/sops-age.key sops -d secrets.enc.yaml4. Configure LatchGate
Section titled “4. Configure LatchGate”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.
5. Verify with latchgate doctor
Section titled “5. Verify with latchgate doctor”latchgate doctorThe 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.
Declaring secrets in action manifests
Section titled “Declaring secrets in action manifests”Each action manifest declares which secrets it needs:
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 secretThe 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: falseexecute 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.
Security properties
Section titled “Security properties”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.
Secret rotation
Section titled “Secret rotation”To rotate a secret:
- Decrypt, edit, and re-encrypt:
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.- 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.
Key rotation (age keys)
Section titled “Key rotation (age keys)”To rotate the age encryption key:
- Generate a new age key.
- Update
.sops.yamlto include both the old and new public keys. - Re-encrypt the secrets file with
sops updatekeys secrets.enc.yaml. - Update
sops_key_fileinlatchgate.tomlto point to the new key. - Once confirmed working, remove the old key from
.sops.yamland runupdatekeysagain.
Alternative encryption backends
Section titled “Alternative encryption backends”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"Error handling
Section titled “Error handling”| Error | Cause | Resolution |
|---|---|---|
sops binary not found | sops is not on $PATH | Install SOPS and ensure sops is on $PATH |
sops decryption failed | Wrong key, corrupted file, or backend auth failure | Verify sops_key_file, check sops -d manually |
failed to parse sops output as JSON | SOPS file is not valid YAML/JSON after decryption | Re-encrypt a valid file |
key file not found | sops_key_file path does not exist | Check the path in latchgate.toml |
required secret 'X' not found in sops file | Manifest declares required: true but key is missing | Add 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.
Production checklist
Section titled “Production checklist”-
sops_secrets_fileis set and points to an encrypted file -
sops_key_fileis set (or the SOPS backend is configured via environment) - The age key file has
chmod 600permissions -
latchgate doctorpasses the SOPS check - Every action with
required: truesecrets 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.