Skip to content

fix(sops-audit): allowlist non-secret values, and skip TOML-encrypted files - #75

Merged
nkg merged 1 commit into
mainfrom
fix/plaintext-scan-allowlist
Sep 1, 2026
Merged

fix(sops-audit): allowlist non-secret values, and skip TOML-encrypted files#75
nkg merged 1 commit into
mainfrom
fix/plaintext-scan-allowlist

Conversation

@nkg

@nkg nkg commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Closes #74.

The problem

The plaintext-secret scan matches any 8+ character value after a secret-ish
key, with no notion of what a value means. Ordinary configuration trips it:

update_password: on_create     # flagged

password matches as a substring of update_password, and on_create is
nine characters. That is the documented API of ansible.builtin.user — a
consumer cannot reword their way out of it, and their only lever was
plaintext-scan-exclude on a whole file, disarming real detection across a
file that manages credentials to silence one line.

Found the hard way in HordiaLabs/platform, where main was red for hours
on a comment containing that pair.

The fix

Two classes are suppressed, both cases where the value cannot be a secret:

Class Examples
Indirection — the value names something rather than being it {{ vault_pw }}, ${API_KEY}, $TOKEN, !vault, lookup(...), ENC[...]
Config enums — a closed list of setting values on_create, always, never, true/false, enabled/disabled

Deliberately not allowlisted: changeme, placeholder, password,
secret. Those are plausible real bad values and keep failing. The allowlist
only ever suppresses — it never widens what counts as a secret.

Also: TOML-encrypted files were being scanned

The SOPS skip covered the YAML, JSON and dotenv markers but not [sops], so
an encrypted .toml was scanned rather than skipped — even though .toml is
in the scanned extension list. Verified against all four forms; only the
plain .toml is now scanned.

No false positive today, because ENC[AES256_GCM,...] breaks the value
character class. Fragile rather than broken, which is worth fixing before it
becomes the latter.

Tests

tests/plaintext-scan.test.sh, wired into self-test.yml. 21 fixtures,
and the important half are real secrets that must still be caught:

── must still be caught (real secrets) ──
  ok    flag      aws secret w/ slashes
  ok    flag      base64 private key
  ok    flag      changeme NOT allowed
  ...
── must now be allowed (cannot be a secret) ──
  ok    clean     ansible enum
  ok    clean     jinja template
  ...
plaintext-scan.test.sh: 21 passed

This matters more than usual here. An allowlist can only make a scanner
less sensitive, and that is the direction that fails silently — a scanner
that stops detecting reports success. So:

  • The test extracts KEY/SEP/Q/ENUMS from the workflow itself
    rather than restating them. A copy would drift, and a drifted test proves
    nothing about what actually runs.
  • I checked the guard bites: adding changeme to the enum list turns the
    suite red, not quietly green.

For whoever merges

@v2 is a floating tag, so re-pointing it publishes this to every consumer
repo at once
. Worth landing deliberately rather than on autopilot — that is
why this is a PR and not a push.

🤖 Generated with Claude Code

https://claude.ai/code/session_017EDkXRcCrVDfu2e8V9Yy52

… files

Closes #74.

The plaintext-secret scan matched any 8+ character value after a secret-ish
key, with no notion of what a value MEANS. Ordinary configuration tripped it:
`update_password: on_create` matches on the `password` substring, and
`on_create` is nine characters. That is the documented API of
ansible.builtin.user, so a consumer cannot reword their way out of it, and
their only lever was plaintext-scan-exclude on a whole file — disarming real
detection across a file that manages credentials to silence one line.

Two classes are now suppressed, both cases where the value CANNOT be a
secret:

  1. Indirection — the value names something rather than being it. Jinja/Go
     templates, shell and env expansion, Ansible vault refs, lookups, and
     SOPS ENC[...] ciphertext.
  2. Config enums — a closed list of setting values: on_create, always,
     never, true/false, enabled/disabled, and similar.

Deliberately NOT allowlisted: changeme, placeholder, password, secret. Those
are plausible real bad values and keep failing. The allowlist only ever
suppresses; it never widens what counts as a secret.

Also fixes the SOPS skip missing TOML. The regex covered the YAML, JSON and
dotenv metadata markers but not `[sops]`, so an encrypted .toml was scanned
rather than skipped even though .toml is in the scanned extension list. No
false positive today only because ENC[...] breaks the value character class.

tests/plaintext-scan.test.sh, wired into self-test.yml. 21 fixtures, and the
important half are real secrets that must STILL be caught — an AWS key with
slashes, a base64 private key, a github token, and changeme.

This matters more than usual. The workflow reaches every consumer repo
through the floating @v2 tag, and an allowlist can only make the scan LESS
sensitive, which is the direction that fails silently: a scanner that stops
detecting reports success. So the test extracts KEY/SEP/Q/ENUMS from the
workflow itself rather than restating them, because a drifted copy would
prove nothing about what actually runs.

Verified the guard bites: adding `changeme` to the enum list turns the suite
red rather than quietly green.

Note for whoever merges: @v2 is a floating tag, so re-pointing it publishes
this to every consumer at once. Worth landing deliberately.
@nkg
nkg merged commit 18d7bed into main Sep 1, 2026
12 checks passed
@nkg
nkg deleted the fix/plaintext-scan-allowlist branch September 1, 2026 13:59
nkg added a commit that referenced this pull request Sep 1, 2026
PR #75 landed the sops-audit allowlist, the TOML skip fix and the
plaintext-scan fixtures without a changelog entry; write it now as the
3.2.1 section so release.yml has notes to publish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHRYacEb7ABoYsNn7X71VY
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.

sops-audit: plaintext-secret scan has no allowlist, flags ordinary config as secrets

1 participant