Skip to content

fix(gateway): replace placeholder token handling with JWT resource server - #142

Draft
seonghobae wants to merge 15 commits into
developfrom
fix/gateway-jwt-resource-server
Draft

fix(gateway): replace placeholder token handling with JWT resource server#142
seonghobae wants to merge 15 commits into
developfrom
fix/gateway-jwt-resource-server

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Buyer-visible security gap

The gateway previously exposed a class named JwtAuthenticationFilter whose validator accepted only the literal example value valid_token. That was not a defensible production identity boundary. This draft replaces the placeholder with Spring Security's maintained reactive OAuth 2.0 Resource Server JWT path while preserving a standalone fail-closed mode that requires no invented issuer, key, client secret, or CI credential.

This is an independent, path-disjoint security slice from protected develop; it does not deepen the durable-job stack or modify the CDC acknowledgement PR.

Exact current identity

  • base branch: develop;
  • exact live base tip: 622e5e6c3d534f230c390f10e3832efadfc01825;
  • branch: fix/gateway-jwt-resource-server;
  • exact current head: c6efe5abde600e9147e050d6bc52659b1493973d;
  • current synthetic merge revision: 6265d30070553cff4014b75ae76b2e947d628be5;
  • GitHub mergeability: true;
  • Draft: true.

Old checks, reviews, approvals, and synthetic-merge evidence do not transfer to later heads.

RCA: placeholder authentication boundary

Immediate cause: the historical hand-written filter treated one hard-coded bearer string as valid and did not establish cryptographic JWT verification.

Technical root cause: the WebFlux gateway lacked an explicit reactive OAuth 2.0 Resource Server JWT SecurityWebFilterChain binding /etl/** and /cdc/** to supported Spring Security verification infrastructure.

Systemic cause: earlier tests exercised the placeholder class directly instead of proving the registered WebFlux security chain's runtime behavior.

The implemented remediation uses Spring Security's reactive resource-server/Jose stack, removes the direct JJWT placeholder dependency and literal token acceptance, leaves only actuator health/info intentionally public, protects workload routes in jwt mode, rejects unknown security modes at startup, and keeps deny as the secure standalone default. Deployment-owned issuer/JWK/audience/algorithm configuration remains external configuration rather than a newly invented secret contract. docs/security/gateway-jwt.md records the control, rollback, failure behavior, and APA 7 primary references.

Follow-on RCA: deny mode returned an inactive Basic challenge

After the JWT boundary implementation, exact head 9dba8a63c41e7dd090c7dcb775ef67b863e7a335 still had one source-local CI failure. CI run 31281592066, including macOS job 93163731340, reached GatewaySecurityConfigurationRuntimeTest and failed only denyModeExposesOnlyHealthAndInfoRoutes: /etl/test returned 401 Unauthorized with WWW-Authenticate: Basic instead of the intended 403 Forbidden service-disable response.

The immediate symptom was the status mismatch. The first hypothesis that Basic authentication had simply not been disabled was falsified by exact source inspection: httpBasic, form login, logout, and CSRF were already explicitly disabled. The technical root cause was that deny mode had no explicit authentication entry point, so exception translation could still select a default challenge in the isolated WebFlux test context. That produced a misleading Basic challenge even though deny mode is not an authentication mode.

Remediation options and feasibility

  • execute_now — configure deny mode with Spring Security's reactive HttpStatusServerEntryPoint(HttpStatus.FORBIDDEN). Supported by the current reactive API, requires no credential or new permission, changes only deny-mode failure translation, and is directly covered by the already-failing runtime test.
  • reject — change the test to accept 401/Basic. This would codify an inactive authentication mechanism instead of the documented service-disable contract and would not address the root cause.
  • reject — add an identity-provider URL/key merely to eliminate the challenge. Deny mode deliberately exists so standalone startup does not invent deployment trust material.

Commit acf2b12023a6a39fd9357779dbac641db5471b49 applies the smallest safe production fix: an explicit forbidden authentication entry point only in deny mode. No existing test was weakened. Commit c6efe5abde600e9147e050d6bc52659b1493973d records the resulting gateway security behavior in CHANGELOG.md.

Test-first and aggregate workflow evidence

The original source-security RED remains auditable at 3e1590853606650337b9be88527cf68fd3bdc1e5, which added the boundary contract before production implementation.

For the follow-on runtime defect, GatewaySecurityConfigurationRuntimeTest was already RED on exact head 9dba8a63c41e7dd090c7dcb775ef67b863e7a335. After the minimal deny-mode entry-point repair, exact head acf2b12023a6a39fd9357779dbac641db5471b49 ran the same hosted matrix and the gateway runtime tests passed on macOS, Ubuntu, and Windows. Final source head c6efe5abde600e9147e050d6bc52659b1493973d then caused the complete pull-request workflow set to run.

Current aggregate results associated with that source head are green:

  • CI 31282034979: aggregate success;
  • Dependency Review 31282034962: success;
  • CycloneDX SBOM 31282034977: success;
  • SAST Semgrep 31282034968: success;
  • Security Scan 31282034956: aggregate success;
  • CodeRabbit commit status on source head: success;
  • unresolved review threads: zero;
  • formal reviews: zero;
  • GitHub mergeability: true.

The conditional self-hosted test job is skipped and is not counted as positive evidence. Aggregate success is not automatically literal-source evidence.

Exact-source evidence RCA

Fresh job-log inspection proves that the current protected workflow definitions predate #121's literal-head source controls.

CI source identity

CI macOS job 93164820122 invoked actions/checkout without an explicit source-head ref. GitHub fetched 6265d30070553cff4014b75ae76b2e947d628be5 as refs/remotes/pull/142/merge, checked out that synthetic merge, and then ran the Maven reactor. Therefore CI 31282034979 is useful merge-preview evidence but is not accepted as literal-head source proof for c6efe5ab.... The Ubuntu and Windows aggregate success does not cure that source-identity contract.

Security Scan source identity

Security Scan hard Trivy job 93164819936 likewise invoked default checkout, fetched and checked out synthetic merge 6265d30070553cff4014b75ae76b2e947d628be5, and then executed trivy fs .. Its zero-finding result therefore describes the synthetic merge tree, not the literal PR source tree. Scorecard job 93164819898 uses the same default synthetic-merge source identity. OSV independently checks explicit base/head revisions, and dependency review ran successfully, but those controls do not substitute for the hard Trivy filesystem gate's literal-source requirement.

Accordingly, green CI and Security Scan aggregates are retained as compatibility/security-preview evidence only. Literal-head CI and hard scanner acceptance remain non-passing until protected workflow controls explicitly bind execution to the unchanged PR head.

Remediation feasibility

  • read_only_dependency — integrate the central literal-head scanner repair and ci: schedule NVIDIA OpenCode maintenance agent #121's repository-local exact-source CI/SBOM controls through their own writer leases, then rerun this unchanged source head under those protected definitions. This addresses the source-identity root cause.
  • reject — treat the synthetic merge SHA as equivalent to the source head. That weakens the explicit exact-source acceptance contract and can hide source/base identity drift.
  • reject — copy central scanner controls into this feature branch merely to manufacture passing evidence. The scanner control plane is owned by the separately leased ContextualWisdomLab/.github loop, and the repository-local CI controls belong to the earlier ci: schedule NVIDIA OpenCode maintenance agent #121 integration boundary.
  • reject — manually reinterpret aggregate green as exact-head proof. A green conclusion cannot change the revision that was actually checked out and scanned.

Approval state

There is no qualifying independent non-author formal APPROVED review on exact current source head c6efe5abde600e9147e050d6bc52659b1493973d. No reviewer, team, App identity, credential, or approval is invented to satisfy that governance requirement. Approval is not the sole remaining merge gate because literal-source CI and hard Security Scan evidence are independently unavailable.

The current primary autonomous OpenCode formal-review route is also a read-only central dependency: automated dispatch receipts for mightyETL report that the repository is absent from OPENCODE_REPOSITORY_DISPATCH_TARGETS, while Noema requires a qualifying current-head primary OpenCode approval before its independent verdict can qualify. Do not self-approve or bypass that sequence.

Merge boundary

Keep this PR Draft. Do not merge until the unchanged source head has accepted literal-head CI and hard security evidence after the protected control-plane integrations, every required repository/governance gate passes, zero unresolved valid review finding remains, and a qualifying independent non-author formal APPROVED review is anchored to that exact head. Queued, pending, absent, cancelled, skipped-required, failed, predecessor-head, stale-base, status-only, author-only, or synthetic-merge-only evidence is not passing.

@coderabbitai

coderabbitai Bot commented Aug 8, 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 Plus

Run ID: 102dabe9-924f-4470-90bb-7c96faa5ee13

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Collaborator Author

Fresh exact-head RCA for current 18108d61a0e2a98508d56814c7ea1f58b07a613c (base develop@622e5e6c3d534f230c390f10e3832efadfc01825): CI run 31321076262 fails on all three hosted OS jobs while Dependency Review 31321076246, SBOM 31321076237, SAST 31321076265, and Security Scan 31321076225 are green. macOS job 93264042990 reaches the real gateway tests and fails only GatewayJwtSecuritySourceContractTest.publishesFailClosedDeploymentAndStandardsContract at line 70.

The failing assertion is contract.contains("non-blank sub"). Exact source inspection shows this is not a stale invented requirement: GatewaySecurityConfiguration.stableSubjectAuthenticationConverter() rejects jwt.getSubject() == null || subject.isBlank() with BadJwtException, and the class/Javadoc explicitly requires a non-blank stable sub. The current docs/security/gateway-jwt.md instead says only that a production token "should provide a stable non-sensitive subject identifier" and never records the enforced non-blank sub runtime contract.

RCA: source behavior and source-contract test agree; authoritative operator documentation drifted to weaker wording. Smallest safe remedy is documentation-only: state explicitly that JWT mode requires a non-blank sub and that missing/blank subjects fail authentication before routing, then rerun the unchanged semantic test boundary. Reject weakening/removing the assertion because that would hide a live runtime requirement. This branch moved under another writer during this invocation, so I am leaving source/ref mutation to that active writer rather than racing it. Note also that this hosted run still checks out synthetic merge 6f94c1532791bcd63d1ea97dd5f88b00b7b65bb4, so a subsequent aggregate GREEN remains merge-preview evidence until literal-source controls are integrated.

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