Skip to content

Concepts

Jake Paine edited this page May 23, 2026 · 4 revisions

Concepts

What Keyseal is

A workflow layer on top of SOPS-compatible encrypted YAML documents committed to a Git repo. Keyseal provides naming conventions, schema validation, and runtime output tooling. It does not implement encryption, run a server, or expose an API. Keyseal uses the official SOPS Go decrypt library for read-only decryption in render, exec, and validation paths. The external SOPS binary is still required for commands that create, edit, or rotate encrypted files.

Keyseal also owns the small amount of execution context needed to make decrypt and mutation workflows predictable. In particular, it can pass the configured sops.age_key_file to the SOPS Go decrypt library or SOPS CLI so a team can keep the age private key outside the repo without exporting a shell variable for every command. Mutating commands check sops.binary; read-only decrypt commands do not.

Logical names vs file paths

Every secret is identified by a logical name - a slash-separated path without a file extension, like production/platform/app or staging/infra/redis.

Internally, Keyseal maps these to file paths by joining the logical name with the configured encrypted extension (default: .enc.yaml):

production/platform/app  →  <repo_root>/production/platform/app.enc.yaml
staging/infra/redis      →  <repo_root>/staging/infra/redis.enc.yaml

All commands accept logical names, not file paths. You never pass .enc.yaml extensions on the command line.

Logical names are validated strictly: no absolute paths, no backslashes, no .enc. in the name, no traversal sequences, no redundant path components. The name must be a clean relative path.

The Git-aware commands follow the same rule. keyseal diff production/platform/app, keyseal history production/platform/app, and keyseal rollback production/platform/app --to <commit> all resolve through the same logical-name mapping before they invoke Git.

Encrypted by default

When you run keyseal add, the resulting file at *.enc.yaml is always ciphertext. Keyseal writes the plaintext starter document to a secure temporary file (mode 0600), passes it to sops encrypt --filename-override <target>, and then atomically renames only the ciphertext output to the final path. The temporary file is removed immediately after.

There is no command that writes plaintext to an .enc.yaml path - only sops edit (via keyseal edit) can modify the plaintext content, and it always re-encrypts on save.

Age key resolution

When Keyseal decrypts with the SOPS Go library or invokes the SOPS CLI, the age private key path is resolved in this order:

  • SOPS_AGE_KEY_FILE from the environment
  • sops.age_key_file from keyseal.yaml
  • SOPS's own default lookup

That gives you a stable project-level default in config while preserving shell-level overrides for CI or one-off commands.

Decrypted output

render --stdout and render --out <path> both decrypt secrets to produce usable output. This is intentional. The point of storing secrets in Git is to be able to use them at runtime - render is the mechanism for that.

The default file mode for rendered output is 0600. The safety check in render --out will reject modes with group or world bits unless you pass --force.

Git-backed workflow

Keyseal assumes you are working inside an existing Git repository. It does not create or manage the repo for you.

Git is the source of truth for:

  • Reviewing what changed: keyseal status, keyseal status <logical-name>, keyseal diff, keyseal history
  • Recording a change cleanly: keyseal commit, or --commit / -m on mutating commands
  • Restoring an older encrypted file: keyseal rollback <logical-name> --to <commit>
  • Syncing recipients from .sops.yaml: keyseal updatekeys <logical-name> or keyseal updatekeys --all

Auto-commit is available but off by default. If git.auto_commit: true is set in keyseal.yaml, mutating commands that support config-driven commits commit on success unless you pass --no-commit. A command-line -m always implies commit. updatekeys is explicit-only: it commits only when you pass --commit or -m.

updatekeys calls SOPS to refresh file recipients from .sops.yaml. It does not rotate secret values or the SOPS data encryption key.

Rollback is file restore, not semantic reconstruction. Keyseal asks Git for the encrypted file as it existed in a previous commit and writes that version back to the working tree. If the target file has local changes, rollback refuses to overwrite it.

The Git-aware commands stay scoped to Keyseal-managed files. status <logical-name> narrows the view to one secret, and history --oneline provides a compact per-secret log without exposing general repository history.

Secret documents

Each .enc.yaml file (when decrypted) must conform to the secret document schema:

version: 1
kind: env
name: production/platform/app
description: Core app secrets for production  # optional
values:
  APP_KEY: base64:...
  DB_PASSWORD: hunter2

The kind field must be env - it is the only currently supported kind. The values map must not be empty by default (controlled by validation.require_values in keyseal.yaml). Every key in values must match the configured key pattern (default: ^[A-Z0-9_]+$).

Render and exec

render decrypts one or more secrets, merges their values maps (last document wins on conflicts), and writes the result to a file or stdout in the requested format (dotenv, JSON, or YAML).

exec does the same merge but injects the result into the environment of a subprocess rather than writing a file. The subprocess inherits the current environment, with secret values overlaid.

Neither render nor exec writes secrets to persistent storage beyond what you explicitly ask for.

What doctor is for

A read-only pre-flight check. It inspects keyseal.yaml, mutating-command SOPS CLI availability, age key context, .sops.yaml, and every .enc.yaml file in the repository, then reports what it finds. It never writes or modifies anything.

Checks include: config validity, whether SOPS recipients are real (not placeholder), whether the SOPS CLI is available for mutating commands, whether any .enc.yaml files are plaintext, whether every file decrypts with the library and validates, and whether output path settings are safe.

Exit code is 0 if all checks pass or warn. Exit code is 1 if any check fails. See Doctor for the full check reference.

What Keyseal does not do

  • No access control. Anyone with the age private key can decrypt all files that were encrypted with it. Keyseal does not add any layer of access restriction on top of that.
  • No secret rotation. Keyseal does not update, expire, or rotate values.
  • No secret server. There is no daemon, API, or persistent process.
  • No encryption implementation. Keyseal uses the official SOPS Go decrypt library for read-only decrypt and the external SOPS CLI for mutation. If SOPS CLI is not installed, Keyseal cannot add, edit, or updatekeys, but render/exec/validation can still decrypt with age key material.
  • No Kubernetes, AWS, GCP, or Vault backend. Keyseal works with age keys. It does not support cloud KMS or other backends that SOPS itself supports - though nothing prevents you from configuring SOPS to use those backends independently.
  • No profiles execution. The profiles section of keyseal.yaml is reserved for future use. It is parsed and validated but no command currently uses it.

Clone this wiki locally