Skip to content

feat(tls): single-source and activate neutral TLS + management surface - #138

Open
cuioss-oliver wants to merge 13 commits into
mainfrom
feature/plan-31b-neutral-tls-config-surface
Open

feat(tls): single-source and activate neutral TLS + management surface#138
cuioss-oliver wants to merge 13 commits into
mainfrom
feature/plan-31b-neutral-tls-config-surface

Conversation

@cuioss-oliver

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

Copy link
Copy Markdown
Collaborator

Summary

The gateway's declared TLS floor and cipher-suite allowlist were inert in production — the
neutral gateway.yaml tls keys had zero production consumers, so neither side of the apparent
"double source" was actually in force. This PR makes the neutral surface the single effective
source: gateway.yaml's TLS policy is now bound onto the terminated listener through the
HttpServerOptionsCustomizer seam (TlsServerCustomizer, sibling of the existing
MtlsServerCustomizer), and the raw quarkus.* duplicates that were never doing anything are
deleted outright. The management interface gets a first-class neutral management: policy block
projected onto quarkus.management.* by a single-ordinal (>300) SmallRye ConfigSource
(NeutralTlsConfigSource) — the seam the management path actually enumerates, since
VertxHttpRecorder.initializeManagementInterface never consults HttpServerOptionsCustomizer
beans. The plain-HTTP management opt-out is a loud WARN, never a boot refusal, and is proven on a
dedicated sixth compose gateway instance (api-sheriff-plain-mgmt). ADR-0025 extends ADR-0011's
neutral-name ruling from the JWKS trust surface to the whole server-TLS surface, with a three-way
policy / deployment-bound / build-time classification.

Changes

  • D1 — Bind the neutral gateway.yaml TLS block to the terminated listener, delete the inert raw keys, relocate the build-time knob

    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.java (new): maps tls.min_version, tls.cipher_suites, tls.alpn onto the real HttpServerOptions for the terminated main listener; every mapping is a no-op when its key is absent.
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/TlsConfig.java: adds the cipherSuites component.
    • api-sheriff/src/main/resources/schema/gateway.schema.json: adds tls.cipher_suites.
    • api-sheriff/src/main/resources/application.properties: deletes quarkus.tls.protocols, quarkus.tls.cipher-suites, quarkus.tls.alpn, quarkus.http.ssl.client-auth (all proven inert or redundant against the shipped quarkus-vertx-http-3.37.4 bytecode); relocates quarkus.ssl.native to api-sheriff/pom.xml's native profile (it is a build-time GraalVM knob); retains quarkus.http.ssl-port and quarkus.http.insecure-requests as deliberately deployment-bound.
    • Tests: TlsServerCustomizerTest (new), MtlsServerCustomizerTest, ConfigModelContractTest, ConfigLoaderTest updated for the new arity/coverage; config/valid/gateway.yaml and the test application.properties fixture reconciled so tests and production take the same branch through HttpServerOptionsUtils.getTlsConfiguration's registry-default guard.
  • D2 — Neutral management policy block and the projecting ConfigSource

    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/ManagementConfig.java (new): neutral management: block, TLS policy only — no port component (see Scope Deviation Accepted below).
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSource.java (new) + META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider (new): single-ordinal (>300) SmallRye ConfigSource parsing gateway.yaml's tls:/management: blocks directly (it runs before CDI).
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java: adds the management component.
    • api-sheriff/src/main/resources/schema/gateway.schema.json: adds the management object; port is declared solely to be refused ("not": {} plus a sibling errorMessage), landing the actionable rejection at the exact /management/port JSON Pointer.
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java: enables the validator's errorMessage extension.
    • Tests: ConfigLoaderTest gains the R7 gate (rejectsManagementPortNamingTheDeploymentKnobAtItsOwnPointer plus its matched positive control); NeutralTlsConfigSourceTest (new); ConfigModelContractTest, SheriffMetricsTest updated for the new arity.
  • D3 — Audited plain-HTTP management opt-out, proven on a sixth compose instance

    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/ConfigLogMessages.java: adds WARN.MANAGEMENT_PLAIN_HTTP (ApiSheriff-115).
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAudit.java (new): @ApplicationScoped StartupEvent observer emitting the WARN on the observed effective state, eagerly activated and actively invoking its collaborator to defeat the lazy-CDI-proxy trap (lesson 2026-07-20-18-002).
    • api-sheriff/src/main/resources/application.properties: declares the key-less named TLS bucket the opt-out selects, unconditionally (not %it-scoped).
    • integration-tests/docker-compose.yml: adds the sixth gateway service api-sheriff-plain-mgmt (publishing 19005:9000, setting QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME, omitting the management cert env pair); updates the "all five instances" lockstep comments to describe the deliberate exception.
    • integration-tests/scripts/start-integration-container.sh: adds a dedicated http:// readiness gate block (not folded into the existing https://-hard-coded loop).
    • Tests: ManagementPlainHttpAuditTest (new, real startup-event path), ManagementPlainHttpOptOutIT (new), ManagementPlainHttpActivationWiringTest (new, no-Docker descriptor-parsing test with the five HTTPS instances as the matched negative control).
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.java (commit 50abc37, post-outline finalize finding): tls.cipher_suites now validates every declared suite against SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites() and aborts boot on an unsupported/mistyped entry, symmetric with the existing min_version/alpn fail-closed checks.
  • D4 — ADR-0025

    • doc/adr/0025-The_whole_server-TLS_surface_is_neutral_in_gatewayyaml_and_bound_by_exactly_two_seams.adoc (new): extends ADR-0011's neutral-name ruling to the whole server-TLS surface; records the policy / deployment-bound / build-time three-way classification, both binding seams (HttpServerOptionsCustomizer for the terminated listener, the projecting ConfigSource for management), and the rejected ManagementConfig.tlsConfigurationName()-as-fallback candidate.
  • D5 — Documentation and the machine-asserted completion bar

    • doc/configuration.adoc, doc/architecture.adoc, doc/LogMessages.adoc (adds ApiSheriff-115 and the previously-undocumented ApiSheriff-106), doc/user/tls-edge.adoc, doc/user/README.adoc, doc/user/environment-variable-overrides.adoc (new): the three-layer documentation of the neutral TLS surface, the management opt-out, and the secure-default-vs-override-route audit of the config surface.
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/SingleSourceTlsContractTest.java (new): parses the shipped application.properties and mechanically asserts no raw server-TLS quarkus.* policy key remains, with an explicit allowlist for the two named exceptions, plus the quarkus.ssl.native absent-here/present-in-pom.xml-native-profile pair.
    • integration-tests/src/main/docker/sheriff-config/gateway.yaml: the primary IT instance now declares min_version and cipher_suites so the stack exercises a gateway.yaml-sourced protocol/cipher setting end-to-end.

Scope Deviation Accepted

Two scope deviations from the original outline, both recorded in this plan's decision.log under
the (scope-deviation:accept) marker and reflected in the code as shipped:

  1. D2/D3 reshaped after research. The outline originally called for BOTH a schema
    hard-rejection of management.port AND a ConfigValidator rule with an actionable message.
    ConfigLoader aborts on schema failure before binding ever runs, so the validator rule would
    have been unreachable dead code. Research against the quarkusio/quarkus 3.37 branch
    established that (a) quarkus.management.tls-configuration-name pointed at a key-less named
    TLS bucket is a genuine, runtime-overridable, native-safe off-switch for management TLS with a
    legible ConfigurationException on misuse, and (b) the actionable rejection message belongs in
    the JSON schema at its exact JSON Pointer via the networknt errorMessage extension.
    Resolution: management HTTPS stays the default on deployment-bound port 9000; the
    originally-specified bespoke gateway.yaml acknowledgement key was dropped in favour of the
    Quarkus-native QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME; WARN 115 is retained but triggers
    on observed effective state at startup, not on a declared key.
  2. The neutral management block declares no port. Its only route into Quarkus would be the
    same single-ordinal (>300) config source, where it would silently outrank a deployment-set
    QUARKUS_MANAGEMENT_PORT. ConfigValidator/schema instead refuses a management.port key with
    a message naming the real knob (quarkus.management.port / QUARKUS_MANAGEMENT_PORT).

Two Corrections to the Original Spec, Established by Evidence

  • The spec framed the problem as a "double source" where gateway.yaml and quarkus.* both
    configured the same concepts. In fact the neutral tls: keys had zero production consumers
    — parsed, schema-validated, then never applied. The declared TLS 1.2 floor and cipher allowlist
    were not in force in production. This was a live security defect, not a
    configuration-tidiness problem. It hid because the test fixture supplied its certificate through
    quarkus.tls.key-store.pem.* (selecting the TLS-registry listener path), while production
    supplies it through quarkus.http.ssl.certificate.* — so tests and production took opposite
    branches through HttpServerOptionsUtils.getTlsConfiguration's registry-default guard.
  • The spec named HttpServerOptionsCustomizer as the binding seam for the management interface.
    Verified against quarkus-vertx-http-3.37.4 bytecode: VertxHttpRecorder enumerates those beans
    only on the main-server path, never for the management interface. ADR-0025 records the correct
    seam (the projecting ConfigSource).

Verification

  • verify -Ppre-commit and verify -Pcoverage are green.
  • The full unit suite is green.
  • The containerised Docker IT suite was run against a freshly-built native image: 89 completed, 0
    failures, 0 errors, 0 skipped. It was run twice more after later commits, green each time.
  • The first containerised run found a real defect that every other gate missed: the new
    api-sheriff-plain-mgmt instance crash-looped with "Port 8443 seems to be in use" because it
    mounts the shared sheriff-config (which declares passthrough_sni, starting the SNI front
    listener on 8443) without the QUARKUS_HTTP_SSL_PORT=8444 move. Fixed in commit bc3d622.
  • The finalize security audit found that tls.cipher_suites had no fail-closed validation. A probe
    against a real Vert.x HTTPS listener established that a bogus suite name lets server.listen()
    succeed — the gateway boots green and then resets every TLS handshake. Fixed in commit 50abc37
    with symmetric boot-failure coverage for all three TLS knobs.

Known-Open Findings (out of this plan's footprint, not fixed here)

Both are recorded as findings and documented in doc/user/environment-variable-overrides.adoc
rather than fixed, because both are outside this plan's server-TLS-configuration footprint:

  • 4b0102QUARKUS_HTTP_SSL_PROTOCOLS on the api-sheriff-mtls IT instance works only
    while that overlay's gateway.yaml declares no tls.min_version; adding one there would
    silently shadow the environment pin.
  • df7bd1 — what the token-sheriff extension's readiness check reports in a deployment that
    configures gateway.yaml issuers but no sheriff.token.issuers.* was not established by reading
    and is flagged as explicitly inconclusive.

Related Issues

None.


Generated by plan-finalize skill

Summary by CodeRabbit

  • New Features

    • Added configurable TLS policies for the public listener, including minimum versions, cipher suites, ALPN, and mTLS.
    • Management interfaces now use HTTPS by default, with an explicit plain-HTTP opt-out.
    • Added warnings when management security is downgraded to HTTP.
    • Added validation for unsupported TLS settings and rejected management-port configuration.
  • Documentation

    • Added TLS edge, environment override, architecture, and configuration guidance.
    • Documented deployment ownership of ports and certificate material.
  • Bug Fixes

    • Improved operator-facing configuration validation messages and sanitized WebSocket security logging.

cuioss-oliver and others added 13 commits July 31, 2026 00:51
…tener

Add TlsServerCustomizer mapping tls.min_version, tls.cipher_suites and tls.alpn from gateway.yaml onto the terminated HTTPS listener, making gateway.yaml the single effective source of terminated-listener TLS policy. Delete the inert raw quarkus.tls.protocols/.cipher-suites/.alpn keys, which never reached the listener because the TLS registry is consulted only when the certificate comes from quarkus.tls.key-store.*, while this gateway supplies it via quarkus.http.ssl.certificate.*. Relocate the build-time quarkus.ssl.native knob to the pom native profile. Add the cipherSuites component to TlsConfig and align the test profile onto quarkus.http.ssl.certificate.* so tests exercise the production listener path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
…gement.port in-schema

gateway.yaml gains a first-class neutral `management:` policy block, bound through
a new single-ordinal ConfigSource seam, and the deliberate absence of a port key is
made discoverable instead of silent.

- ManagementConfig: the neutral management policy record (tls.enabled), declaring no
  port component. The management port stays deployment-bound at quarkus.management.port
  (default 9000, QUARKUS_MANAGEMENT_PORT).
- NeutralTlsConfigSource + its ConfigSourceProvider registration: the seam that reaches
  the management listener, which never enumerates HttpServerOptionsCustomizer beans. Its
  ordinal sits above the environment source's 300 so a policy named in gateway.yaml is
  not silently overridden; that is safe only because it projects policy keys only — never
  a port, never trust material, and never key material into the default TLS registry
  bucket, which would make management inherit HTTPS invisibly through
  registry.getDefault() (upstream quarkus issue 43380).
- gateway.schema.json: the management object declares `port` solely in order to refuse it,
  via an always-failing subschema plus the networknt errorMessage extension, so the
  actionable sentence naming quarkus.management.port / QUARKUS_MANAGEMENT_PORT and ADR-0025
  lands at the /management/port pointer rather than surfacing as a generic unknown-key
  error against the parent object. ConfigLoader enables the extension, which is off unless
  the keyword is named on the registry configuration.

No ConfigValidator rule is added: loadGateway returns on the first schema failure and never
reaches validation, so a validator rule for a schema-rejected key would be unreachable dead
code. The errorMessage adoption is backed by an executed test asserting the exact sentence,
written before the mechanism was relied on.
…a sixth instance

Running the management interface on plain HTTP is a legitimate deployment behind a
trusted boundary, so it stays possible — but it stops being silent.

The downgrade route is Quarkus-native rather than bespoke: quarkus.management.tls-
configuration-name selects the `plain-management` TLS bucket that application.properties
now declares with no key material, the recorder finds no key/cert, and the management
listener starts plain. It is runtime configuration, so the same native image serves both
postures with no rebuild — which is why the new compose instance needs neither a variant
image nor a config overlay, only an environment variable.

- ManagementPlainHttpAudit observes the real StartupEvent and warns (ApiSheriff-115) on
  the OBSERVED EFFECTIVE STATE — the TLS material the listener actually resolved, the same
  question the recorder asks. An audit keyed on a configuration key would report a
  comfortable fiction as soon as that key was renamed or superseded.
- The observer actively invokes the audit rather than holding a collaborator, and the
  accompanying test fires the event through the container's own bus. A normal-scoped
  observer whose proxy is never touched silently never runs while a directly-invoking test
  stays green (lesson 2026-07-20-18-002).
- api-sheriff-plain-mgmt publishes management on 19005 with its own http:// readiness gate,
  deliberately outside the https-hard-coded loop that gates 19000-19004. The lockstep
  comments on the other five instances now describe the exception instead of quietly
  becoming false.
- ManagementPlainHttpActivationWiringTest asserts the activation is really wired, with the
  five HTTPS instances as the matched negative control, and pins the two constraints that
  make the mechanism work: the selected bucket carries no key material, and no DEFAULT
  quarkus.tls.key-store.* bucket exists — the management interface falls back to the default
  registry bucket and would otherwise inherit HTTPS through a path no config file mentions.

Also repairs the testboot fixture, which declared no token_validation and so could not
complete a container boot at all — TokenValidatorProducer forces the validator eagerly
regardless of the route table's auth requirements.
…g seams

ADR-0025 restates ADR-0011's neutral-name ruling as a general rule and extends it from the JWKS trust surface to the whole server-TLS surface: every knob is policy (neutral in gateway.yaml), deployment-bound (ports and material, supplied by the deployment), or build-time (never in the runtime surface at all). ADR-0011 stays Accepted and is extended, not superseded.

The classification is not filing. It is what makes the single-ordinal projecting ConfigSource safe: because the source projects policy keys only, no deployment-bound knob is in the projected set, so nothing an operator sets in the environment can be outranked by surprise — and the second, lower-ordinal source that would otherwise be needed is unnecessary rather than merely forbidden.

Also recorded: why the two seams are each the only one available on their path (the customizer is enumerated after createSslOptions on the main path; the management interface never enumerates customizers); why the neutral management block declares no port; why management HTTPS stays the default with a runtime-native, audited opt-out rather than a bespoke key or a boot refusal; and the reversal on quarkus.management.tls-configuration-name — both original objections are plausible readings of its name and both fail against the mechanism, so the reversal is recorded to stop a future reader re-deriving them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
…onsumer

The neutral key was bound but never read: NeutralTlsConfigSource.project() returned an empty map for every document, so gateway.yaml's management.tls.enabled reached nothing. A neutral name that binds nothing is the exact defect this plan exists to eliminate — it reads as enforcement while enforcing nothing — so the seam now projects.

management.tls.enabled=false projects exactly one key, quarkus.management.tls-configuration-name=plain-management, selecting the key-less bucket application.properties already declares. Absent and true project NOTHING, deliberately: the secure default needs no key to express it, and the silence is what keeps QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME a working route onto the same Quarkus key. The neutral door and the deployment door are two routes onto one lever, not two mechanisms with a precedence puzzle between them.

Every projection assertion goes through the assembled SmallRyeConfig rather than the source's own accessor, because the question Quarkus asks is what the resolved Config returns after ordinal arbitration — and each no-projection case runs against a competing value at the environment source's ordinal, so 'the deployment value survives' is an observation rather than the absence of one. The projected key set's cardinality is pinned so a second key must arrive as a visible diff.

The top-level tls block is no longer parsed here. It governs the terminated listener, which has a live CDI seam able to observe the SSL options Quarkus actually built; reading it in the config source was a parse with no key to emit for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
…WARN

ApiSheriff-115 observes the effective state and cannot tell which door produced it, so a remedy naming only the deployment environment variable is now incomplete: gateway.yaml's management.tls.enabled=false reaches the same Quarkus key. An operator who took the neutral door would be told to remove an override they never set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
The configuration reference gains cipher_suites, the now-effective min_version/alpn semantics with their terminated-listener scope limit, a management field reference explaining why the block declares no port and why a downgrade takes the whole surface, and a per-key verdict table recording the disposition of every raw quarkus.* server-TLS key with its class and reason — including both named exceptions, since an undocumented exception would be a silent trim.

The architecture document records the two binding seams and, for each, why it is the only one available on its path: the customizer is enumerated after the SSL options are built, and the management recorder never enumerates customizers at all. It also records the ordering constraint that follows — the projecting ConfigSource is constructed before CDI exists, so it cannot read the bound model and re-reads gateway.yaml itself, which is why a malformed document degrades silently there and an unexpected shape reads as 'not explicitly disabled'.

The operator guide gains the TLS policy keys and the management interface: HTTPS by default, the single-port constraint that makes any downgrade total, the two doors onto the opt-out and why they do not compete, why the escape hatch is runtime-native rather than bespoke, and an explicit warning that quarkus.management.enabled=false is the opposite of a fix — build-time fixed, and it moves probes onto the TLS-terminated router.

New operator page environment-variable-overrides.adoc states the three-way classification in operator language and enumerates what stays deployment-bound. LogMessages registers ApiSheriff-115 and the previously-undocumented ApiSheriff-106, and repairs three rows that had drifted from their templates — including a phantom INFO-5 row for an event the code emits as WARN-106.

The primary IT fixture now declares min_version and a cipher allowlist so the integration stack exercises a gateway.yaml-sourced protocol and cipher setting. The floor is 1.2 and the allowlist spans both enabled protocols: under a 1.2 floor a 1.3-only allowlist would leave TLS 1.2 with nothing to negotiate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
The keys this plan deleted were inert, not merely redundant, and nothing structural stops that defect growing back: a contributor reaching for a familiar quarkus.tls.* name would reintroduce a surface that reads as enforcement while reaching no listener, and every existing test would stay green. SingleSourceTlsContractTest turns that reintroduction into a failing build by classifying every default-profile key in the shipped application.properties and refusing any server-TLS POLICY key outside a named allowlist.

The R1 relocation is asserted in BOTH halves — quarkus.ssl.native absent from application.properties AND present, enabled, inside the pom's native profile. The absence assertion alone would pass just as happily if the knob had been dropped outright, and a native image without SSL support fails at runtime rather than at build.

One assertion guards the guard: every allowlist entry must be present in the file AND reachable by the detector. Without it the allowlist could quietly fill with decorative entries — a stale key that no longer exists, or one the namespace matcher never reaches — leaving the bar weaker than it reads. That check is what caught quarkus.http.ssl-port falling outside a dot-terminated quarkus.http.ssl. prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
…oute pairs

The management-TLS pair is one instance of a general shape, so the operator page now answers the same four questions for the rest of the surface: default value, security posture of that default, override route, and whether the route is deployment-bound by necessity (a port, material, a build-time knob) or merely by accident (a policy knob that never got a neutral name).

Swept: terminated-listener TLS policy, mTLS, the SNI split and its ports, JWKS trust and egress, token validation and its eager boot assembly, the inbound filter modes, request limits and admission budgets, and response headers/CORS. Two answers are worth naming. The transport framing bounds have NO override route at all, deliberately — an operator hunting for the variable should be told it does not exist rather than left searching. And the inbound filter mode has no environment route on purpose: a validation posture relaxable by an environment variable would be a weakening with no trace in the document under review.

Two areas are recorded as findings rather than fixed, since both sit outside this plan's footprint. The interaction between the gateway's own validator and the extension's parallel sheriff.token.issuers.* readiness surface could not be established by reading, so the page says so explicitly instead of asserting a verdict it did not earn. And QUARKUS_HTTP_SSL_PROTOCOLS on the mtls IT instance works today only because that overlay declares no tls.min_version — adding one would silently shadow the env pin, which is the precedence rule behaving correctly and surprisingly at the same time.

No inert key was found in the shipped properties after this change, and the page says that is a statement about this revision rather than a general guarantee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
api-sheriff-plain-mgmt could never boot. It is the only gateway instance that
mounts the shared sheriff-config rather than overlaying its own gateway.yaml --
deliberately, so the management plain-HTTP opt-out is the single variable under
test. But that shared gateway.yaml declares a non-empty passthrough_sni, which
starts the SNI front listener on the public port 8443, and the instance was
never given QUARKUS_HTTP_SSL_PORT=8444 to move the terminated HTTPS listener to
the internal port. The two collided and the container crash-looped with "Port
8443 seems to be in use by another process".

The four other gateway instances escape this only because each overlays
gateway.yaml with a variant declaring no passthrough_sni, so the invariant --
documented at the passthrough_sni block in sheriff-config/gateway.yaml -- had
never bitten before.

Add the port move, and delete the "no passthrough front on this instance"
comment, which asserted the opposite of what the mounted config does.

Found by running the containerised suite: a green test-compile showed nothing.
After the fix the suite is 89 completed, 0 failures, 0 errors, 0 skipped, 0
flakes, with ManagementPlainHttpOptOutIT 3/3 and WARN ApiSheriff-115 observed
firing in the native container through the real StartupEvent path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
…uards

NeutralTlsConfigSource carried an unresolved OpenRewrite marker before a
deliberately broad `catch (Exception e)` guarding the YAML pre-parse of
gateway.yaml. CLAUDE.md forbids committing code with a marker present.

The catch itself is required, not speculative -- it guards a real
external-input parse boundary -- so the fix is to place the suppression
comment in its correct position ahead of `catch`, matching the convention
already used at JwksTrustProfileResolver.java:115, and drop the stray TODO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
TlsServerCustomizer forwarded every declared cipher suite straight to
HttpServerOptions with no validation. A probe against a real Vert.x HTTPS
listener showed the bind SUCCEEDS with a bogus suite name -- the gateway boots
green and then resets every TLS handshake with SSLHandshakeException. A
partially mistyped list is worse: the valid entries still negotiate, silently
narrowing the declared cipher policy to an unaudited subset.

Validate each declared suite against the JDK's supported set and abort the boot
with a message naming the offending suite and its closest supported matches,
mirroring the existing applyMinVersion/applyAlpn fail-closed shape. The
supported set is resolved per boot, not in a static initializer, so a native
image cannot bake in the build host's cipher set.

Adds symmetric boot-failure coverage for all three TLS knobs plus a guard test
pinning the integration fixture's four-suite allowlist.

Found by the finalize security-audit sweep (finding 1421ff).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo
Tier-1 re-enrichment scoped to api-sheriff, the only module this plan added
components to. Records TlsServerCustomizer and ManagementPlainHttpAudit under
gateway.tls, NeutralTlsConfigSource under gateway.config, and ManagementConfig
under gateway.config.model, plus the ADR-0025 three-way key classification
(POLICY / DEPLOYMENT-BOUND / BUILD-TIME) as a durable hint.

Tier-0 found no structural change: the new components landed inside existing
modules, so the discovered descriptor is byte-unchanged and is not recommitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFEs53xjkecC5nX6XRdEMo

@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, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The gateway now defines TLS policy in gateway.yaml, applies it to the terminated HTTPS listener, and projects management TLS policy through MicroProfile Config. Management HTTP opt-out auditing, schema validation, native SSL configuration, documentation, and integration coverage were added.

Changes

TLS and management configuration

Layer / File(s) Summary
Configuration contract and validation
api-sheriff/src/main/java/..., api-sheriff/src/main/resources/..., api-sheriff/src/test/java/...
Adds management TLS and cipher-suite model fields, schema validation, deployment-bound port rejection, and native-profile SSL configuration.
Terminated-listener TLS binding
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.java, api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/*
Applies TLS version, cipher-suite, and ALPN settings with fail-closed validation.
Management TLS projection and audit
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSource.java, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAudit.java, api-sheriff/src/test/java/...
Projects the plain-management configuration and audits the effective management HTTP or HTTPS state at startup.
Plain-management deployment coverage
integration-tests/*
Adds a dedicated plain-management Compose service, readiness polling, wiring checks, HTTP health checks, HTTPS rejection checks, and warning-log assertions.
TLS architecture and operating documentation
doc/*, .plan/project-architecture/api-sheriff/enriched.json
Documents TLS ownership, precedence, validation, management operation, environment overrides, and build-time settings.

WebSocket log catalogue

Layer / File(s) Summary
WebSocket log message updates
doc/LogMessages.adoc
Removes upstream detail, replaces raw WebSocket origins with bounded dispositions, and moves the idle-timeout message to warning level.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 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 PR's main TLS configuration and management-surface changes.

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.

@cuioss-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Potential Unhandled Exception

Set.of(E... elements) throws an IllegalArgumentException if the input array contains duplicate elements. While default JSSE providers typically return unique cipher suite names in SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites(), certain JDK distributions, custom security providers, or security provider aliases can return duplicate entries in the array. Passing the array directly to Set.of(...) will throw an unhandled IllegalArgumentException during startup instead of executing normally or throwing the intended IllegalStateException. Consider using Arrays.stream(SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites()).collect(Collectors.toSet()) to safely handle potential duplicate cipher suite names.

private static Set<String> supportedCipherSuites() {
    try {
        return Set.of(SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites());
    } catch (NoSuchAlgorithmException e) {
        throw new IllegalStateException(
                "Refusing to start — the platform default SSLContext is unavailable, so the declared "
                        + "tls.cipher_suites allowlist cannot be validated",
                e);
    }

@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: 8

🧹 Nitpick comments (2)
api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizerTest.java (1)

107-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test duplicates enabledRequiresClientAuthWithTrustAnchor and cannot observe the property it names.

The act and the two assertions at lines 118-123 are identical to enabledRequiresClientAuthWithTrustAnchor (lines 48-61). The only addition is the pre-assertion at lines 114-115. That pre-assertion reads a bare Vert.x HttpServerOptions, which never carries a Quarkus property in a plain unit test. So it asserts the Vert.x library default, not that quarkus.http.ssl.client-auth was deleted.

The stated behaviour is "no raw quarkus.http.ssl.client-auth default is needed". The mechanism that would prove it is the content of api-sheriff/src/main/resources/application.properties and api-sheriff/src/test/resources/application.properties. Neither is reachable from here.

Assert the absence where it is observable. A contract test that scans those two property files for quarkus.http.ssl.client-auth proves the single-source claim; this test does not. The PR already has SingleSourceTlsContractTest, which looks like the correct home.

As per path instructions: "Report a stated behaviour whose mechanism is absent, and name the file that would have to contain it. Prose, a code comment, or a summary table is not a mechanism."

Source: Path instructions

api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSource.java (1)

88-94: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive the plain-management bucket reference from a single definition. PLAIN_MANAGEMENT_BUCKET, quarkus.tls.plain-management.reload-period, QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME=plain-management, and the integration test string are each written independently. The opt-out contract only needs one change to name a missing bucket if they drift; source them from the Java constant or the application.properties bucket key instead.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: d230750c-030a-4dad-8c54-c9aa59adfc84

📥 Commits

Reviewing files that changed from the base of the PR and between b903526 and d73807e.

📒 Files selected for processing (37)
  • .plan/project-architecture/api-sheriff/enriched.json
  • api-sheriff/pom.xml
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/ConfigLogMessages.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSource.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/ManagementConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/TlsConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAudit.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.java
  • api-sheriff/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider
  • api-sheriff/src/main/resources/application.properties
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSourceTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/SingleSourceTlsContractTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAuditTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizerTest.java
  • api-sheriff/src/test/resources/application.properties
  • api-sheriff/src/test/resources/config/testboot/gateway.yaml
  • api-sheriff/src/test/resources/config/valid/gateway.yaml
  • doc/LogMessages.adoc
  • doc/adr/0025-The_whole_server-TLS_surface_is_neutral_in_gatewayyaml_and_bound_by_exactly_two_seams.adoc
  • doc/architecture.adoc
  • doc/configuration.adoc
  • doc/user/README.adoc
  • doc/user/environment-variable-overrides.adoc
  • doc/user/tls-edge.adoc
  • integration-tests/docker-compose.yml
  • integration-tests/pom.xml
  • integration-tests/scripts/start-integration-container.sh
  • integration-tests/src/main/docker/sheriff-config/gateway.yaml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ManagementPlainHttpActivationWiringTest.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ManagementPlainHttpOptOutIT.java

Comment on lines +52 to +58
* All three knobs are <em>fail-closed</em>: an unrecognized {@code min_version}, an unrecognized
* {@code alpn} identifier and a {@code cipher_suites} entry the JDK does not support each abort the
* boot with an actionable {@link IllegalStateException} rather than being silently ignored. The
* cipher check matters most, because an unsupported suite name is <em>not</em> rejected anywhere
* downstream: Vert.x binds the listener happily and the defect only surfaces as an
* {@code SSLHandshakeException} on every client connection — a listener that accepts TCP but can
* never complete a handshake, or, for a partly mistyped list, a silently narrowed allowlist.

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

The cipher guard does not cover the protocol/suite mismatch it claims to prevent.

The javadoc states that the cipher check prevents "a listener that accepts TCP but can never complete a handshake". The predicate !supported.contains(suite) only rejects a name the JDK does not know. It cannot fire for the more likely operator error: a suite list that does not intersect the enabled protocol set.

Concrete case: min_version: "1.2" with cipher_suites holding only TLS_AES_* suites. Every name is JDK-supported, so the boot succeeds. TLS 1.3 negotiates, and TLS 1.2 has no suite at all, so every TLS 1.2 client fails the handshake. The comment in integration-tests/src/main/docker/sheriff-config/gateway.yaml at lines 44-45 names this exact outcome, which confirms the gap is known and unguarded.

Choose one of two fixes. Either add a cross-check that each enabled protocol retains at least one declared suite, or narrow the javadoc so it claims only what the predicate enforces.

🛡️ Option A — cross-check each enabled protocol against the allowlist
     private static void applyCipherSuites(TlsConfig config, HttpServerOptions options) {
         List<String> cipherSuites = config.cipherSuites();
         if (cipherSuites.isEmpty()) {
             return;
         }
         Set<String> supported = supportedCipherSuites();
         for (String suite : cipherSuites) {
             if (!supported.contains(suite)) {
                 List<String> closest = closestSupported(suite, supported);
                 throw new IllegalStateException(
                         "Refusing to start — unsupported tls.cipher_suites entry '%s'; this JDK supports no such suite%s"
                                 .formatted(suite,
                                         closest.isEmpty() ? "" : ", closest supported: " + closest));
             }
         }
+        assertEveryEnabledProtocolIsNegotiable(options.getEnabledSecureTransportProtocols(), cipherSuites);
         cipherSuites.forEach(options::addEnabledCipherSuite);
         LOGGER.debug("Terminated HTTPS listener cipher-suite allowlist: %s", cipherSuites);
     }
+
+    /**
+     * Refuses an allowlist that leaves an enabled protocol with no negotiable suite — a listener that
+     * binds but fails every handshake for that protocol version.
+     */
+    private static void assertEveryEnabledProtocolIsNegotiable(Set<String> enabledProtocols,
+            List<String> cipherSuites) {
+        for (String protocol : enabledProtocols) {
+            if (negotiableSuites(protocol).stream().noneMatch(cipherSuites::contains)) {
+                throw new IllegalStateException(
+                        ("Refusing to start — tls.min_version enables %s, but tls.cipher_suites declares no suite "
+                                + "that %s can negotiate; that protocol would fail every handshake")
+                                .formatted(protocol, protocol));
+            }
+        }
+    }

negotiableSuites(String protocol) can be derived from an SSLEngine created with that single protocol enabled, then reading getEnabledCipherSuites().

📝 Option B — narrow the javadoc claim
- * cipher check matters most, because an unsupported suite name is <em>not</em> rejected anywhere
- * downstream: Vert.x binds the listener happily and the defect only surfaces as an
- * {`@code` SSLHandshakeException} on every client connection — a listener that accepts TCP but can
- * never complete a handshake, or, for a partly mistyped list, a silently narrowed allowlist.
+ * cipher check matters most, because an unsupported suite <em>name</em> is not rejected anywhere
+ * downstream: Vert.x binds the listener happily and a typo surfaces only as an
+ * {`@code` SSLHandshakeException}, or, for a partly mistyped list, as a silently narrowed allowlist.
+ * The check validates suite <em>names</em> only. It does not verify that every protocol enabled by
+ * {`@code` tls.min_version} retains a negotiable suite; declaring only TLS 1.3 suites under a
+ * {`@code` 1.2} floor still binds a listener that no TLS 1.2 client can handshake with.

As per path instructions: "Check a guard's predicate against the scenario the guard exists for. Report a condition that cannot fire in that scenario."

Also applies to: 132-149

Source: Path instructions

Comment on lines +56 to +66
/**
* The verbatim {@code tls.cipher_suites} allowlist of
* {@code integration-tests/src/main/docker/sheriff-config/gateway.yaml}: two TLS 1.3 suites plus
* the ECDHE_RSA pair that covers TLS 1.2 against the suite's RSA server certificate. Validation
* that rejected any of these would break the containerised stack, so they are asserted here.
*/
private static final List<String> IT_FIXTURE_SUITES = List.of(
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");

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 | 🟠 Major | ⚡ Quick win

IT_FIXTURE_SUITES mirrors the integration fixture by hand, so it can drift silently.

The javadoc declares this list to be "the verbatim tls.cipher_suites allowlist" of integration-tests/src/main/docker/sheriff-config/gateway.yaml (lines 46-50), and integrationFixtureAllowlistIsAccepted exists to prove validation does not reject what the containerised stack negotiates. The list is a hand-copied duplicate, not derived from that file. If someone edits the fixture, this test keeps asserting the old four suites and passes. The guarantee the javadoc states then no longer holds, and the regression only appears in the container suite.

Read the allowlist from the fixture at test time so the two cannot diverge.

♻️ Proposed fix — derive the list from the fixture
-    private static final List<String> IT_FIXTURE_SUITES = List.of(
-            "TLS_AES_256_GCM_SHA384",
-            "TLS_AES_128_GCM_SHA256",
-            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
-            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
+    private static final Path IT_FIXTURE = Path
+            .of("../integration-tests/src/main/docker/sheriff-config/gateway.yaml");
+
+    /**
+     * Reads {`@code` tls.cipher_suites} out of the integration fixture, so an edit to that document
+     * cannot leave this assertion guarding a stale list.
+     */
+    private static List<String> itFixtureSuites() throws IOException {
+        assertTrue(Files.isRegularFile(IT_FIXTURE),
+                () -> "integration fixture not found at " + IT_FIXTURE.toAbsolutePath());
+        GatewayConfig fixture = /* the project's YAML/ConfigLoader entry point */ loadFixture(IT_FIXTURE);
+        List<String> suites = fixture.tls().orElseThrow().cipherSuites();
+        assertFalse(suites.isEmpty(), "the integration fixture must declare a cipher-suite allowlist");
+        return suites;
+    }

If the cross-module relative path is unwanted, copy the fixture into this module at build time with a Maven resource step and read the copy. Either way, remove the hand-maintained duplicate.

As per path instructions: "Treat a hardcoded list that must mirror a set defined elsewhere ... as a defect unless it is derived from that source at build or run time."

Also applies to: 186-200

Source: Path instructions

Comment on lines +19 to +21
jwks:
source: file
file: ../integration-tests/src/main/docker/certificates/test-jwks.json

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find how a file-sourced JWKS path is resolved.
rg -n -C6 'source.*file|jwks' --glob '*.java' -g '!**/test/**' | rg -n -C6 'Path|Paths|resolve|File'
# Confirm the fixture exists at the referenced location.
fd -t f 'test-jwks.json'

Repository: cuioss/API-Sheriff

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -u

printf 'Tracked files matching gateway.yaml and test-jwks.json:\n'
git ls-files | rg '(^|/)(gateway\.yaml|test-jwks\.json)$' || true

printf '\nTracked files matching JWKS/config-related Java source (non-test):\n'
git ls-files '*.java' | rg -v '/test/' | rg -i 'jwks|jwk|key|config|authoris|auth' || true

printf '\nSearch jwks/source/file in Java files (non-test):\n'
rg -n -C4 -i 'jwks|source|file' --glob '*.java' . | rg -n -C4 -i 'jwks|source|file' || true

printf '\nInspect referenced gateway file if present:\n'
for f in $(git ls-files | rg '(^|/)gateway\.yaml$'); do
  if rg -n 'jwks' "$f" >/dev/null; then
    echo "--- $f"
    cat -n "$f" | sed '/jwks/,+12p' | sed -n '1,60p'
  fi
done

Repository: cuioss/API-Sheriff

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -u

printf 'Locate test resources and relevant config files:\n'
git ls-files | rg 'api-sheriff/(src/test/resources/config/testboot/|pom.xml|.*gateway.*|.*test.*|.*properties).*' | sed -n '1,200p'

printf '\nGateway YAML files matching jwks:\n'
git ls-files | rg 'gateway\.yaml$|\.ya?ml$' | xargs -r rg -n -C3 'jwks:' || true

printf '\nFiles with sheriff.config.dir:\n'
git ls-files | xargs -r rg -n -C4 'sheriff\.config\.dir|target/test-classes/config/testboot' || true

Repository: cuioss/API-Sheriff

Length of output: 40565


🏁 Script executed:

#!/bin/bash
set -u

printf 'JwksTrustProfileResolver outline:\n'
ast-grep outline api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/JwksTrustProfileResolver.java --view expanded || true

printf '\nTokenValidatorProducer outline:\n'
ast-grep outline api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/TokenValidatorProducer.java --view expanded || true

printf '\nResolving JWKS file references:\n'
sed -n '1,260p' api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/JwksTrustProfileResolver.java

Repository: cuioss/API-Sheriff

Length of output: 7332


🏁 Script executed:

#!/bin/bash
set -u

printf 'TokenValidatorProducer applyJwks section:\n'
sed -n '1,220p' api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/TokenValidatorProducer.java

printf '\nSearch token-validation library files and model mapping:\n'
rg -n -C4 'applyJwks|FileJwksLoaderConfig|file.*jwks|JwksLoader|resolveConfigDir|ConfigProducer' --glob '*.java' api-sheriff | head -n 240

Repository: cuioss/API-Sheriff

Length of output: 40777


🏁 Script executed:

#!/bin/bash
set -u

printf 'Locate upstream JWKS file loader references:\n'
rg -n -C4 'FileJwksLoader|JwksFile|jwksFilePath|fileJwks|FileJwksLoaderConfig|FileJwks' . | head -n 240

printf '\nRead-only behavioral probe: Java Paths.resolve semantics for config-relative path:\n'
python3 - <<'PY'
from pathlib import PurePosixPath
config_dir = PurePosixPath('target/test-classes/config/testboot')
path_string = '../integration-tests/src/main/docker/certificates/test-jwks.json'
result = config_dir / path_string
print(result.as_posix())
PY

Repository: cuioss/API-Sheriff

Length of output: 1791


Resolve jwks.file to the config-relative fixture before passing it to the validator builder.

TokenValidatorProducer.applyJwks() passes jwks.file() through as an unmodified String, and the test runs from target/test-classes/config/testboot. Relative paths like ../integration-tests/src/main/docker/certificates/test-jwks.json resolve from the process working directory instead of the JWKS loader or config directory, so boot can fail when it only finds the fixture while running tests from api-sheriff/. Resolve Path.of(configDir).resolve(jwks.file()).normalize().toString() before builder.jwksFilePath(...), or switch the fixture to an absolute path.

Comment thread doc/architecture.adoc
Comment on lines +119 to +124
| Management interface
| `NeutralTlsConfigSource`, a SmallRye `ConfigSource` projecting `management.tls.enabled: false`
onto `quarkus.management.tls-configuration-name`.
| `VertxHttpRecorder.initializeManagementInterface` never enumerates `HttpServerOptionsCustomizer`
beans. The only lever left on that path is the configuration the recorder reads, so a config
source is the seam *by elimination*, not by preference.

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

Remove the stale HTTPS-only statement.

This new section documents management.tls.enabled: false and a plain-HTTP management path. Later in doc/architecture.adoc, Lines [551-555] still state that no plain-HTTP management surface remains. Lines [590-597] also describe the management surface as HTTPS-only.

Update those retained paragraphs so the architecture document matches the new opt-out.

Comment thread doc/configuration.adoc
Comment on lines +162 to +164
tls: # TLS POLICY for the terminated listener. Neutral names only:
# ports and certificate/trust material stay deployment-
# supplied and never appear here (ADR-0025)

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 | 🟠 Major | ⚡ Quick win

Keep trust material out of neutral configuration examples.

The examples place deployment-specific mTLS trust-anchor paths in gateway.yaml, despite the documented rule that trust material stays deployment-bound.

  • doc/configuration.adoc#L162-L164: remove the client_ca path shown at Line [174] and document the deployment-owned binding.
  • doc/user/tls-edge.adoc#L91-L97: remove /etc/sheriff/client-ca.pem from the neutral example.
📍 Affects 2 files
  • doc/configuration.adoc#L162-L164 (this comment)
  • doc/user/tls-edge.adoc#L91-L97

Source: Path instructions

Comment thread doc/configuration.adoc
Comment on lines +838 to +845
*These keys are the single effective source.* The three policy keys above are bound onto the
terminated listener by a customizer that runs after the runtime has built its SSL options, so what
is written here is what the listener starts with. There is deliberately no raw `quarkus.tls.*`
policy key left in `application.properties` for them to compete with -- see
link:#_server_tls_key_verdicts[Server-TLS key verdicts] for the disposition of every key that used
to live there, and
link:adr/0025-The_whole_server-TLS_surface_is_neutral_in_gatewayyaml_and_bound_by_exactly_two_seams.adoc[ADR-0025]
for the classification rule that decides where a new knob belongs.

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

Use conditional precedence wording for TLS policy.

The documents state that gateway.yaml is the only effective source, but the supplied TlsServerCustomizer preserves runtime defaults when a neutral key is omitted. QUARKUS_HTTP_SSL_PROTOCOLS can therefore remain effective until tls.min_version is declared.

  • doc/configuration.adoc#L838-L845: state that the neutral key is authoritative when declared.
  • doc/configuration.adoc#L948-L958: align the server-TLS verdict introduction with that conditional rule.
  • doc/user/environment-variable-overrides.adoc#L214-L220: retain the documented fallback behavior.
  • doc/user/environment-variable-overrides.adoc#L386-L388: remove the unconditional “No” answer.
  • doc/user/tls-edge.adoc#L46-L49: replace “only source” with the conditional precedence statement.
📍 Affects 3 files
  • doc/configuration.adoc#L838-L845 (this comment)
  • doc/configuration.adoc#L948-L958
  • doc/user/environment-variable-overrides.adoc#L214-L220
  • doc/user/environment-variable-overrides.adoc#L386-L388
  • doc/user/tls-edge.adoc#L46-L49

Comment on lines +69 to +71
private static final List<String> HTTPS_SERVICES = List.of(
"api-sheriff", "api-sheriff-mtls", "api-sheriff-cookie", "api-sheriff-cookie-2",
"api-sheriff-ws-admission");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Derive the "must-stay-HTTPS" service set from docker-compose.yml instead of hardcoding it.

HTTPS_SERVICES is a fixed list of five service names. noOtherInstanceActivatesTheOptOut only checks these five. If a new api-sheriff* instance is added to docker-compose.yml and it accidentally sets QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME, this test does not detect it, because the new service is outside the hardcoded enumeration. The guard's purpose is to catch exactly this scenario, but its predicate cannot fire for a service it does not know about.

Derive the checked set from the compose file itself, for example by enumerating every service key except OPT_OUT_SERVICE (and any explicitly non-gateway services), so a newly added instance is covered without a manual update to this test.

♻️ Proposed direction
-    private static final List<String> HTTPS_SERVICES = List.of(
-            "api-sheriff", "api-sheriff-mtls", "api-sheriff-cookie", "api-sheriff-cookie-2",
-            "api-sheriff-ws-admission");
+    // Derived at test time from docker-compose.yml so a newly added instance is
+    // covered automatically instead of requiring a manual addition to this list.
+    private static List<String> httpsServices() throws IOException {
+        return allServiceNames().stream()
+                .filter(name -> !OPT_OUT_SERVICE.equals(name))
+                .toList();
+    }

Based on path instructions: "Treat a hardcoded list that must mirror a set defined elsewhere ... as a defect unless it is derived from that source at build or run time. Name the authoritative definition and the drift it permits."

Also applies to: 101-111

Source: Path instructions

Comment on lines +83 to +94
void managementRefusesHttps() {
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(plainManagementBaseUri().replace("http://", "https://") + "/q/health/live"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();

assertThrows(IOException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()),
"a plain listener cannot complete a TLS handshake; if this succeeds the instance is "
+ "serving HTTPS and the opt-out silently did nothing");
}

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

Narrow the expected exception to SSLException.

The javadoc states this test proves the port does not answer over HTTPS. IOException is too broad for that claim. ConnectException, HttpConnectTimeoutException, and a DNS failure are all IOException, so the assertion also passes when nothing listens on the port at all. The test then reports success for the wrong reason.

A TLS handshake failure against a plain listener raises SSLException (usually SSLHandshakeException). Assert that type, so the test distinguishes "the listener is plain" from "there is no listener".

💚 Proposed fix
-        assertThrows(IOException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()),
+        assertThrows(SSLException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()),
                 "a plain listener cannot complete a TLS handshake; if this succeeds the instance is "
                         + "serving HTTPS and the opt-out silently did nothing");

Add the import:

import javax.net.ssl.SSLException;

java.io.IOException stays in use for readContainerLog.

As per path instructions: "Check a guard's predicate against the scenario the guard exists for."

📝 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
void managementRefusesHttps() {
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(plainManagementBaseUri().replace("http://", "https://") + "/q/health/live"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
assertThrows(IOException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()),
"a plain listener cannot complete a TLS handshake; if this succeeds the instance is "
+ "serving HTTPS and the opt-out silently did nothing");
}
void managementRefusesHttps() {
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(plainManagementBaseUri().replace("http://", "https://") + "/q/health/live"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
assertThrows(SSLException.class, () -> client.send(request, HttpResponse.BodyHandlers.discarding()),
"a plain listener cannot complete a TLS handshake; if this succeeds the instance is "
"serving HTTPS and the opt-out silently did nothing");
}

Source: Path instructions

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