Skip to content

fix(config): replace TOKEN_SIGNING_SECRET entropy heuristic#30

Merged
babs merged 3 commits intomasterfrom
fix/secret-entropy-gate
Apr 28, 2026
Merged

fix(config): replace TOKEN_SIGNING_SECRET entropy heuristic#30
babs merged 3 commits intomasterfrom
fix/secret-entropy-gate

Conversation

@babs
Copy link
Copy Markdown
Owner

@babs babs commented Apr 28, 2026

Summary

The previous distinct-byte heuristic on TOKEN_SIGNING_SECRET was structurally broken in both directions, surfaced by an external review pass:

  • False positive. A real openssl rand -hex 32 value is drawn from a 16-symbol alphabet; expected number of distinct chars in 64 draws is ~15.75, so a genuinely random secret intermittently fell below the < 16 threshold and was rejected at startup under PROD_MODE=true. The README itself recommended this generator.
  • False negative. Literals like `0123456789abcdef0123456789abcdef` have exactly 16 distinct chars and passed the gate despite trivially repeating with period 16. A patterned secret with no actual entropy past the first 16 bytes would clear the check.

Replacement: three-part shape check

  1. All-same byte (period 1) → `is N copies of a single byte`.
  2. Repeating period < len(b) → `repeats with period P`. Catches both the period-16 false-pass above and shorter shapes like `abcabc…`.
  3. Tiny alphabet (< 8 distinct values) → catches the `aaaa…b`-shaped near-miss and uneven-run-length shapes (`aaaaabbbbbcccccddddd…`) that defeat both the period and all-same checks.

Threshold = 8 is the most defensive floor with zero practical false-positive rate on real random output: 32 hex chars over a 16-symbol alphabet expect ~14 distinct; base64 / raw expect more.

Behavioral envelope unchanged

Same two enforcement sites as before — warn path during secret parsing, hard-fail under `PROD_MODE`. Dev / single-replica deployments that intentionally use a weak secret keep working under `PROD_MODE=false` with a startup warning.

Tests

Test What it pins
`TestLoad_SecretWeaknessWarning_PeriodicRepeat` Headline regression for the famous false-pass: rejects `0123456789abcdef0123456789abcdef`.
`TestLoad_SecretWeaknessWarning_TinyAlphabet` 4-case boundary table: 2-distinct rejects, 7-distinct rejects (just below floor), 8-distinct passes (at floor, true 8-symbol non-periodic fixture), 15-distinct passes.
`TestLoad_SecretWeaknessWarning_NonPeriodic` False-positive direction: frozen `openssl rand -hex 32`, base64-shaped, and alphanumeric-32 samples all pass.
`TestWeakSecretReason_FreshRandomNeverFlags` 9000-sample experiment per CI run (1000 iters × 3 encodings × 3 raw-byte lengths). Demonstrates zero false-positives on fresh `crypto/rand` output across raw, hex, and base64 shapes at lengths 32 / 48 / 64. A flake here is a real bug, not test noise.
`TestLoad_ProdMode_BlocksWeakSecret` Now table-driven (3 shapes × 2 modes = 6 sub-tests).

The `setAllRequired` test fixture's `TOKEN_SIGNING_SECRET` moved from the literal famous false-pass (`0123456789abcdef…`) to a non-periodic real-random hex sample so every existing test runs against a secret the new gate accepts.

`distinctByteCount` removed entirely — no dead code, no temptation for a future maintainer to resurrect the broken heuristic.

Docs

`specs.md`, `README.md`, and `docs/configuration.md` all describe the new semantics and point at `manifests/scripts/generate-signing-secret.sh` as the canonical generator path.

Test plan

  • CI green (`test`, `lint`, `keycloak-e2e`, `fuzz-smoke`, `manifest-prod`, `govulncheck`, `build`).
  • `go test ./config/ -run "Secret|FreshRandom|TinyAlphabet" -v` passes (8000+ assertions land in the new tests).
  • Manual sanity: a real `openssl rand -hex 32` value loads cleanly under `PROD_MODE=true` (was previously intermittent).

babs added 3 commits April 28, 2026 02:40
The previous distinct-byte check was structurally broken in two
directions:

  - False positive: a real `openssl rand -hex 32` value is drawn
    from a 16-symbol alphabet; the expected number of distinct
    chars in 64 draws is ~15.75, so a genuinely random secret
    intermittently fell below the < 16 threshold and was rejected
    at startup under PROD_MODE=true.

  - False negative: literals like
    `0123456789abcdef0123456789abcdef` have exactly 16 distinct
    chars and passed the gate despite trivially repeating with
    period 16. A patterned secret with no actual entropy past the
    first 16 bytes would clear the check.

Replaced with a three-part shape check on the raw secret bytes:

  1. All-same byte (period 1) → "is N copies of a single byte".
  2. Repeating period < len(b) → "repeats with period P". Catches
     both the period-16 false-pass above and shorter shapes like
     `abcabc…`.
  3. Tiny alphabet (< 8 distinct values) → catches the
     `aaaa…b`-shaped near-miss and uneven-run-length shapes
     (`aaaaabbbbbcccccddddd…`) that defeat both the period and
     all-same checks. Threshold of 8 is the most defensive floor
     with zero practical false-positive rate on real random
     output: 32 hex chars over a 16-symbol alphabet expect ~14
     distinct, base64 / raw expect more.

The new check fires at the same two sites as the old one — the
warn path during secret parsing, and the hard-fail path under
PROD_MODE — so dev / single-replica deployments that intentionally
use a weak secret keep working under PROD_MODE=false with a
startup warning.

Tests:

  - TestLoad_SecretWeaknessWarning_PeriodicRepeat is the headline
    regression for the famous false-pass: rejects
    `0123456789abcdef0123456789abcdef`.
  - TestLoad_SecretWeaknessWarning_TinyAlphabet is a 4-case
    boundary table: 2-distinct rejects, 7-distinct rejects (just
    below the floor), 8-distinct passes (at the floor, true
    8-symbol non-periodic fixture), 15-distinct passes.
  - TestLoad_SecretWeaknessWarning_NonPeriodic covers the
    false-positive direction with frozen `openssl rand -hex 32`,
    base64-shaped, and alphanumeric-32 samples.
  - TestWeakSecretReason_FreshRandomNeverFlags is a 9000-sample
    experiment per CI run (1000 iterations × 3 encodings × 3
    raw-byte lengths) demonstrating zero false-positives on fresh
    crypto/rand output across raw, hex, and base64 shapes at
    lengths 32 / 48 / 64.
  - TestLoad_ProdMode_BlocksWeakSecret is now table-driven (3
    shapes × 2 modes = 6 sub-tests).
  - The setAllRequired test fixture's TOKEN_SIGNING_SECRET moved
    from the literal famous false-pass to a non-periodic
    real-random hex sample so every existing test runs against a
    secret the new gate accepts.
  - distinctByteCount removed entirely.

Docs aligned: specs.md, README.md, and docs/configuration.md
all describe the new semantics and point at
manifests/scripts/generate-signing-secret.sh as the canonical
generator path.
The validator rejects three weak-secret shapes — all-same byte,
short repeating period, AND tiny alphabet (< 8 distinct values).
The prose in README, docs/configuration.md, and specs.md
described only the first two; the tiny-alphabet floor (added in
this PR) was visible only via the inline Go code. A reader of
the docs alone now sees the full contract and the rationale for
each class.
The Demo stack section described the script as emitting "a 32-byte
random hex string". The script actually emits a 64-character
cryptographically-random base64 string (it pipes
`openssl rand -base64 48` through tr-d and head -c 64). Same
ballpark of bits of entropy, different encoding.
@babs babs merged commit 6046c0f into master Apr 28, 2026
7 checks passed
@babs babs deleted the fix/secret-entropy-gate branch April 28, 2026 00:49
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.

1 participant