feat(tls): single-source and activate neutral TLS + management surface - #138
feat(tls): single-source and activate neutral TLS + management surface#138cuioss-oliver wants to merge 13 commits into
Conversation
…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
There was a problem hiding this comment.
Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
📝 WalkthroughWalkthroughThe gateway now defines TLS policy in ChangesTLS and management configuration
WebSocket log catalogue
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
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. Comment |
PR Reviewer Guide 🔍
|
There was a problem hiding this comment.
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 winThis test duplicates
enabledRequiresClientAuthWithTrustAnchorand 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.xHttpServerOptions, which never carries a Quarkus property in a plain unit test. So it asserts the Vert.x library default, not thatquarkus.http.ssl.client-authwas deleted.The stated behaviour is "no raw
quarkus.http.ssl.client-authdefault is needed". The mechanism that would prove it is the content ofapi-sheriff/src/main/resources/application.propertiesandapi-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-authproves the single-source claim; this test does not. The PR already hasSingleSourceTlsContractTest, 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 winDerive 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
📒 Files selected for processing (37)
.plan/project-architecture/api-sheriff/enriched.jsonapi-sheriff/pom.xmlapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/ConfigLogMessages.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSource.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/ManagementConfig.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/TlsConfig.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAudit.javaapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.javaapi-sheriff/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProviderapi-sheriff/src/main/resources/application.propertiesapi-sheriff/src/main/resources/schema/gateway.schema.jsonapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/NeutralTlsConfigSourceTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/SingleSourceTlsContractTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAuditTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/MtlsServerCustomizerTest.javaapi-sheriff/src/test/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizerTest.javaapi-sheriff/src/test/resources/application.propertiesapi-sheriff/src/test/resources/config/testboot/gateway.yamlapi-sheriff/src/test/resources/config/valid/gateway.yamldoc/LogMessages.adocdoc/adr/0025-The_whole_server-TLS_surface_is_neutral_in_gatewayyaml_and_bound_by_exactly_two_seams.adocdoc/architecture.adocdoc/configuration.adocdoc/user/README.adocdoc/user/environment-variable-overrides.adocdoc/user/tls-edge.adocintegration-tests/docker-compose.ymlintegration-tests/pom.xmlintegration-tests/scripts/start-integration-container.shintegration-tests/src/main/docker/sheriff-config/gateway.yamlintegration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ManagementPlainHttpActivationWiringTest.javaintegration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ManagementPlainHttpOptOutIT.java
| * 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. |
There was a problem hiding this comment.
🩺 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
| /** | ||
| * 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"); |
There was a problem hiding this comment.
📐 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
| jwks: | ||
| source: file | ||
| file: ../integration-tests/src/main/docker/certificates/test-jwks.json |
There was a problem hiding this comment.
🩺 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
doneRepository: 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' || trueRepository: 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.javaRepository: 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 240Repository: 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())
PYRepository: 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.
| | 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. |
There was a problem hiding this comment.
🎯 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.
| tls: # TLS POLICY for the terminated listener. Neutral names only: | ||
| # ports and certificate/trust material stay deployment- | ||
| # supplied and never appear here (ADR-0025) |
There was a problem hiding this comment.
🗄️ 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 theclient_capath shown at Line [174] and document the deployment-owned binding.doc/user/tls-edge.adoc#L91-L97: remove/etc/sheriff/client-ca.pemfrom the neutral example.
📍 Affects 2 files
doc/configuration.adoc#L162-L164(this comment)doc/user/tls-edge.adoc#L91-L97
Source: Path instructions
| *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. |
There was a problem hiding this comment.
🎯 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-L958doc/user/environment-variable-overrides.adoc#L214-L220doc/user/environment-variable-overrides.adoc#L386-L388doc/user/tls-edge.adoc#L46-L49
| 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"); |
There was a problem hiding this comment.
🔒 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
| 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"); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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
Summary
The gateway's declared TLS floor and cipher-suite allowlist were inert in production — the
neutral
gateway.yamltlskeys 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 theHttpServerOptionsCustomizerseam (TlsServerCustomizer, sibling of the existingMtlsServerCustomizer), and the rawquarkus.*duplicates that were never doing anything aredeleted outright. The management interface gets a first-class neutral
management:policy blockprojected onto
quarkus.management.*by a single-ordinal (>300) SmallRyeConfigSource(
NeutralTlsConfigSource) — the seam the management path actually enumerates, sinceVertxHttpRecorder.initializeManagementInterfacenever consultsHttpServerOptionsCustomizerbeans. 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'sneutral-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.yamlTLS block to the terminated listener, delete the inert raw keys, relocate the build-time knobapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/TlsServerCustomizer.java(new): mapstls.min_version,tls.cipher_suites,tls.alpnonto the realHttpServerOptionsfor 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 thecipherSuitescomponent.api-sheriff/src/main/resources/schema/gateway.schema.json: addstls.cipher_suites.api-sheriff/src/main/resources/application.properties: deletesquarkus.tls.protocols,quarkus.tls.cipher-suites,quarkus.tls.alpn,quarkus.http.ssl.client-auth(all proven inert or redundant against the shippedquarkus-vertx-http-3.37.4bytecode); relocatesquarkus.ssl.nativetoapi-sheriff/pom.xml'snativeprofile (it is a build-time GraalVM knob); retainsquarkus.http.ssl-portandquarkus.http.insecure-requestsas deliberately deployment-bound.TlsServerCustomizerTest(new),MtlsServerCustomizerTest,ConfigModelContractTest,ConfigLoaderTestupdated for the new arity/coverage;config/valid/gateway.yamland the testapplication.propertiesfixture reconciled so tests and production take the same branch throughHttpServerOptionsUtils.getTlsConfiguration's registry-default guard.D2 — Neutral management policy block and the projecting
ConfigSourceapi-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/ManagementConfig.java(new): neutralmanagement: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) SmallRyeConfigSourceparsinggateway.yaml'stls:/management:blocks directly (it runs before CDI).api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java: adds themanagementcomponent.api-sheriff/src/main/resources/schema/gateway.schema.json: adds themanagementobject;portis declared solely to be refused ("not": {}plus a siblingerrorMessage), landing the actionable rejection at the exact/management/portJSON Pointer.api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java: enables the validator'serrorMessageextension.ConfigLoaderTestgains the R7 gate (rejectsManagementPortNamingTheDeploymentKnobAtItsOwnPointerplus its matched positive control);NeutralTlsConfigSourceTest(new);ConfigModelContractTest,SheriffMetricsTestupdated 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: addsWARN.MANAGEMENT_PLAIN_HTTP(ApiSheriff-115).api-sheriff/src/main/java/de/cuioss/sheriff/gateway/tls/ManagementPlainHttpAudit.java(new):@ApplicationScopedStartupEventobserver emitting the WARN on the observed effective state, eagerly activated and actively invoking its collaborator to defeat the lazy-CDI-proxy trap (lesson2026-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 serviceapi-sheriff-plain-mgmt(publishing19005:9000, settingQUARKUS_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 dedicatedhttp://readiness gate block (not folded into the existinghttps://-hard-coded loop).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(commit50abc37, post-outline finalize finding):tls.cipher_suitesnow validates every declared suite againstSSLContext.getDefault().getSupportedSSLParameters().getCipherSuites()and aborts boot on an unsupported/mistyped entry, symmetric with the existingmin_version/alpnfail-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 (HttpServerOptionsCustomizerfor the terminated listener, the projectingConfigSourcefor management), and the rejectedManagementConfig.tlsConfigurationName()-as-fallback candidate.D5 — Documentation and the machine-asserted completion bar
doc/configuration.adoc,doc/architecture.adoc,doc/LogMessages.adoc(addsApiSheriff-115and the previously-undocumentedApiSheriff-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 shippedapplication.propertiesand mechanically asserts no raw server-TLSquarkus.*policy key remains, with an explicit allowlist for the two named exceptions, plus thequarkus.ssl.nativeabsent-here/present-in-pom.xml-native-profile pair.integration-tests/src/main/docker/sheriff-config/gateway.yaml: the primary IT instance now declaresmin_versionandcipher_suitesso the stack exercises agateway.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.logunderthe
(scope-deviation:accept)marker and reflected in the code as shipped:hard-rejection of
management.portAND aConfigValidatorrule with an actionable message.ConfigLoaderaborts on schema failure before binding ever runs, so the validator rule wouldhave been unreachable dead code. Research against the
quarkusio/quarkus3.37 branchestablished that (a)
quarkus.management.tls-configuration-namepointed at a key-less namedTLS bucket is a genuine, runtime-overridable, native-safe off-switch for management TLS with a
legible
ConfigurationExceptionon misuse, and (b) the actionable rejection message belongs inthe JSON schema at its exact JSON Pointer via the networknt
errorMessageextension.Resolution: management HTTPS stays the default on deployment-bound port 9000; the
originally-specified bespoke
gateway.yamlacknowledgement key was dropped in favour of theQuarkus-native
QUARKUS_MANAGEMENT_TLS_CONFIGURATION_NAME; WARN115is retained but triggerson observed effective state at startup, not on a declared key.
managementblock declares noport. Its only route into Quarkus would be thesame single-ordinal (>300) config source, where it would silently outrank a deployment-set
QUARKUS_MANAGEMENT_PORT.ConfigValidator/schema instead refuses amanagement.portkey witha message naming the real knob (
quarkus.management.port/QUARKUS_MANAGEMENT_PORT).Two Corrections to the Original Spec, Established by Evidence
gateway.yamlandquarkus.*bothconfigured 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 productionsupplies it through
quarkus.http.ssl.certificate.*— so tests and production took oppositebranches through
HttpServerOptionsUtils.getTlsConfiguration's registry-default guard.HttpServerOptionsCustomizeras the binding seam for the management interface.Verified against
quarkus-vertx-http-3.37.4bytecode:VertxHttpRecorderenumerates those beansonly on the main-server path, never for the management interface. ADR-0025 records the correct
seam (the projecting
ConfigSource).Verification
verify -Ppre-commitandverify -Pcoverageare green.failures, 0 errors, 0 skipped. It was run twice more after later commits, green each time.
api-sheriff-plain-mgmtinstance crash-looped with "Port 8443 seems to be in use" because itmounts the shared
sheriff-config(which declarespassthrough_sni, starting the SNI frontlistener on 8443) without the
QUARKUS_HTTP_SSL_PORT=8444move. Fixed in commitbc3d622.tls.cipher_suiteshad no fail-closed validation. A probeagainst 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
50abc37with 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.adocrather than fixed, because both are outside this plan's server-TLS-configuration footprint:
4b0102—QUARKUS_HTTP_SSL_PROTOCOLSon theapi-sheriff-mtlsIT instance works onlywhile that overlay's
gateway.yamldeclares notls.min_version; adding one there wouldsilently shadow the environment pin.
df7bd1— what thetoken-sheriffextension's readiness check reports in a deployment thatconfigures
gateway.yamlissuers but nosheriff.token.issuers.*was not established by readingand is flagged as explicitly inconclusive.
Related Issues
None.
Generated by plan-finalize skill
Summary by CodeRabbit
New Features
Documentation
Bug Fixes