UNOMI-974: Require an explicit admin and health-check password at startup - #850
Merged
Conversation
Closed
10 tasks
…rtup The shipped karaf and health-check accounts fell back to a value carried in the distribution when the operator set nothing. Both now resolve only from UNOMI_ROOT_PASSWORD and UNOMI_HEALTHCHECK_PASSWORD, with no fallback. Removing the fallback is necessary but not sufficient, because of how Karaf resolves properties: PropertiesLoader and PropertiesLoginModule both substitute with defaultsToEmptyString=true, so an unset property yields an empty value rather than an unusable account. The account must therefore be made unusable deliberately. bin/setenv and the Docker entrypoint refuse to start when either variable is unset, and AuthenticationFilter requires a non-blank password on any Basic credential it accepts. That last check runs at each point a Basic credential is consumed rather than once at the top of filter(). The public paths and every V2 path ignore Authorization entirely, so a single up-front check would turn a stray or stale header into a 401 on requests that must succeed anonymously. The shell guards cannot cover every way the JVM is started - karaf.bat calls setenv.bat without testing errorlevel, as setenv itself documents, and a systemd unit or container command override skips them too - so the REST-layer requirement is what actually holds. The guards are executed by the tests rather than grepped, because a check whose text is present but whose condition never matches would otherwise pass silently. Documentation, examples, compose files and the setup scripts no longer carry a sample password. Operators upgrading must set both variables before starting; the 3.0-to-3.1 migration guide covers it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
asf-gitbox-commits
force-pushed
the
UNOMI-974-explicit-admin-password
branch
from
August 14, 2026 14:32
8f0f416 to
4e3092f
Compare
…ndpoints too
The blank-password refusal added to AuthenticationFilter only covers JAX-RS. Two other surfaces
parse the Basic header themselves and call LoginContext.login() directly against the same karaf
realm, so an empty password still authenticated there: the health check HTTP context and the
GraphQL servlet validator. Both now refuse it.
The health check path was reachable in practice. It split the decoded credential with split(":"),
which discards trailing empty strings, so "health::x" decoded to ["health", "", "x"] and handed
JAAS an empty password. Bounding the split to two parts restores the RFC 7617 rule that the
password is everything after the first colon, and makes the emptiness check meaningful. The same
parser threw out of the servlet on several malformed headers, answering 500 where 401 was meant;
it now returns no credential instead. The scheme is matched case-insensitively (RFC 7235 2.1)
because the previous blind substring(6) accepted "basic " and a stricter check would have started
rejecting those clients.
Both tests stub a realm that accepts any credential. That is deliberate: a rejecting realm answers
"not authenticated" whether or not the guard exists, so only an accepting one can tell "refused
before JAAS" from "JAAS said no". The health check test asserts on the credential the realm was
actually handed, which is the only way to catch the split bug, since the bypass still returned
"authenticated".
extractBasicCredentials is covered exhaustively -- 9 header shapes that must yield no credential
and 12 that must decode to specific values, including the empty, colon-only and non-ASCII cases,
and an assertion that neither element is ever null so the emptiness check cannot throw.
AuthenticationFilter's guard at the ordinary V3 private path had no test: the suite only exercised
the tenants branch and the V2 branch, so deleting that third call site left every test green. Two
tests now cover it.
Documentation that still presented the removed karaf/karaf and health/health pairs as working
defaults is corrected, including the configuration chapter, which contradicted its own REST API
security section. building-and-deploying told readers to run ./bin/karaf with no password step,
which now fails outright; it gains the export step and the Windows caveat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The shipped karaf and health-check accounts fell back to a value carried in the distribution when
the operator set nothing. Both now resolve only from UNOMI_ROOT_PASSWORD and
UNOMI_HEALTHCHECK_PASSWORD, with no fallback.
Removing the fallback is necessary but not sufficient, because of how Karaf resolves properties:
PropertiesLoader and PropertiesLoginModule both substitute with defaultsToEmptyString=true, so an
unset property yields an empty value rather than an unusable account. The account must therefore be
made unusable deliberately. bin/setenv and the Docker entrypoint refuse to start when either
variable is unset, and AuthenticationFilter requires a non-blank password on any Basic credential it
accepts.
That last check runs at each point a Basic credential is consumed rather than once at the top of
filter(). The public paths and every V2 path ignore Authorization entirely, so a single up-front
check would turn a stray or stale header into a 401 on requests that must succeed anonymously.
The shell guards cannot cover every way the JVM is started - karaf.bat calls setenv.bat without
testing errorlevel, as setenv itself documents, and a systemd unit or container command override
skips them too - so the REST-layer requirement is what actually holds. The guards are executed by
the tests rather than grepped, because a check whose text is present but whose condition never
matches would otherwise pass silently.
Documentation, examples, compose files and the setup scripts no longer carry a sample password.
Operators upgrading must set both variables before starting; the 3.0-to-3.1 migration guide covers
it.
Jira: https://issues.apache.org/jira/browse/UNOMI-974