Skip to content

Add SOPS secrets adapter - #1906

Open
indiebrain wants to merge 2 commits into
basecamp:mainfrom
indiebrain:add-sops-secrets-adapter
Open

Add SOPS secrets adapter#1906
indiebrain wants to merge 2 commits into
basecamp:mainfrom
indiebrain:add-sops-secrets-adapter

Conversation

@indiebrain

@indiebrain indiebrain commented Jul 1, 2026

Copy link
Copy Markdown

What

Adds a sops secrets adapter, so Kamal can pull deploy-time secrets from a
sops-encrypted file:

kamal secrets fetch --adapter sops --from config/secrets.enc.yaml REGISTRY_PASSWORD DB_PASSWORD

--from names the encrypted file; the positional arguments are the keys to read
from it. This matches the existing adapter convention where --from is the
container and the arguments are items within it (AWS, Doppler, 1Password, GCP).

Why

sops is a widely used, file-based secrets tool (age / AWS·GCP·Azure KMS / PGP).
Teams that keep an encrypted secrets file in their repo — the common GitOps
pattern — currently have to shell out to sops by hand and reshape its output
before Kamal can use it. This adapter makes it a first-class source.

Unlike most hosted password-manager adapters, sops adds little ongoing
maintenance surface: it is a single, stable, actively maintained CLI (now a CNCF
project) with no hosted account and no per-vendor login flow. The adapter shells
out to exactly one command and parses JSON.

Behavior

  • No --account — sops resolves its own decryption key from its config and
    environment, so requires_account? is false (like aws_secrets_manager and
    doppler). Following the existing convention, --account is ignored if given.
  • One decrypt per callsops --decrypt --output-type json -- <file>. The
    adapter passes --output-type json to sops internally (it is not a user-facing
    option), normalizing YAML / JSON / dotenv / INI sources to a single JSON parse
    path; the -- guards against a filename that looks like a flag.
  • Nested keys are flattened into parent/child paths, and non-string values
    (numbers, booleans, lists) are coerced to strings — the same treatment merged
    for AWS Secrets Manager in Stringify AWS Secrets Manager values #1833.
  • Fetch-all when no keys are given (returns every flattened key), and a
    parent key selects its whole subtree.
  • Distinct, tested errors for a missing --from, an unknown key, a decrypt
    failure, and a missing sops CLI.

Notes

  • sops is an operator-installed CLI, not a new gem dependency — presence is
    checked at runtime via check_dependencies!, exactly like the other adapters.
  • The decrypt command escapes the filename and uses -- for end-of-options, in
    the spirit of the OS-command-injection hardening merged in Security Fix: Prevent OS Command Injection in Passbolt adapter (CWE-78) #1697 (CWE-78).
  • Binary-format sops files (an opaque whole-file blob) are out of scope — a
    key/value secrets adapter has nothing addressable to extract from them.

Tests

test/secrets/sops_adapter_test.rb covers specific-key fetch, nested flattening,
fetch-all, non-string coercion, subtree-by-parent, and all four error paths.
bin/test passes (the only failures are the Docker-based test/integration
suite, unrelated to this change). rubocop is clean.

Docs

Companion documentation for kamal-deploy.org is proposed in basecamp/kamal-site#201.

Fetch deploy-time secrets from a sops-encrypted file via
`kamal secrets fetch --adapter sops --from secrets.enc.yaml KEY...`.

The `--from` option names the encrypted file; positional args are keys
within it. The file is decrypted once with `sops --decrypt --output-type
json`, nested structures are flattened into `parent/child` paths, and
non-string values are coerced to strings. Passing no keys returns every
key in the file, and a parent key selects its whole subtree. sops resolves
its own decryption key, so no `--account` is required.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new sops-based secrets adapter so kamal secrets fetch can decrypt a sops-encrypted file at deploy time, flatten nested keys into parent/child paths, and return secrets as JSON.

Changes:

  • Added Kamal::Secrets::Adapters::Sops adapter that decrypts via sops --decrypt --output-type json and supports fetch-all, subtree selection, and non-string coercion.
  • Added a dedicated test suite covering normal fetch behavior, flattening/coercion behavior, and key/dependency/decrypt error paths.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/kamal/secrets/adapters/sops.rb Implements the sops secrets adapter including decrypt/flatten/select and dependency checks.
test/secrets/sops_adapter_test.rb Adds unit tests validating adapter behavior and expected error messages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/kamal/secrets/adapters/sops.rb
Guard against a sops file whose top-level document is not an object (for
example a JSON array or scalar): raise a clear error instead of letting a
non-Hash value reach the flattening logic. Covered by a new test.
@indiebrain indiebrain changed the title Add sops secrets adapter Add SOPS secrets adapter Jul 1, 2026
@indiebrain
indiebrain requested a review from Copilot July 1, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants