Skip to content

fix(verifier): fail-fast on missing/weak JWT secret (token-forgery P0) - #582

Closed
NeOMakinG wants to merge 2 commits into
mainfrom
fix/verifier-jwt-secret-validation
Closed

fix(verifier): fail-fast on missing/weak JWT secret (token-forgery P0)#582
NeOMakinG wants to merge 2 commits into
mainfrom
fix/verifier-jwt-secret-validation

Conversation

@NeOMakinG

Copy link
Copy Markdown
Contributor

Fail-fast on a missing/weak verifier JWT secret (token-forgery P0)

The bug

NewAuthService (internal/service/auth.go) and NewPortalAuthService (internal/portal/auth.go) both stored []byte(secret) with zero validation, and jwt_secret is omitempty in the config. So an unset (or trivially short) secret meant HS256 tokens were signed AND validated with a zero-length key — letting anyone forge a token for any public_key and bypass vault-ownership auth on every protected route.

The agent-backend already guards this (JWT_SECRET required:"true" + a >= 32 length check in its config Validate()); the verifier and portal did not — an asymmetry surfaced during a stack security audit.

The fix

  • NewAuthService now returns (*AuthService, error) and fails if the secret is empty or < 32 chars (mirroring the backend floor); server.go logrus.Fatalfs on that error.
  • NewPortalAuthService gets the same >= 32 floor (fatal internally, since its NewServer is a struct-literal constructor with no error return — a forgeable auth service must never boot).
  • Dev/CI configs that used short secrets (verifier.example.json mysecret, the CI workflow test-secret, Makefile devsecret/test-portal-secret, the portal test fixture) bumped to clearly-non-production >= 32-char values; SERVER_JWT_SECRET added to both docker-compose.yaml verifier services.

Verification (codex-reviewed)

A codex review of the first commit caught (and this PR fixes) two things that would otherwise have shipped broken: the identical gap in the portal auth, and the dev/CI startup breakage from the new validation rejecting the old short secrets.

receipts

This is a backend security fix — no UI. Receipt = the new rejection test + build/lint:

$ go test ./internal/service/ -run TestNewAuthService_RejectsWeakSecret
# asserts: empty + "too-short" + 31-char secrets are REJECTED; a 32-char secret is accepted.
  • TestNewAuthService_RejectsWeakSecret (new) pins the validation.
  • Existing auth suite updated via a newAuthServiceForTest helper that pads short test secrets to the new minimum while preserving the right-vs-wrong-secret distinctness in the ValidateToken tests.
  • gofmt/parse clean on all changed Go files; YAML + JSON configs validated.

Note: the local toolchain is Go 1.26 but this repo targets go 1.25 (a vendored bytedance/sonic@v1.14.2 is incompatible with Go 1.26's runtime internals — undefined: GoMapIterator), so the package test suite can't be run locally; CI (Go 1.25) exercises it. The changed Go files compile and gofmt-validate in isolation.

Out of scope (tracked separately)

The auth.enabled=false production-disable gap (internal/api/middleware.go:42) and the cosmos-signing verifier bypass are separate findings from the same audit, not addressed here.

🤖 Generated with Claude Code

NeOMakinG and others added 2 commits June 11, 2026 02:18
NewAuthService stored []byte(secret) with ZERO validation, and server.jwt_secret
is omitempty in config — so an unset secret meant HS256 tokens were signed AND
validated with an empty key, letting anyone forge a token for any public_key.
Validate the secret (>= 32 chars, mirroring the agent-backend JWT_SECRET floor)
and fail server startup if it's missing/weak, instead of silently shipping a
forgeable verifier. Adds a rejection test + a test helper so the existing suite's
short test secrets are padded to the new minimum without losing their identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dev/CI secrets

Codex review of the first commit caught two things:
1. internal/portal/auth.go NewPortalAuthService had the IDENTICAL forgery gap
   (stored []byte(secret) with no validation). Apply the same >=32-char floor;
   fatal at startup (the portal NewServer is a struct-literal ctor with no error
   return, so internal fatal is the minimal correct fix).
2. The new validation would BREAK dev/CI startup — verifier.example.json
   ('mysecret'), CI workflow ('test-secret'), Makefile ('devsecret'/
   'test-portal-secret'), and the portal test fixture all used <32-char secrets.
   Bumped each to a clearly-non-production >=32-char value, and added
   SERVER_JWT_SECRET to both docker-compose verifier services (env overrides the
   mounted config via AutomaticEnv).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3405c54f-fd7c-4c2c-ae62-debe74faad7d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/verifier-jwt-secret-validation

Comment @coderabbitai help to get the list of available commands and usage tips.

@NeOMakinG

Copy link
Copy Markdown
Contributor Author

Closing — out of scope for our agent work. The finding (verifier JWT secret unvalidated → token-forgery if jwt_secret is unset) is real and documented in the spike notes for the verifier team to pick up, but it's not part of the agent stack we're driving.

@NeOMakinG NeOMakinG closed this Jun 11, 2026
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