Skip to content

feat(tls-edge): SNI passthrough L4 relay + mTLS edge termination#100

Open
cuioss-oliver wants to merge 12 commits into
mainfrom
feature/tls-edge
Open

feat(tls-edge): SNI passthrough L4 relay + mTLS edge termination#100
cuioss-oliver wants to merge 12 commits into
mainfrom
feature/tls-edge

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the TLS-edge design from doc/plan/06-tls-edge.adoc: an accept-time
SNI split that routes passthrough_sni connections through an opaque L4 TCP
relay while everything else goes through mTLS-terminated handling, plus a
runtime Host-vs-SNI smuggle guard, Toxiproxy-backed TLS/fault integration
tests, three-layer documentation (reference/operator/developer), and an
ADR-0004 wording correction to match the shipped upstream.path REPLACE
semantics.

Compatibility is breaking (pre-1.0 clean-slate, no deprecation).

Changes

  • Accept-time SNI dispatch & L4 relay (api-sheriff):

    • tls/ClientHelloSniParser.java — parses the SNI extension from the raw
      ClientHello to decide the routing path before TLS termination.
    • tls/SniFrontListener.java — accept-time listener that dispatches
      connections based on the parsed SNI.
    • tls/PassthroughRelay.java — opaque L4 TCP relay for passthrough_sni
      connections (no TLS termination at the edge).
    • tls/MtlsServerCustomizer.java — mTLS-terminated handling path for
      non-passthrough connections.
    • tls/TlsEdgeProducer.java, tls/package-info.java — CDI wiring and
      package documentation for the new tls package.
    • quarkus/ConfigProducer.java, edge/GatewayEdgeRoute.java,
      events/EventType.java, ApiSheriffLogMessages.java — supporting
      producer/routing/event/log-message plumbing for the new TLS edge path.
    • pipeline/PassthroughHostGuardStage.java — runtime Host-vs-SNI smuggle
      404 guard (distinct from FramingGate's HTTP-framing control); the
      boot-time collision rule in ConfigValidator was left untouched (owned
      by a concurrently-running plan).
    • src/main/resources/application.properties — TLS-edge configuration
      defaults.
    • Matching unit tests for every new/changed production class
      (ClientHelloSniParserTest, SniFrontListenerTest,
      PassthroughRelayTest, MtlsServerCustomizerTest,
      TlsEdgeProducerTest, PassthroughHostGuardStageTest, EventTypeTest).
  • Toxiproxy-backed integration tests (integration-tests):

    • docker-compose.yml — adds the Toxiproxy service alongside keycloak.
    • TlsPassthroughIT.java, MtlsHandshakeIT.java, PassthroughFaultIT.java,
      HostSmuggleGuardIT.java, GrpcProxyIT.java,
      integration/grpc/GrpcEchoServiceTest.java — new IT suites exercising
      the passthrough relay, mTLS termination, fault injection via Toxiproxy,
      the Host-smuggle guard, and gRPC-over-passthrough.
  • Benchmarks (benchmarks):

    • PassthroughBaselineComparator.java (+ test),
      K6BenchmarkLogMessages.java, k6-scripts/passthrough_relay.js,
      pom.xml, README.adoc — baseline comparator and k6 script for the new
      passthrough relay path.
  • Documentation (three-layer, per constraint):

    • Reference: doc/configuration.adoc — new, distinct TLS-keys section
      (does not touch the PLAN-06 reserved-path list at :521-527).
    • Operator: doc/user/tls-edge.adoc (+ doc/user/README.adoc index).
    • Developer: doc/development/tls-edge.adoc (+ doc/development/README.adoc
      index).
    • doc/README.adoc — append-only top-level index update.
    • doc/architecture.adoc, doc/LogMessages.adoc — updated for the new
      package/log-message surface.
    • doc/adr/0004-topology-indirection.adoc — wording correction: replaces
      the concatenation phrasing with the actually-shipped upstream.path
      REPLACE semantics.

Test Plan

  • Verification command passed (python3 .plan/execute-script.py plan-marshall:build-maven:maven run --command-args "verify -Ppre-commit")
  • Full verify passed (python3 .plan/execute-script.py plan-marshall:build-maven:maven run --command-args "verify")
  • Manual testing completed (if applicable)

Generated by plan-finalize skill

Summary by CodeRabbit

  • New Features

    • Added SNI-based TLS passthrough, relaying selected connections without terminating TLS.
    • Added terminated-connection mTLS support with configurable client certificate authorities.
    • Added fail-closed handling for malformed TLS handshakes and Host/SNI smuggling attempts.
    • Added structured runtime logging for TLS routing, relay startup, and security rejections.
  • Documentation

    • Added configuration and operations guidance for TLS passthrough, mTLS, trust-anchor rotation, and security behavior.
    • Expanded architecture and development documentation for the TLS edge.
  • Tests

    • Added unit and integration coverage for passthrough routing, mTLS handshakes, fault handling, and smuggle protection.

cuioss-oliver and others added 11 commits July 23, 2026 11:22
…y (D1)

Add the Vert.x NetServer front listener that reassembles the full TLS ClientHello (RFC 6066), reads the SNI, and either opaquely L4-relays a passthrough_sni match to the topology-resolved backend (the gateway never handshakes) or hands the still-encrypted stream to the internal terminated Quarkus HTTPS listener. Fail-closed per GW-06: empty/unresolved/malformed SNI takes the terminated-strict path. Exposes the resolved topology as a CDI bean, adds relay-lifecycle INFO and fail-closed WARN LogRecords, and reserves the public TLS port. Unit tests cover ClientHello reassembly/SNI extraction, front-listener routing, and L4 relay byte-fidelity/backpressure/half-close/abort.
Add MtlsServerCustomizer, a Quarkus HttpServerOptionsCustomizer that maps tls.mtls.enabled + client_ca onto the terminated HTTPS listener's client-auth: require-and-verify a client certificate against the configured CA trust anchor, with no HTTP fallback and no optional/want mode. Client-auth stays off when tls.mtls is absent or disabled; enabled without client_ca fails the boot. Passthrough connections are split off at L4 and never mTLS-checked. Unit tests cover the enabled/disabled/absent/misconfigured mapping.
Add PassthroughHostGuardStage, a pipeline pre-check that rejects a
terminated request whose Host header names a passthrough_sni hostname
with 404 (PASSTHROUGH_HOST_SMUGGLED) before route selection, closing the
host-confusion vector against the passthrough backend identity. Matching
mirrors the front listener's case-insensitive, dot/port-normalized SNI
handling; the stage is inert when passthrough_sni is empty.

Cover the guard with a negative-test suite and extend the EventType
catalogue test with the new PASSTHROUGH_HOST_SMUGGLED member.

Co-Authored-By: Claude <noreply@anthropic.com>
…es (D4)

Add a pinned Toxiproxy service and a TLS-enabled passthrough backend (serving its
own CN=passthrough-backend certificate) to the integration-tests compose stack as
purely additive services — existing services are untouched.

Add four black-box integration suites exercising the TLS edge against the running
stack under -Pintegration-tests:
- TlsPassthroughIT: the passthrough relay surfaces the backend's certificate
  identity (opaque relay proof); terminated traffic on the same listener still served.
- MtlsHandshakeIT: mTLS accept/reject proven at the handshake level (trusted cert,
  no cert, wrong-CA cert).
- HostSmuggleGuardIT: a terminated Host naming a passthrough SNI is rejected 404.
- PassthroughFaultIT: a mid-stream Toxiproxy reset propagates as a connection abort,
  not a hang, driven via the Toxiproxy REST control API.

Co-Authored-By: Claude <noreply@anthropic.com>
Reference layer: add a TLS-active status section to doc/configuration.adoc
documenting the now-enforced tls.passthrough_sni / tls.mtls behavior (accept-time
SNI split, exact case-insensitive matching, no-base-path alias rule, the two guards,
mTLS on terminated connections only, no wildcards) without touching the PLAN-06
reserved gateway-paths block.

Operator layer: doc/user/tls-edge.adoc covers configuring passthrough SNI and mTLS
plus the emergency trust-root-replacement blast radius (swapping client_ca
invalidates the entire old trust root at once; no per-cert revocation, no CRL/OCSP).

Developer layer: doc/development/tls-edge.adoc covers the Vert.x NetServer front,
the accept-time L4/L7 split, the internal-handoff loopback relay and port ownership
(8443 public / 8444 internal), the runtime Host-vs-SNI smuggle guard, and the
fail-closed GW-06 rationale with the Traefik CVE-2026-32305 / CVE-2025-66491 and
KrakenD CVE-2026-39821 references.

Record the TLS edge in doc/architecture.adoc's component model and append-only index
lines in doc/README.adoc, doc/user/README.adoc, and doc/development/README.adoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pq3eQyg78SuTKrC8pLpLs
The Decision "Decompose-on-read" bullet described the forward URI as
resolved(base_url) + route.upstream.path + <remainder>, implying concatenation of
three terms. The shipped semantics (RouteTableBuilder.applyRouteUpstreamPath and
DispatchStage.upstreamRequestUri) are REPLACE: a declared route.upstream.path
replaces the alias-derived base path, and the forward URI is
stripTrailingSlash(effective base path) + <request-path remainder after the matched
prefix>. Doc-only; the decompose-on-read decision itself is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pq3eQyg78SuTKrC8pLpLs
…gression comparator

Add the k6 passthrough_relay.js aspect driving the opaque L4 TCP relay path
(mapped SNI -> L4 relay -> TLS backend), plus an empty-passthrough_sni mode
re-measuring the same proxied static route for a no-regression check against
the PLAN-04 plain-proxy baseline. Add PassthroughBaselineComparator, a
noise-band comparator asserting the empty-mode run does not regress beyond a
percentile-band tolerance (n/a, never 0, for an unmeasured metric), with a
JUnit 5 unit test using CUI Test Generator. Register the two -Pbenchmark k6
executions and the post-integration-test comparator, and document the new
aspect and its two modes in the README aspect matrix and methodology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pq3eQyg78SuTKrC8pLpLs
The whole-tree pre-commit quality gate applied mechanical OpenRewrite
normalizations across the plan's touched TLS-edge and benchmark sources:
fully-qualified annotation/type references simplified to imports
(@java.io.Serial, @org.jspecify.annotations.Nullable, StandardCharsets,
Consumer), wildcard static-import collapse, import-group blank-line
separators, and array-initializer whitespace. No semantic changes; the
quality gate passes green with these applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pq3eQyg78SuTKrC8pLpLs
…dge classes

Resolves Q-Gate finding 765287 (jacoco-branch-min). The api-sheriff JaCoCo
bundle branch ratio was 998/1262 = 0.7908, below the 0.80 minimum. Add
branch-covering JUnit 5 tests (AAA, no Mockito) for the plan-touched TLS-edge
classes, lifting the bundle to 1020/1262 = 0.8082:

- ClientHelloSniParserTest: extension-walk / name-entry edge cases
  (seek past a leading non-SNI extension, non-host_name entry skip, truncated
  and overlong server_name list, name length past list end) and the Result
  null-normalization branch.
- PassthroughHostGuardStageTest: normalize branches for non-numeric, empty,
  and multi-colon :port suffixes.
- SniFrontListenerTest: normalizeSni trailing-dot / case / whitespace path.
- TlsEdgeProducerTest (new): boot wiring — front listener start/stop with a
  resolved passthrough SNI, empty-passthrough short-circuit, and the
  unresolved-alias defensive skip (ADR-0009).

api-sheriff jacoco check: "All coverage checks have been met."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pq3eQyg78SuTKrC8pLpLs
Consolidate the byte-identical TrustAllManager X509TrustManager defined in
MtlsHandshakeIT, PassthroughFaultIT, and TlsPassthroughIT into a single
package-visible class in MtlsHandshakeIT, imported by the other two, and drop
the now-unused imports. Test-only, package-private; no API or behavior change.

Co-Authored-By: Claude <noreply@anthropic.com>
The pre-commit OpenRewrite recipe normalizes import ordering in two grpc
integration-test files touched incidentally by the whole-tree gate. Committing
the gate-mandated formatting so the working tree is OpenRewrite-clean. No
behavioral change.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cuioss-oliver, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cuioss-oliver, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c10f358a-fe97-4c5d-9ea1-d67392d7fa32

📥 Commits

Reviewing files that changed from the base of the PR and between 2385691 and 4ee9dba.

📒 Files selected for processing (2)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParser.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParserTest.java
📝 Walkthrough

Walkthrough

Adds an accept-time TLS SNI edge with opaque passthrough relaying, terminated mTLS support, Host-vs-SNI smuggle protection, integration coverage, and a new passthrough benchmark workflow with baseline regression comparison.

Changes

TLS edge routing and security

Layer / File(s) Summary
TLS edge contracts and request guard
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/{events,pipeline,edge}/..., api-sheriff/src/main/java/de/cuioss/sheriff/gateway/{ApiSheriffLogMessages,quarkus/ConfigProducer}.java
Adds passthrough events and logs, exposes resolved topology, and rejects matching terminated Host values before route selection.
ClientHello parsing and TCP relay
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/{ClientHelloSniParser,PassthroughRelay}.java, api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/{ClientHelloSniParserTest,PassthroughRelayTest}.java
Reassembles and parses ClientHello SNI fail-closed, then relays buffered and live TCP data with backpressure, half-close, and abort handling.
SNI front listener and boot wiring
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/{SniFrontListener,TlsEdgeProducer,MtlsServerCustomizer}.java, api-sheriff/src/main/resources/application.properties
Routes mapped SNI to passthrough targets, routes other connections to internal HTTPS, starts and stops the front listener, and maps mTLS configuration to HTTPS server options.
TLS behavior validation and deployment support
api-sheriff/src/test/java/de/cuioss/sheriff/gateway/{pipeline,events}/..., api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/{SniFrontListener,TlsEdgeProducer,MtlsServerCustomizer}Test.java, integration-tests/...
Covers guard semantics, listener startup and routing, mTLS handshakes, passthrough certificates, relay resets, and supporting Docker services.
TLS edge documentation
doc/architecture.adoc, doc/configuration.adoc, doc/development/tls-edge.adoc, doc/user/tls-edge.adoc, doc/{README,LogMessages}.adoc, doc/*/README.adoc, doc/adr/0004-topology-indirection.adoc
Documents TLS edge routing, configuration constraints, fail-closed behavior, operator mTLS procedures, and updated log and topology references.

Passthrough benchmark workflow

Layer / File(s) Summary
Passthrough benchmark execution
benchmarks/src/main/resources/k6-scripts/passthrough_relay.js, benchmarks/pom.xml, benchmarks/README.adoc
Adds mapped and empty k6 modes, Maven executions, and benchmark methodology describing the passthrough and no-regression runs.
Baseline comparator and verification
benchmarks/src/main/java/de/cuioss/sheriff/gateway/k6/benchmark/..., benchmarks/src/test/java/de/cuioss/sheriff/gateway/k6/benchmark/...
Compares throughput and latency metrics against a tolerance band, renders markdown results, reports regressions, and tests missing metrics and tolerance validation.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main TLS-edge change set: SNI passthrough relay plus mTLS termination.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/package-info.java (1)

33-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the Jspecify compile dependency.

api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/package-info.java imports org.jspecify.annotations.NullMarked, and this module uses @NullMarked in many package-info classes. Add org.jspecify:jspecify as a compile dependency if NullMarked is intended to be available here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/package-info.java`
around lines 33 - 37, Add org.jspecify:jspecify as a compile dependency for the
module containing the TLS package-info.java files, ensuring
org.jspecify.annotations.NullMarked resolves consistently across all
package-info classes.
🧹 Nitpick comments (3)
integration-tests/docker-compose.yml (2)

56-101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

api-sheriff's depends_on isn't updated for the new passthrough-backend/toxiproxy services.

TlsPassthroughIT and PassthroughFaultIT require passthrough-backend (and, for the latter, toxiproxy) to be reachable through the gateway's relay target, but the api-sheriff service's depends_on block still only lists keycloak, go-httpbin, asset-origin, and grpc-echo. Both new services define healthchecks, so gating on service_healthy here would reduce the chance of cold-start flakiness in the IT suite.

♻️ Suggested depends_on addition (outside this range — in the api-sheriff service block)
    depends_on:
      keycloak:
        condition: service_started
      go-httpbin:
        condition: service_started
      asset-origin:
        condition: service_started
      grpc-echo:
        condition: service_healthy
      passthrough-backend:
        condition: service_healthy
      toxiproxy:
        condition: service_healthy
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-tests/docker-compose.yml` around lines 56 - 101, Update the
api-sheriff service’s depends_on block to include passthrough-backend and
toxiproxy, using service_healthy conditions for both alongside the existing
dependencies. Preserve all current dependency entries and conditions.

56-101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Runtime apk add/cert-gen diverges from the repo's pre-provisioned cert convention.

The comment says this "mirror[s] the self-signed convention the certificates/ scripts use," but keycloak/api-sheriff actually mount pre-generated certs (./src/main/docker/certificates), whereas this service installs openssl and generates a cert on every container start. That adds a network dependency (package registry reachability) and startup latency to every test run/restart, and reintroduces the risk of the set -e script failing before nginx even execs.

Consider generating the passthrough-backend cert once (e.g. via a build-time Dockerfile step or a script under certificates/) and mounting it read-only, consistent with the existing convention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-tests/docker-compose.yml` around lines 56 - 101, The
passthrough-backend service currently installs openssl and generates TLS
material at runtime, unlike the repository’s pre-provisioned certificate
convention. Remove the runtime apk add and openssl generation from its
entrypoint, add or reuse pre-generated certificate and key assets under the
existing certificates convention, and mount them read-only at the paths
referenced by the nginx configuration while preserving the passthrough-backend
service and healthcheck behavior.
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizer.java (1)

73-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Route mTLS-enabled logging through the catalog, at INFO not DEBUG.

This is a security-relevant boot decision — comparable to SNI_FRONT_LISTENER_STARTED and PASSTHROUGH_RELAY_ESTABLISHED, which are both added as structured INFO LogRecords in this same PR. Here it's an ad-hoc LOGGER.debug(...) string, invisible at the default INFO level and outside the doc/LogMessages.adoc catalog.

♻️ Proposed fix — add a catalog entry and log at INFO
-        LOGGER.debug("mTLS enabled: terminated HTTPS listener requires a client certificate signed by '%s'",
-                clientCa);
+        LOGGER.info(ApiSheriffLogMessages.INFO.MTLS_ENABLED, clientCa);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizer.java`
around lines 73 - 85, Update customizeHttpsServer to replace the ad-hoc
LOGGER.debug mTLS message with the catalog’s structured INFO LogRecord
mechanism. Add the corresponding mTLS-enabled message to the documented log
catalog, preserving clientCa as the logged value and matching the existing
patterns used by SNI_FRONT_LISTENER_STARTED and PASSTHROUGH_RELAY_ESTABLISHED.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParserTest.java`:
- Around line 169-178: Update ClientHelloFixture.withCorruptExtensionLength() to
modify the actual server_name extension-length field directly at its known
offset, rather than scanning for a 0x00 0x00 byte pattern. Ensure the fixture
makes that extension length exceed the remaining buffer while leaving session
and cipher-suite lengths intact, so malformedExtensionLength exercises the
extension-length overrun validation path.

In `@benchmarks/pom.xml`:
- Around line 616-632: Move the compare-passthrough-baseline execution for
PassthroughBaselineComparator after stop-integration-containers in the
post-integration-test execution order, while preserving its existing
configuration. Ensure dump-keycloak-logs and container shutdown run before the
comparator can fail the build.

In `@benchmarks/README.adoc`:
- Around line 212-217: Update the “Passthrough relay, two modes
(API-Sheriff-only)” description to state that only passthrough uses a
TLS-terminating backend with its own certificate. Retain the comparison to ws
and grpc only for including real protocol-appropriate backend cost, and remove
the claim that those aspects share a TLS-enabled backend.

In `@doc/configuration.adoc`:
- Around line 768-781: The operator-facing TLS documentation must describe
runtime normalization consistently: in doc/configuration.adoc lines 768-781,
update the active TLS summary near the passthrough and Host-vs-SNI rules; in
doc/user/tls-edge.adoc lines 29-40, update the passthrough operating rules.
State that runtime removes one trailing dot from SNI and strips one :port suffix
from Host before passthrough/smuggle decisions, while preserving the existing
exact-match and other TLS constraints.

---

Outside diff comments:
In `@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/package-info.java`:
- Around line 33-37: Add org.jspecify:jspecify as a compile dependency for the
module containing the TLS package-info.java files, ensuring
org.jspecify.annotations.NullMarked resolves consistently across all
package-info classes.

---

Nitpick comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizer.java`:
- Around line 73-85: Update customizeHttpsServer to replace the ad-hoc
LOGGER.debug mTLS message with the catalog’s structured INFO LogRecord
mechanism. Add the corresponding mTLS-enabled message to the documented log
catalog, preserving clientCa as the logged value and matching the existing
patterns used by SNI_FRONT_LISTENER_STARTED and PASSTHROUGH_RELAY_ESTABLISHED.

In `@integration-tests/docker-compose.yml`:
- Around line 56-101: Update the api-sheriff service’s depends_on block to
include passthrough-backend and toxiproxy, using service_healthy conditions for
both alongside the existing dependencies. Preserve all current dependency
entries and conditions.
- Around line 56-101: The passthrough-backend service currently installs openssl
and generates TLS material at runtime, unlike the repository’s pre-provisioned
certificate convention. Remove the runtime apk add and openssl generation from
its entrypoint, add or reuse pre-generated certificate and key assets under the
existing certificates convention, and mount them read-only at the paths
referenced by the nginx configuration while preserving the passthrough-backend
service and healthcheck behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 87eea754-9255-4678-b467-49deb75868ab

📥 Commits

Reviewing files that changed from the base of the PR and between 3f60d49 and 2385691.

📒 Files selected for processing (41)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/ApiSheriffLogMessages.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/events/EventType.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/pipeline/PassthroughHostGuardStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParser.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/PassthroughRelay.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/SniFrontListener.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsEdgeProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/package-info.java
  • api-sheriff/src/main/resources/application.properties
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/events/EventTypeTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/pipeline/PassthroughHostGuardStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParserTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/PassthroughRelayTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/SniFrontListenerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/TlsEdgeProducerTest.java
  • benchmarks/README.adoc
  • benchmarks/pom.xml
  • benchmarks/src/main/java/de/cuioss/sheriff/gateway/k6/benchmark/K6BenchmarkLogMessages.java
  • benchmarks/src/main/java/de/cuioss/sheriff/gateway/k6/benchmark/PassthroughBaselineComparator.java
  • benchmarks/src/main/resources/k6-scripts/passthrough_relay.js
  • benchmarks/src/test/java/de/cuioss/sheriff/gateway/k6/benchmark/PassthroughBaselineComparatorTest.java
  • doc/LogMessages.adoc
  • doc/README.adoc
  • doc/adr/0004-topology-indirection.adoc
  • doc/architecture.adoc
  • doc/configuration.adoc
  • doc/development/README.adoc
  • doc/development/tls-edge.adoc
  • doc/user/README.adoc
  • doc/user/tls-edge.adoc
  • integration-tests/docker-compose.yml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/GrpcProxyIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/HostSmuggleGuardIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/MtlsHandshakeIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/PassthroughFaultIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/TlsPassthroughIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/grpc/GrpcEchoServiceTest.java

Comment on lines +169 to +178
@Test
@DisplayName("fails closed on an extension length that runs past the buffer")
void malformedExtensionLength() {
byte[] hello = ClientHelloFixture.withCorruptExtensionLength();

ClientHelloSniParser.Result result = parser.parse(hello);

assertTrue(result.complete());
assertTrue(result.serverName().isEmpty(), "a structural inconsistency fails closed");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test fixture corrupts the wrong field — doesn't test what it claims.

withCorruptExtensionLength()'s scan starts at absolute offset 5+4+34=43, which is the session_id_length byte (0x00) immediately followed by the cipher_suites_length high byte (also 0x00) — a false "0x00 0x00" match that breaks the loop before it ever reaches the real server_name extension-length field (offset 45-46). It ends up inflating cipher_suites_length/corrupting the cipher-suite byte instead, so malformedExtensionLength (Line 169) passes via a different fail-closed path (a skip() past buffer bounds) rather than exercising the SNI extension-length overrun check it's named for.

🐛 Proposed fix — build the corrupt extension directly instead of scanning for a byte pattern
-        static byte[] withCorruptExtensionLength() {
-            byte[] hello = withSni("corrupt.example.com");
-            // The extension length is the two bytes immediately after the server_name extension type
-            // (0x00 0x00). Locate the first 0x00 0x00 pair inside the extensions area and inflate the
-            // following length so it overruns the buffer.
-            for (int i = 5 + 4 + 34; i < hello.length - 4; i++) {
-                if (hello[i] == 0x00 && hello[i + 1] == 0x00) {
-                    hello[i + 2] = (byte) 0x7F;
-                    hello[i + 3] = (byte) 0xFF;
-                    break;
-                }
-            }
-            return hello;
-        }
+        static byte[] withCorruptExtensionLength() {
+            // Declare a server_name extension whose length field overruns the extensions block,
+            // exercising the extensionsEnd bound check directly instead of pattern-scanning bytes.
+            byte[] sni = serverNameExtension("corrupt.example.com");
+            sni[2] = 0x7F;
+            sni[3] = (byte) 0xFF;
+            return withRawExtensions(sni);
+        }

Also applies to: 334-347

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ClientHelloSniParserTest.java`
around lines 169 - 178, Update ClientHelloFixture.withCorruptExtensionLength()
to modify the actual server_name extension-length field directly at its known
offset, rather than scanning for a 0x00 0x00 byte pattern. Ensure the fixture
makes that extension length exceed the remaining buffer while leaving session
and cipher-suite lengths intact, so malformedExtensionLength exercises the
extension-length overrun validation path.

Comment thread benchmarks/pom.xml
Comment on lines +616 to +632
<!-- Assert the empty-passthrough_sni run did not regress beyond the noise
band against the PLAN-04 plain-proxy baseline. Additive, beside
process-k6-results; reads the two k6 summaries produced in this lane. -->
<execution>
<id>compare-passthrough-baseline</id>
<phase>post-integration-test</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>de.cuioss.sheriff.gateway.k6.benchmark.PassthroughBaselineComparator</mainClass>
<arguments>
<argument>${k6.output.dir}</argument>
</arguments>
</configuration>
</execution>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Comparator failure will skip container cleanup — orphaned containers on every detected regression.

compare-passthrough-baseline is declared in post-integration-test before dump-keycloak-logs (Line 634) and stop-integration-containers (Line 650), all bound to the same plugin/phase. Maven runs same-phase executions of one plugin in declaration order, and PassthroughBaselineComparator.main() deliberately throws IllegalStateException when result.regressed() — that's the whole point of the gate. When that happens, the build aborts right there and the two cleanup executions after it never run, leaving the benchmark Docker containers running (and Keycloak logs undumped) on every real regression — exactly the failure mode CI is most likely to hit.

Move this execution after stop-integration-containers (it only reads local files under ${k6.output.dir}, so cleanup order doesn't affect it) so containers are always torn down regardless of the comparator's verdict.

♻️ Suggested reordering
-                            <!-- Assert the empty-passthrough_sni run did not regress beyond the noise
-                                 band against the PLAN-04 plain-proxy baseline. Additive, beside
-                                 process-k6-results; reads the two k6 summaries produced in this lane. -->
-                            <execution>
-                                <id>compare-passthrough-baseline</id>
-                                <phase>post-integration-test</phase>
-                                <goals>
-                                    <goal>java</goal>
-                                </goals>
-                                <configuration>
-                                    <mainClass>de.cuioss.sheriff.gateway.k6.benchmark.PassthroughBaselineComparator</mainClass>
-                                    <arguments>
-                                        <argument>${k6.output.dir}</argument>
-                                    </arguments>
-                                </configuration>
-                            </execution>
-
                             <!-- Dump Keycloak container logs before stopping containers -->
                             <execution>
                                 <id>dump-keycloak-logs</id>
                                 ...
                             </execution>
                             <!-- Stop integration test containers -->
                             <execution>
                                 <id>stop-integration-containers</id>
                                 ...
                             </execution>
+
+                            <!-- Assert the empty-passthrough_sni run did not regress beyond the noise
+                                 band against the PLAN-04 plain-proxy baseline. Placed after container
+                                 teardown so a detected regression never blocks cleanup. -->
+                            <execution>
+                                <id>compare-passthrough-baseline</id>
+                                <phase>post-integration-test</phase>
+                                <goals>
+                                    <goal>java</goal>
+                                </goals>
+                                <configuration>
+                                    <mainClass>de.cuioss.sheriff.gateway.k6.benchmark.PassthroughBaselineComparator</mainClass>
+                                    <arguments>
+                                        <argument>${k6.output.dir}</argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/pom.xml` around lines 616 - 632, Move the
compare-passthrough-baseline execution for PassthroughBaselineComparator after
stop-integration-containers in the post-integration-test execution order, while
preserving its existing configuration. Ensure dump-keycloak-logs and container
shutdown run before the comparator can fail the build.

Comment thread benchmarks/README.adoc
Comment on lines +212 to +217
Passthrough relay, two modes (API-Sheriff-only)::
The `passthrough` aspect drives the opaque L4 TCP relay and — unlike the six pure-overhead HTTP
aspects, and exactly like `ws` and `grpc` — rides a *TLS-enabled backend* that completes the
handshake and presents its own certificate. It therefore folds a real backend's cost into the
number and is a *protocol-relay measurement*, not a backend-free gateway-overhead figure: read it as
relay overhead versus the terminated baseline within one run, never as an absolute throughput claim.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Passthrough/ws-grpc analogy is inaccurate — mixes two different comparison axes.

The claim that passthrough "exactly like ws and grpc — rides a TLS-enabled backend that completes the handshake and presents its own certificate" conflates two distinct properties. Only passthrough's backend actually terminates its own TLS (the entire point of the opaque L4 relay). The doc's own "Protocol-appropriate backends" entry and the fairness-caveat NOTE describe ws/grpc backends only as "protocol-appropriate" (WebSocket echo, gRPC), never as TLS-terminating — for those aspects the gateway still owns TLS termination. The valid analogy to ws/grpc is "folds a real backend's cost into the number" (not backend-free), not "rides a TLS-enabled backend."

✏️ Suggested rewording
-The `passthrough` aspect drives the opaque L4 TCP relay and — unlike the six pure-overhead HTTP
-aspects, and exactly like `ws` and `grpc` — rides a *TLS-enabled backend* that completes the
-handshake and presents its own certificate. It therefore folds a real backend's cost into the
-number and is a *protocol-relay measurement*, not a backend-free gateway-overhead figure: read it as
-relay overhead versus the terminated baseline within one run, never as an absolute throughput claim.
+The `passthrough` aspect drives the opaque L4 TCP relay against a *TLS-enabled backend* that
+completes the handshake and presents its own certificate — unique among the matrix, since every
+other aspect's TLS is terminated at the gateway. Like `ws` and `grpc`, it is not backend-free: it
+folds a real backend's cost into the number and is a *protocol-relay measurement*, not a
+backend-free gateway-overhead figure: read it as relay overhead versus the terminated baseline
+within one run, never as an absolute throughput claim.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Passthrough relay, two modes (API-Sheriff-only)::
The `passthrough` aspect drives the opaque L4 TCP relay and — unlike the six pure-overhead HTTP
aspects, and exactly like `ws` and `grpc` — rides a *TLS-enabled backend* that completes the
handshake and presents its own certificate. It therefore folds a real backend's cost into the
number and is a *protocol-relay measurement*, not a backend-free gateway-overhead figure: read it as
relay overhead versus the terminated baseline within one run, never as an absolute throughput claim.
The `passthrough` aspect drives the opaque L4 TCP relay against a *TLS-enabled backend* that
completes the handshake and presents its own certificate — unique among the matrix, since every
other aspect's TLS is terminated at the gateway. Like `ws` and `grpc`, it is not backend-free: it
folds a real backend's cost into the number and is a *protocol-relay measurement*, not a
backend-free gateway-overhead figure: read it as relay overhead versus the terminated baseline
within one run, never as an absolute throughput claim.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/README.adoc` around lines 212 - 217, Update the “Passthrough
relay, two modes (API-Sheriff-only)” description to state that only passthrough
uses a TLS-terminating backend with its own certificate. Retain the comparison
to ws and grpc only for including real protocol-appropriate backend cost, and
remove the claim that those aspects share a TLS-enabled backend.

Comment thread doc/configuration.adoc
Comment on lines +768 to +781
[[_tls_active]]
*Status -- active.* The `tls.passthrough_sni` and `tls.mtls` blocks are *enforced at runtime*, not
merely parsed: an accept-time front listener reads the SNI from the reassembled ClientHello and
splits each connection at L4 (passthrough) or L7 (terminated); terminated connections apply
`mtls` client-certificate verification; and the runtime Host-vs-SNI smuggle guard rejects a
terminated request whose `Host` names a passthrough SNI with `404` before route selection. Three
constraints an operator must keep in mind, all stated above and restated here because they are the
common configuration mistakes: SNI matching is *exact* -- *no wildcard* entries; a passthrough
alias URL carries *no base path*; and `mtls` applies to *terminated connections only* (the gateway
never participates in a passthrough handshake). Operating these keys -- including the emergency
trust-root-replacement blast radius -- is covered in the link:user/tls-edge.adoc[operator TLS-edge
topic]; the front-listener architecture and the fail-closed (GW-06) rationale are covered in the
link:development/tls-edge.adoc[developer TLS-edge topic].

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align the operator-facing TLS contract with runtime normalization.

The runtime removes one trailing FQDN dot from SNI and strips one :port suffix from Host before the passthrough/smuggle decision. Document those rules consistently in both operator-facing locations:

  • doc/configuration.adoc#L768-L781: include the normalization behavior in the active TLS summary.
  • doc/user/tls-edge.adoc#L29-L40: include the same behavior in the passthrough operating rules.
📍 Affects 2 files
  • doc/configuration.adoc#L768-L781 (this comment)
  • doc/user/tls-edge.adoc#L29-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/configuration.adoc` around lines 768 - 781, The operator-facing TLS
documentation must describe runtime normalization consistently: in
doc/configuration.adoc lines 768-781, update the active TLS summary near the
passthrough and Host-vs-SNI rules; in doc/user/tls-edge.adoc lines 29-40, update
the passthrough operating rules. State that runtime removes one trailing dot
from SNI and strips one :port suffix from Host before passthrough/smuggle
decisions, while preserving the existing exact-match and other TLS constraints.

…(S2789)

Sonar new_reliability_rating flagged java:S2789 at ClientHelloSniParser.Result:
the canonical constructor null-normalized the serverName Optional, but every
construction site (needMoreData, parsed, and extractServerName's returns) already
supplies a non-null Optional, so the guard was dead defensive code that an
Optional must never carry. Remove the compact constructor and the test that
exercised the now-absent null branch. No behavioral change; coverage stays green.

Co-Authored-By: Claude <noreply@anthropic.com>
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