Skip to content

Secrets Management

Griffen Fargo edited this page Jun 25, 2026 · 3 revisions

Secrets Management

Manage environment secrets across local and VPS environments. strut provides tools to generate, source from secret managers, sync, encrypt at rest, diff, rotate, and validate .env files.

Since: v0.25.0 (init-secrets), v0.26.0 (secrets push/pull/diff/validate), v0.27.0 (hydrate, lock/unlock, rotate, template, export, status)

Overview

Command Purpose
init-secrets Generate a populated .env from a .env.template (random values)
secrets hydrate Build .env from template, resolving secret references (vault, exec, file)
secrets push Upload local .env to VPS (secure SCP, mode 600)
secrets pull Download .env from VPS to local
secrets diff Compare local vs remote keys (values redacted)
secrets validate Check required_vars, detect placeholders, weak secrets, unresolved refs
secrets status Show the secrets pipeline state for a stack
secrets rotate Re-hydrate/re-generate, validate, push, and optionally restart
secrets template Reverse-engineer a .env.template from an existing .env
secrets export Export .env to docker-secret, k8s-secret, or env-json format
secrets lock Encrypt .env at rest using age or GPG (safe to commit)
secrets unlock Decrypt an encrypted .env back to plaintext

Generating Secrets (init-secrets)

Start from a stack's .env.template and auto-generate cryptographic secrets for password, token, and key variables:

strut my-app init-secrets --env prod
strut my-app init-secrets --env prod --dry-run   # preview without writing
strut my-app init-secrets --env staging --force  # overwrite existing values

How It Works

  1. Reads stacks/<stack>/.env.template
  2. Detects placeholder values (changeme, xxxx, empty, etc.)
  3. Auto-generates secrets based on key name patterns:
    • *SECRET*, *JWT* — 32-byte hex
    • *PASSWORD* — 16-byte hex
    • *SALT* — 16-byte hex
    • *TOKEN*, *KEY* (generic) — 24-byte hex
    • *ENCRYPTION* — 32-byte hex
  4. Respects generation hints in comments:
    # Generate with: openssl rand -hex 64
    MY_SECRET=changeme
  5. Leaves API keys, domains, and URLs as-is (user must fill manually)

Safety

  • Never overwrites existing non-placeholder values (unless --force)
  • Safe to re-run — only fills in missing/placeholder values
  • Variables needing manual input (API keys, external URLs) are left untouched

Placeholder Detection

These patterns are recognized as placeholders:

  • Empty values
  • changeme, CHANGEME, Change-Me
  • your-*, YOUR_*
  • xxxx, XXXX
  • replace-*, REPLACE_*
  • todo, TODO, fixme, FIXME
  • example, placeholder

Hydrating from Secret Managers (since v0.27.0)

secrets hydrate resolves secret references in your .env.template from external sources:

strut my-app secrets hydrate --env prod
strut my-app secrets hydrate --env prod --dry-run

Secret Reference Syntax

In your .env.template, use these prefixes:

Prefix Source Example
vault://<item> Vaultwarden/Bitwarden (via bw CLI) DB_PASS=vault://my-db-creds
exec://<command> Stdout of a shell command TOKEN=exec://op read "My Token"
file://<path> Contents of a file CERT=file:///run/secrets/cert
(plain value) Literal — copied as-is PORT=3000

Security note: exec:// runs commands with your local privileges. Only hydrate templates you trust.

Pluggable Providers

The hydrate system is extensible. Providers are auto-detected based on the reference prefix. You can add custom providers following the extensibility guide in the project docs.

Syncing Secrets to VPS

Push

Upload local .env to the VPS:

strut my-app secrets push --env prod
strut my-app secrets push --env prod --force     # overwrite existing remote
strut my-app secrets push --env prod --dry-run

Before pushing, strut automatically validates required_vars (if the file exists). If any required variables are missing, the push aborts.

Pull

Download the remote .env to local:

strut my-app secrets pull --env prod
strut my-app secrets pull --env prod --force     # overwrite local file

Diff

Compare local and remote env files without exposing values:

strut my-app secrets diff --env prod

Output shows:

  • Changed — keys present in both but with different values
  • Only in local — keys you have locally but not on VPS
  • Only on remote — keys on VPS you don't have locally

Values are never displayed — only key names.

Validate

Check that all variables in required_vars are present, non-empty, and well-formed:

strut my-app secrets validate --env prod

Since v0.27.0, validate also detects:

  • Placeholder values that were never filled in
  • Weak secrets (short, low-entropy)
  • Unresolved secret references (e.g. leftover vault:// or exec:// strings)

Secrets Status (since v0.27.0)

Show the full secrets pipeline state for a stack:

strut my-app secrets status --env prod

Displays:

  • Local env file info (path, size, variable count)
  • Template references and whether they're hydrated
  • Remote sync state (in-sync, stale, or missing)
  • required_vars coverage
  • Deploy key presence

Secrets Rotation (since v0.27.0)

Rotate secrets in one step — re-hydrate or re-generate, validate, push to VPS, and optionally restart containers:

strut my-app secrets rotate --env prod
strut my-app secrets rotate --env prod --restart
strut my-app secrets rotate --env prod --dry-run

Template Generation (since v0.27.0)

Reverse-engineer a .env.template from an existing .env file. Useful when you have a working env and want to create a template for the team:

strut my-app secrets template --env prod
strut my-app secrets template --env prod --output stacks/my-app/.env.template

Export (since v0.27.0)

Export your .env to other formats for use in different deployment systems:

strut my-app secrets export --env prod --format docker-secret
strut my-app secrets export --env prod --format k8s-secret
strut my-app secrets export --env prod --format env-json

Encryption at Rest — Lock/Unlock (since v0.27.0)

Encrypt .env files so they're safe to commit to version control:

strut my-app secrets lock --env prod       # .prod.env -> .prod.env.age
strut my-app secrets unlock --env prod     # .prod.env.age -> .prod.env

How It Works

  • Backends: age (preferred), gpg (fallback) — auto-detected
  • Recipients: Create .strut-recipients with age or SSH public keys (one per line). If absent, encrypts to self using ~/.ssh/id_ed25519.pub
  • Identity: --identity <file> or STRUT_AGE_IDENTITY env var. Defaults to ~/.age/key.txt, ~/.ssh/id_ed25519, or ~/.ssh/id_rsa

Workflow with Lock/Unlock

# Decrypt to edit
strut my-app secrets unlock --env prod
nano .prod.env

# Re-encrypt before committing
strut my-app secrets lock --env prod
git add .prod.env.age
git commit -m "update prod secrets"

Typical Workflow

# 1. Create .env from template with generated secrets
strut my-app init-secrets --env prod
#    OR hydrate from a secret manager
strut my-app secrets hydrate --env prod

# 2. Fill in remaining values manually (API keys, domains)
nano .prod.env

# 3. Validate everything is set
strut my-app secrets validate --env prod

# 4. Push to VPS
strut my-app secrets push --env prod

# 5. Encrypt for safe storage in git
strut my-app secrets lock --env prod

# 6. Deploy
strut my-app release --env prod

File Resolution

strut looks for env files in this order:

  1. stacks/<stack>/.<env>.env (stack-level)
  2. <project_root>/.<env>.env (project-level)

Remote path: <deploy_dir>/.<env>.env

Relationship to Other Tools

Tool Purpose
init-secrets Generate initial .env from template (random values)
secrets hydrate Resolve secret refs from vault/exec/file providers
secrets push/pull Sync .env between local and VPS
secrets diff Compare local vs remote
secrets lock/unlock Encrypt/decrypt .env at rest
secrets rotate Full rotation cycle (re-gen + validate + push + restart)
secrets template Generate .env.template from existing .env
secrets export Convert .env to docker-secret, k8s-secret, env-json
ssh:keygen Generate deploy keypairs for CI/CD
ci:init Bootstrap CI/CD secrets from topology
keys env:* Rotate/validate individual env vars
drift detect Detect config file drift (broader than just secrets)

Clone this wiki locally