Repository navigation
fix(security): the audit and pentest read each listener's protocol, so Kafka's default GSSAPI and named listeners no longer pass - #379
Merged
Conversation
…o Kafka's default GSSAPI and named listeners no longer pass SASL Authentication and SASL Mechanism Strength passed when sasl.enabled.mechanisms was set, and Kafka reports GSSAPI for that key on every broker that leaves it alone: a broker with only PLAINTEXT://:9092, SecurityApiIT's container among them, read "SASL mechanisms: GSSAPI", and the pentest called its metadata PROTECTED. On the kafka-cluster chart's Strimzi cluster it went the other way: the broker-wide key is empty, so SASL Authentication failed although listener PLAIN-9092 is SCRAM, and No Plaintext Listeners and the unencrypted probe passed because they looked for the text PLAINTEXT://, which PLAIN-9092 (SASL_PLAINTEXT, no TLS) never holds. Listener Protocol Map passed whenever the map held SASL_PLAINTEXT, and Kafka's default map holds all four protocols. SecurityKafkaHelpers now works out each listener's protocol from listeners and listener.security.protocol.map, a missing listeners counting as Kafka's default PLAINTEXT://:9092. SASL Authentication and metadata-leak pass when some listener is SASL_* and none is PLAINTEXT. No Plaintext Listeners, Listener Protocol Map and the unencrypted probe flag PLAINTEXT and SASL_PLAINTEXT listeners in use, and name them; advertised.listeners is no longer read. SASL Mechanism Strength judges the SASL listeners' listener.name.<listener>.sasl.enabled.mechanisms, falling back to the broker-wide key, and says "No SASL listener" when there is none. Whether DescribeConfigs returns the per-listener key is not checked on a live cluster yet. SecurityListenerProtocolsTest feeds the audit and the pentest the chart's Strimzi config (with an empty and a GSSAPI broker-wide key), a vanilla PLAINTEXT broker, a SASL_SSL-only broker and a PLAINTEXT listener beside a SASL one. SecurityApiIT now asserts the container fails SASL Authentication, warns on plaintext naming its BROKER listener (which only a config read from the broker holds) and is VULNERABLE to both probes. Caveat security-broker-wide-keys is rewritten to say what the checks read now and that the ssl.* checks are still broker-wide; the skill's bullet follows it. The security refs the edit moved are re-pointed and anchored. Grades change with no config change: the chart's cluster now passes SASL Authentication and warns on No Plaintext Listeners and Listener Protocol Map, and a vanilla broker fails SASL Authentication, which a security gate or a saved drift baseline will show.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The security audit and the pentest decided SASL and encryption from broker-wide text, not from the listeners. Kafka's defaults and Strimzi's named listeners both broke that. Checked in the code:
sasl.enabled.mechanismswas non-empty.describeConfigsreturns Kafka's default for that key,GSSAPI, on a broker that never sets it. So a broker with onlyPLAINTEXT://:9092read "SASL mechanisms: GSSAPI" and passed. SASL Mechanism Strength (check 17) passed the same way, and the pentest'smetadata-leakprobe reported PROTECTED. SecurityApiIT's own container (KafkaTestResource.java:37, all PLAINTEXT listeners) is such a broker. The IT asserted only the report's shape, so nothing caught it.plainlistener (charts/kafka-cluster/values.yaml:94-99) is SCRAM-SHA-512 withtls: false. Strimzi names itPLAIN-9092and maps it toSASL_PLAINTEXT, and it sets the mechanisms per listener, so the broker-wide key is empty:unencryptedprobe passed, because they looked for the textPLAINTEXT://, andPLAIN-9092never holds it. Check 2 also readadvertised.listenersfor the same text.contains("PLAINTEXT") && !contains("SASL_PLAINTEXT")on the raw map. Kafka's default map lists all four protocols, so the check passed on every broker that leaves the map unset, and on any map that holds SASL_PLAINTEXT, even beside a PLAINTEXT listener.security-broker-wide-keysdescribed thePLAINTEXT://text match and the Strimzi case. No caveat covered the GSSAPI-default false PASS. None of the security refs inkates mcpwas anchored, so a ref that moved still passedTestMCPCaveatRefsExist.Changes
SecurityKafkaHelperslistenerProtocols(config): listener name to protocol, inlistenersorder. The name is the text before://, upper-cased withLocale.ROOTas Kafka does. The protocol is that name's entry inlistener.security.protocol.map, or the name itself when the map has none. A missing or blanklistenerscounts as Kafka's defaultPLAINTEXT://:9092.sasl(protocol)(SASL_),unencrypted(protocol)(PLAINTEXT or SASL_PLAINTEXT), andsaslRequired(protocols): some listener is SASL_ and none is PLAINTEXT.listeners(protocols, predicate)formats the matching listeners asPLAIN-9092 (SASL_PLAINTEXT), ....saslMechanisms(config, protocols): the union, over the SASL listeners, oflistener.name.<listener>.sasl.enabled.mechanisms, each falling back to the broker-wide key.SecurityServicesaslRequired. Its detail names the SASL listeners. A failure says "No SASL listener", or names the PLAINTEXT listeners when they sit beside a SASL one.advertised.listenersis no longer read: its names are a subset of the listener names.saslMechanisms.Listeners: NAME (PROTOCOL), ...for every listener.SecurityPentestServicemetadata-leakis VULNERABLE when check 1's rule fails. Its text now covers a PLAINTEXT listener beside a SASL one.unencryptedis VULNERABLE when a listener is PLAINTEXT or SASL_PLAINTEXT, and names it.kates mcpsecurity-broker-wide-keysis rewritten under the same id. It says what the four checks and two probes read now, what unencrypted and SASL mean, and that an SSL listener counts as neither. It says the per-listener mechanisms read is not checked on a live cluster yet. It also says the keystore, truststore, mTLS, TLS and certificate checks still read only broker-widessl.*, so on the chart's cluster they report no keystore and no client authentication while thetlslistener has both.SecurityService.java,SecurityPentestService.javaandSecurityKafkaHelpers.javathat this edit moved is re-pointed and wrapped inmcpAnchoredRef:mcp_tools_security.go:security-config-read-silent,security-tls-config-only,security-broker-wide-keys,security-drift-new-checksandsecurity-compliance-by-category;mcp_caveats.go:pentest-config-only,cve-fixed-list, and the two moved refs ofsecurity-trend-in-memory. Its42,58-69ref does not move and is left as it is.mcp_tools_security.go:71and:713are re-pointed too.Other
skills/kates-cli/SKILL.md: the plaintext bullet matches the new caveat, including the unverified per-listener mechanisms read.eval/mcp/oracle.py: the comment ref toSecurityService.java:42,1653-1693is re-pointed.eval/mcp/tasks.jsonis not touched.Tests
SecurityListenerProtocolsTest. It usesSecurityServiceTrendTest's harness, with a realSecurityKafkaHelperswhosefetchBrokerConfigis overridden. Each case asserts the report has noerrorkey, becauserunAuditgrades an exception F.strimziChartListeners, run with a broker-widesasl.enabled.mechanismsof""and ofGSSAPI. It uses the chart'slisteners(REPLICATION-9091, PLAIN-9092, TLS-9093) and its map, which also names CONTROLPLANE-9090, pluslistener.name.plain-9092.sasl.enabled.mechanisms=SCRAM-SHA-512. Results: SASL Authentication PASS, No Plaintext Listeners WARN naming PLAIN-9092, Listener Protocol Map WARN, SASL Mechanism Strength PASS, metadata-leak PROTECTED, unencrypted VULNERABLE.vanillaPlaintextDefaultGssapi:PLAINTEXT://:9092, the default map, GSSAPI. Results: auth FAIL, mechanism strength WARN, plaintext WARN, protocol map WARN, both probes VULNERABLE.saslSslOnly:SASL_SSL://:9094, SCRAM-SHA-512. Auth, plaintext, protocol map and mechanism strength all PASS, and both probes are PROTECTED.plaintextBesideSasl: a PLAINTEXT listener beside a SASL one fails check 1 and leaks metadata (D1).SecurityApiITnow asserts that the PLAINTEXT container fails SASL Authentication and warns on No Plaintext Listeners withBROKER (PLAINTEXT)in the detail. Only a config read from the real broker names that listener; an unread config would show the defaultPLAINTEXT. It also asserts the container is VULNERABLE tometadata-leakandunencrypted.Run locally, all passing:
git diff --name-only origin/main...HEAD: 9 paths underkates/,cli/cmd/,eval/mcp/andskills/. So the Java and CLI checks apply. Nothing underdocs/,charts/,.github/,scripts/,Makefileor*.y(a)mlchanged, so the book, chart, actionlint, shellcheck, yamllint,make check-helpand docsgen-*.sh --checkchecks were skipped.eval/andskills/trigger no CI job.cd kates && ./mvnw -o test -Dquarkus.grpc.codegen.skip=true -Dquarkus.http.test-port=0: 1,471 tests, 0 failures, 0 errors, 0 skipped.SecurityListenerProtocolsTestdoes not stubdescribeAcls, sofetchAclslogs a WARN with an NPE stack trace on each audit or pentest call. That is log noise, not a failure.spotless:checkon JDK 25: exit 0.cd cli && go test ./...andgo test -p 1 -race -count=1 ./...: all packages ok. The caveat-ref anchor tests pass against the moved lines.go vet ./...,golangci-lint run --new-from-rev=origin/main ./...(0 issues),govulncheck ./...(no vulnerabilities).scripts/check-cli-style.sh,scripts/check-cli-compat.sh.python3 -m unittest discover eval/mcpagainst the built CLI: 240 tests OK. That is not a CI job, butoracle.pychanged. The newSecurityService.java:1653-1693ref was checked by hand: 1653 isscoreHistory's javadoc, 1693 closes the method.CI only:
SecurityApiITand the other*ITtests (Build & Test runs./mvnw verify, which needs Docker).github-advanced-security, red on every PR from a GitHub-side quota error.Decisions
metadata-leakpass when some listener is SASL_* and none is PLAINTEXT. A SASL-only rule would let an open PLAINTEXT listener beside a SASL one read "Unauthenticated Metadata: PROTECTED".listener.name.<listener>.sasl.enabled.mechanismsfor each SASL listener and falls back to the broker-wide key. Without this, the chart's cluster shows SASL Authentication PASS beside "No SASL mechanisms configured". The caveat says the per-listener read is unverified on a live cluster.listenersmeansPLAINTEXT://:9092. An unread config then grades as an unsecured broker, not as "All listeners use TLS or SASL", which fitssecurity-config-read-silent.eval/mcp/tasks.jsonis left alone: it is frozen (eval/mcp/README.md:81-83).Before merge: the lab DescribeConfigs check. Run
kafka-configs.sh --describe --all --entity-type brokers --entity-name <id>against krafter. It uses the same DescribeConfigs call as the audit, and shows two things: whetherlistener.name.plain-9092.sasl.enabled.mechanismsis returned, and what the broker-widesasl.enabled.mechanismsis. If the per-listener key is not returned, SASL Mechanism Strength on the chart's cluster reads "No SASL mechanisms configured", as the caveat says.Deliberately not done
kates security gate --min-gradein CI or a drift against a saved baseline will show it.unencryptedPROTECTED to VULNERABLE.eval/mcp/tasks.json(frozen).cli/client/mcp_security_test.go:16,65andeval/mcp/testdata/api/security_audit.json:16.plans/anddocs/backend-enhancement-plan.md.security-config-read-silent).security.inter.broker.protocol. Kafka ignores that key wheninter.broker.listener.nameis set, and Strimzi sets it toREPLICATION-9091. So check 11 gives a false WARN on Strimzi, from the same root cause.Landing
Lands after #376 (plan PR 7), #377 (plan PR 8) and #378 (plan PR 9). Each edits
cli/cmd/mcp_caveats.go, the one file this PR shares with them:This PR edits :129 (into two lines), :137-138 and :150-151. Those are disjoint from the lines above, and
git merge-treemerges this branch with each of the three cleanly. None of them touchesSecurityService.java,SecurityPentestService.javaorSecurityKafkaHelpers.java, so no ref this PR points at moves.Recipe once those have merged:
TestMCPCaveatRefAnchorsfails on a ref that was missed.cd cli && go test ./cmd -run TestMCPCaveat -count=1.