Repository navigation
fix(native): the native image registers the API keys file's YAML model for reflection, so it starts with a named keys file - #371
Merged
Conversation
…l for reflection, so it starts with a named keys file Jackson reads kates.api.keys-file into ApiKeyFile.FileModel and EntryModel by reflection, and nothing registered either class for the native image. Deploying the 1.25.1-native image with KATES_API_KEYS_FILE set, which is how MCP agent keys are issued, made readValue throw, so ApiKeys.onStart refused the file as "not YAML of the expected shape" and the pod crash-looped. The JVM image was unaffected, so no JVM test could show it. Both classes now carry @RegisterForReflection, which registers their package-private constructor and fields. The native smoke test never reached this path: it boots with KATES_API_SECURITY_ENABLED=false, and with security off the keys file is never read. After its existing checks it now boots the image again with security on and a keys file of one human read key, written 0644 so the image's non-root user can read it, and checks that GET /api/tests answers 200 with that key and 401 without one. The docker run flags and the liveness wait became start_app and wait_live, shared by both boots. The key is hashed inside the Postgres container, so the host still needs only docker and curl. kates/docs/native-image.md describes the second boot, and says that NativeReflectionRegistryTest scans only public classes in the payload packages, so a package-private model that Jackson reads carries @RegisterForReflection itself.
This was referenced Oct 17, 2026
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 native image can't start once a named API keys file is set. Checked in the code:
ApiKeyFilereadskates.api.keys-filewith Jackson intoApiKeyFile.FileModelandEntryModel, and Jackson builds both by reflection. Nothing registers either class for the native image: notNativeReflectionConfig, notNativePayloadReflectionConfig, notMETA-INF/native-image/reflect-config.json.DisruptionPlaybookCatalogreads YAML the same way, and its classes are registered inNativePayloadReflectionConfig.1.25.1-nativeimage withvalues-native.yamland setKATES_API_KEYS_FILE, which is how MCP agent keys are issued.readValuefinds no constructor or fields, so the parse throws "the keys file is not YAML of the expected shape".ApiKeys.onStartturns that into anIllegalStateException, the Kates API does not start, and the pod crash-loops. The JVM image is unaffected, so no JVM test can show it.scripts/native-smoke-test.shboots withKATES_API_SECURITY_ENABLED=false, andApiKeys.onStartreturns early when security is off, so the keys file is never read. No CI job has booted the native image with security on.NativeReflectionRegistryTestleavescom/bmscomp/kates/securityout ofPAYLOAD_PACKAGES, but adding the package would not catch this either:isSerializedPayloadskips every class that is not public, andApiKeyFile,FileModelandEntryModelare all package-private.The audit offered a second fix, listing both classes in
NativePayloadReflectionConfig's targets next to the playbook classes. That doesn't compile:ApiKeyFileis package-private and the holder is incom.bmscomp.kates.config, soApiKeyFile.FileModel.classcan't be named there.Changes
ApiKeyFile@RegisterForReflectiononFileModeland onEntryModel. Quarkus registers their package-private constructor and fields, and Jackson needs both.scripts/native-smoke-test.shkates_smoke001_native-smoke-only-<pid>, which matchesApiKeyFile.KEY;smoke001, namenative-smoke, typehuman, scopes[read], and the key's SHA-256;mktemp, then made 0644, becausemktempmakes it 0600 and the image runs as a non-root user;/etc/kates/keys.yaml, withKATES_API_KEYS_FILEpointing at it.GET /api/testsanswers 200 withAuthorization: Bearer <key>and 401 without one.sha256suminside the Postgres container, so the host still needs only docker and curl (macOS has nosha256sum).docker runflags becamestart_app, and the liveness wait becamewait_live. Both boots call them, each passing its own flags through"$@", so the two boots can't drift apart. Only the first boot passesKATES_API_SECURITY_ENABLED=false.${APP}name, socleanup()andfail()still cover it.cleanup()also removes the keys file, with${KEYS_FILE:-}soset -udoesn't trip when the script exits before the file exists.kates/docs/native-image.mdNativeReflectionRegistryTestscans only public classes in the payload packages, so a package-private model that Jackson reads, such asApiKeyFile.FileModel, carries@RegisterForReflectionitself.Tests
ApiKeyFileTestandApiKeysTeststill pass.git diff --name-only origin/main...HEAD: 3 paths,kates/docs/native-image.md,kates/src/main/java/com/bmscomp/kates/security/ApiKeyFile.javaandscripts/native-smoke-test.sh. They triggerci.yml(kates),native.yml(kates/**andscripts/native-smoke-test.sh), thelint.ymlscripts job, CodeQL (kates/src),integration.ymlandci-docker.yml. No cli,docs/**, charts,.github, Makefile or yml change.cd kates && ./mvnw -o test -Dquarkus.grpc.codegen.skip=true -Dquarkus.http.test-port=0: 1,466 tests, 0 failures, 0 errors, 0 skipped.spotless:checkon JDK 25: clean.cd cli && go test ./...: all packages ok, including the caveat ref anchors incmd. No CLI ref points atApiKeyFile.java, so the lines added there move no ref.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.shellcheck -x -S warning -e SC2317,SC2034,SC2155 scripts/*.sh kates-ci.sh, thelint.ymlscripts job's exact command, with shellcheck 0.11.0.bash -nonnative-smoke-test.shis also clean.scripts/check-install-kates.sh, the scripts job's second step: all 11 checks ok.docs/**changed (kates/docs/native-image.mdis outside thedocs.ymlandbook.ymlfilters);charts/**changed;make check-help, actionlint and yamllint, because no Makefile,.github/**or yml file changed;scripts/native-smoke-test.sh kates:native-ci, because it needs a Docker native-image build (about 20 minutes) and Docker stays off locally. This is the only check that runs the new code paths. Reviewed by hand instead: the key matchesApiKeyFile.KEY; a keys file with nokates.api.keypassesSecurityStartupValidator;KATES_API_KEYS_FILEmaps tokates.api.keys-file;postgres:16-alpinehassha256sum; and the docs note matchesNativeReflectionRegistryTest'sModifier.isPublicfilter.native.yml): buildskates/Dockerfile.nativeand runsscripts/native-smoke-test.sh. It is the only check that runs the keys-file boot and the reflection fix;integration.yml): the*ITtests and the Kind tests;ci-docker.yml);kates/src(codeql.yml);github-advanced-securityAI scan, which fails on every PR for a GitHub-side reason.Decisions
@RegisterForReflectiononFileModelandEntryModel, asPagedResponsedoes, not inNativePayloadReflectionConfig(see Why).sha256suminside the container. The alternative was the annotations alone, with one native deploy checked by hand.NativeReflectionRegistryTestis not changed. Scanningcom/bmscomp/kates/securitywouldn't catch this bug, because the scan skips classes that are not public. It would flag only public classes that are never serialized (ApiKeyAuthenticationRequest,KatesIdentities,Scopes,KatesPrincipal), and each would need aNOT_SERIALIZEDexemption. Widening the scan to package-private classes in all 9 packages isn't worth it either:ApiKeyFile.FileModelis the onlyreadValue/convertValuetarget inkates/src/main/javathat isn't public.Deliberately not done
ApiKeyFilejavadoc and its parse rules: an id matching^[a-z0-9]{4,32}$, a valid name, typehuman, scoperead, and 64 lower-case hex characters.Landing
This PR shares no file with the earlier plan PRs, so it can land at any time.