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

Doctor

keyseal doctor inspects your repository and reports on its health. It is non-destructive - it never writes files or modifies configuration. Exit code is 0 if all checks pass or warn; exit code is 1 if any check fails. After loading keyseal.yaml, doctor reports SOPS and age tool availability before deeper repository checks.

For CI and release gates, use keyseal verify. It runs the same checks but fails on warnings as well as failures.

Running doctor

keyseal doctor

Text output:

doctor summary: 11 ok, 0 warning(s), 0 failure(s), 0 skipped
[OK] sops binary: SOPS binary is available
[OK] age binary: age binary is available
[OK] keyseal.yaml: Parsed and validated keyseal.yaml
[OK] .sops.yaml: Parsed .sops.yaml
[OK] .sops.yaml creation rules: 2 creation rule(s) include recipient material
[OK] .sops.yaml placeholders: No obvious placeholder recipients were found in .sops.yaml
[OK] repository.root: repository.root resolves to a directory
[OK] defaults.file_mode: defaults.file_mode 0600 is owner-only
[OK] render output paths: Configured render output paths look sane
[OK] repo artifacts: No obvious generated artifacts were found in the repository root
[OK] secret discovery: Discovered 3 encrypted secret file(s)
...

For machine-readable output:

keyseal doctor --json
{
  "summary": {
    "ok": 8,
    "warn": 1,
    "fail": 0,
    "skip": 0,
    "total": 9
  },
  "checks": [
    {
      "name": "keyseal config",
      "status": "ok",
      "severity": "informational",
      "summary": "keyseal.yaml loaded and valid",
      "details": ["root: .", "extension: .enc.yaml", "binary: sops"]
    },
    ...
  ]
}

Check reference

Checks run in a fixed order. Some checks gate later checks - if config fails to load, secret validation is skipped.


keyseal config

What it checks: Whether keyseal.yaml exists in the current directory and loads without errors.

Fail conditions:

  • keyseal.yaml does not exist
  • File exists but fails config.Load (invalid YAML, wrong version, bad regexp in key_pattern, etc.)

Details on pass: reports the resolved root, encrypted_extension, and sops.binary values.

Remediation:

# if missing:
keyseal init

# if invalid, review the error message from:
keyseal doctor

sops config

What it checks: Whether .sops.yaml exists in the current directory and is parseable.

This check runs independently of the config check - even if keyseal.yaml fails to load, .sops.yaml is still inspected.

Fail conditions:

  • .sops.yaml does not exist
  • File exists but cannot be parsed as YAML

Details on pass: reports the number of creation rules found.


sops creation rules

What it checks: Whether the SOPS creation rules have usable recipients configured.

Fail conditions:

  • No creation rules exist in .sops.yaml
  • Rules exist but none have any usable recipient material (no age, pgp, kms, gcp_kms, azure_keyvault, or hc_vault_transit_uri keys with non-empty content, and no key_groups with recipients)

Details on pass: reports how many rules have usable recipients.


sops placeholder check

What it checks: Whether .sops.yaml contains placeholder text that was not replaced after keyseal init.

The check scans the raw file text for the pattern:

\bage1[A-Za-z0-9_]*REPLACE_ME[A-Za-z0-9_]*\b|\bREPLACE_ME\b

Fail conditions: Any match found. The check reports each unique placeholder string found.

Why this matters: SOPS will accept these as age recipients syntactically but they do not correspond to any real key. Files "encrypted" with placeholder recipients cannot be decrypted by anyone.

Remediation: Replace every placeholder in .sops.yaml with a real age public key:

# generate a new key pair
age-keygen -o ~/.config/sops/age/keys.txt
# the public key is printed; paste it into .sops.yaml

sops binary

What it checks: Whether sops.binary resolves and runs sops --version for mutating commands.

Missing SOPS CLI is informational for read-only deployment machines. render, exec, doctor, and verify use the official SOPS Go decrypt library and continue decrypt validation without the external binary. add, edit, and updatekeys still require SOPS CLI and fail clearly when it is unavailable.


age binary

What it checks: Whether sops.age_binary resolves and runs age --version for admin key workflows.

Missing age CLI is informational for production/deploy machines. Read-only decrypt operations need age private key material, not the external age binary.


repository root

What it checks: Whether repository.root (from keyseal.yaml) exists as a directory.

Fail conditions:

  • The path does not exist
  • The path exists but is not a directory

If this check fails, secret discovery is skipped.


output path safety

What it checks: The defaults.file_mode value for unsafe permissions.

Warn conditions: The mode has group or world bits set (anything other than owner-only). Example: 0644 produces a warning; 0600 passes.

Doctor warns rather than fails - having a non-strict default mode is a configuration choice, not necessarily an error. But render --out will refuse to use an unsafe mode unless --force is passed.


output paths

What it checks: Whether defaults.output_dir and any paths in profiles.*.renders[*].out are relative paths or use path traversal.

Fail conditions: Empty path, path traversal (../ prefix after filepath.Clean).

Warn conditions: A path that is valid but relative (relative paths make it easy to accidentally commit rendered output files).

Note: The profiles section is not currently executed by any command, but these paths are still validated so you do not have insecure or surprising defaults when profiles are used.


repo artifacts

What it checks: Whether unexpected build artifacts are present in the repository root.

Warn conditions:

  • A regular file named keyseal exists in <repository.root> (a compiled binary left in the repo)
  • A non-empty dist/ directory exists in <repository.root> (release archives left behind)

Neither of these causes a failure - they are hygiene warnings. Compiled binaries and dist archives should not be committed to the repository.

secret files

What it checks: Counts the encrypted files discovered under repository.root.

Discovery walks the entire repository tree (skipping .git) and collects all files ending in the configured encrypted_extension.

Pass with zero files: reports "No encrypted secret files were discovered yet" - not a failure, just informational.

Pass with files: reports the count.

Per-file checks follow:


Per-file checks (name <relative path>)

For each discovered file, doctor runs:

  1. Logical name mapping: verifies PathToLogicalName succeeds (path maps to a valid logical name that does not escape the repo root)
  2. Round-trip validation: verifies LogicalNameToPath of the mapped name produces the original path
  3. Empty placeholder detection: if the file is empty or whitespace-only, it is reported as WARN and decrypt/schema validation are skipped
  4. Plaintext detection: if the file is non-empty and does not contain SOPS metadata, it is flagged as FAIL because non-empty plaintext at an encrypted path must not be ignored
  5. Decrypt: uses the official SOPS Go decrypt library; failure is reported as FAIL with decrypt context
  6. Schema parse: checks decrypted output is valid YAML with the expected document structure
  7. Validate: checks all fields and key patterns against config validation rules
  8. Duplicate key check: if any duplicate keys in values, reports WARN listing the duplicates
  9. SOPS compatibility warning check: if the SOPS Go library reports compatibility warnings while decrypting, reports WARN after successful schema validation

Decrypt validation is not skipped merely because the external SOPS CLI is unavailable.

SOPS compatibility warnings are intentionally shown in doctor and verify, not in render or exec. This keeps plaintext render output clean while still telling developers when a file may need to be opened and re-saved or re-encrypted with a current SOPS CLI. One common example is an older SOPS file containing a possibly unencrypted YAML comment.


How to fix common failures

Finding Fix
keyseal.yaml missing Run keyseal init
keyseal.yaml invalid Check the error message; usually a wrong version, bad file_mode, or invalid regexp
.sops.yaml missing Run keyseal init or create .sops.yaml manually
Placeholder recipients Replace age1REPLACE_ME values in .sops.yaml with real age public keys
Empty placeholder file Populate it with keyseal edit <logical-name> or recreate it with keyseal add <logical-name>
Plaintext at .enc.yaml path Re-encrypt: rm <file>; keyseal add <logical-name>, then edit with keyseal edit
SOPS binary not found Required only for add, edit, and updatekeys; install sops or set sops.binary before mutating files
age binary missing Install age only on machines that need key generation or inspection; production decrypt needs the age key, not the age CLI
decrypt fails Check that your age private key is at SOPS_AGE_KEY_FILE or sops.age_key_file and is readable
SOPS compatibility warning Open and save or re-encrypt the file with a current SOPS CLI on a developer/admin machine, then rerun doctor
Unsafe file mode Update defaults.file_mode in keyseal.yaml to "0600" or use --force deliberately
Relative output path Use an absolute path in defaults.output_dir
keyseal binary in repo Delete it; add /keyseal to .gitignore
dist/ in repo Delete or move it; add /dist/ to .gitignore

Using JSON output in scripts

# get all failing checks
keyseal doctor --json | jq '.checks[] | select(.status == "fail")'

# count failures
keyseal doctor --json | jq '.summary.fail'

# extract remediation steps for failures
keyseal doctor --json | jq '.checks[] | select(.status == "fail") | .remediation[]'

Clone this wiki locally