Skip to content

Backlog

Everything known to be worth doing, with the reason attached. One item per section; the metadata line is parsed by scripts/backlog.py, which lints it and regenerates the Roadmap — so an item cannot be added here, or quietly finished, without the roadmap following.

Fields: priority high|medium|low · status open|doing|done|dropped · milestone the milestone it unblocks, or -.


BL-01 — Decide what replaces jwt-keycloak on Kong 3.x

Priority: high · Status: open · Milestone: M3b

The upstream is archived; the only non-archived fork has 4 stars and was last pushed in July 2024. It is on the authentication path, and it is the single thing keeping the 3.x image unpublishable.

⚠️ Measured on 2026-09-14, and it narrows the question. oidcify validates Keycloak access tokens too, not only ID tokens: with bearer_jwt_allowed_auds: ['account'] a real access token is accepted, while no token, a malformed one, an invalid signature and a wrong audience are each refused with 401. The end-to-end suite asserts all five on the 3.x line.

So the authentication half needs no fork. What is left is the authorization half: jwt-keycloak also validates scope, roles, realm_roles and client_roles, and can map a claim onto a Kong consumer; oidcify instead maps a groups claim into authenticated_groups for Kong's ACL plugin.

The question is no longer which fork but: do any live routes use those validators? If none do, this closes by consolidation with no new dependency on the authentication path. That is a question about the running configuration, which only its owner can answer.

The replacement mechanism is now proven, not just plausible: the end-to-end suite asserts that oidcify + Kong's ACL plugin authorizes by group in both directions on the 3.x line. What is missing is the mapping from the roles a live configuration uses onto Keycloak groups.

Done when: the live Kong configuration has been checked for scope/roles/realm_roles/ client_roles/consumer_match on jwt-keycloak, and either those routes have an ACL-based equivalent or the plugin is dropped — with the reasoning written in docs/milestones.md.

BL-02 — Kong 3.9.3 Admin API returns 500 with an external plugin registered

Priority: high · Status: open · Milestone: M4

GET / answers Cannot serialise cdata: type not supported when oidcify is registered, so decK — and anything else reading that endpoint — cannot configure a database-backed Kong 3.9.3. Kong 2.8.5 with the same plugin answers 200, and DB-less 3.x is unaffected.

⚠️ It is known upstream and has been open for a year and a half: Kong/kong#14260, "go-pdk plugins break Kong 3.9.0", opened 2025-02-11, eleven comments, still open as of 2026-09-22. Their reproduction is DB-less with an empty configuration; ours is 3.9.3 with a database, where it also blocks decK — so the fault survives two patch releases and is wider than the report.

That settles one thing: waiting for upstream is not a plan. The 3.x line is either DB-less, or it does not use an external plugin.

Done when: either upstream fixes it, or the 3.x line is committed to DB-less and that is stated where deployments are described.

BL-03 — Pin the base image by digest, not by tag

Priority: high · Status: done · Milestone: -

FROM kong:${KONG_VERSION}-ubuntu is a mutable tag. This repository insists deployments pin digests and then rests on a tag one level up: reproducibility stops exactly where it is asserted most loudly.

Done when: the Dockerfile pins kong@sha256:… per line, with the human-readable tag in a comment.

✅ Done 2026-09-15. kong-base-digests.env holds the mapping; ARG KONG_DIGEST defaults to the entry for the default version, so a bare docker build is pinned too; CI and scripts/run-local.sh pass the entry for the line they build; and tests/policy.sh checks the default still matches the file and that every line CI builds has an entry. A wrong digest fails the build outright — verified by passing one.

BL-04 — Publish the image that was tested, not a rebuild of it

Priority: high · Status: done · Milestone: -

The publish step runs build-push-action a second time. With a warm cache it usually produces the same bits — usually is not a guarantee, and the tests certify bits that may not be the ones pushed.

Done when: the image is built once and pushed by digest, or the workflow proves the two digests match before pushing.

✅ Done 2026-09-15. The publish step is docker tag plus docker push of the image the milestones and the end-to-end suite ran against; the second build-push-action is gone, and tests/policy.sh fails if one comes back.

BL-05 — Decide whether to carry the production plugin modifications forward

Priority: high · Status: open · Milestone: M1

Six files differ between reference/baseline-2.0.3/ and the upstream rocks: the replaced image carried locally modified oidc and jwt-keycloak plugins, with extra config fields (internal_request_headers, redirect_after_authentication_failed_uri, timeout). A configuration using any of them would be rejected by the new image.

Measured 2026-09-15, and one risk is worse than this item described. The modifications are: an oidc timeout field, jwt-keycloak's internal_request_headers and redirect_after_authentication_failed_uri — all of which make Kong refuse a configuration that uses them, loudly — and the oidc handler injecting X-Access-Token and X-ID-Token into every authenticated request, which the upstream rock does not. A service reading either header stops receiving it with no error anywhere.

scripts/dump-config.sh <kong-admin-url> <private-path> takes a redacted copy first — worth doing regardless, since the configuration exists in a database and nowhere else — and scripts/check-live-config.sh reads either that dump or a live Admin API to answer the configuration half. The header half has to be answered in the services: grep -ril 'X-ID-Token\|X-Access-Token'. M1 now compares file contents with the six differences declared, so a seventh cannot appear unnoticed.

Done when: the check has been run against each zone, the services have been grepped for those two headers, and the decision to port or drop each modification is recorded in docs/migrating.md.

BL-06 — Add OCI labels and build provenance

Priority: medium · Status: done · Milestone: -

No org.opencontainers.image.source/revision/version, so GHCR does not link the package to the repository and a digest does not say which commit produced it. No SBOM or provenance attestation either, on an image that sits on the authentication path.

Done when: labels are set from the build, and provenance/sbom are enabled on the publish step.

✅ Done 2026-09-15, in the part that was in reach. The image carries the OCI title, description, source, documentation, licence, revision, created and version labels plus the Kong line, with revision and version passed by CI — so a digest in a job spec traces back to a commit.

⚠️ Provenance and SBOM attestations are not added: they are produced by buildx --push, and this workflow deliberately pushes the image it tested instead of rebuilding it (BL-04). Having both needs a step that attests an already-pushed digest. Said here rather than quietly dropped.

BL-07 — Remove build tooling from the published image

Priority: medium · Status: dropped · Milestone: -

git, unzip and curl stay in the final image. Extra surface on an authentication gateway, for tools only the build needs.

Done when: they are purged in the same layer, or the plugins are installed in a builder stage.

⛔ Dropped 2026-09-15 — it cannot be done on this base. apt-get purge git unzip curl takes /usr/local/lib/luarocks with it: the whole rock tree, every plugin, leaving an image where kong version answers and not one plugin exists. With --auto-remove apt goes further still and judges the kong package itself orphaned. Measured on real builds, twice.

The extra surface is real and this is not a comfortable answer — but an image whose plugins have silently vanished is a worse one. The Dockerfile records it where somebody would otherwise try it again.

BL-08 — Warm up the oidcify plugin server on start

Priority: medium · Status: open · Milestone: -

Kong starts the Go plugin server lazily, so the first request after a restart can get a 500 while its socket does not exist yet. Brief, self-correcting, and visible to whoever arrives first.

Done when: whatever starts the container issues a warm-up request before traffic reaches it, or the behaviour is accepted in writing.

BL-09 — Scope packages: write to the job that publishes

Priority: medium · Status: done · Milestone: -

The permission is set workflow-wide, so the policy and e2e jobs inherit registry write access they never use.

Done when: the permission is declared on the build job only.

✅ Done 2026-09-15. The workflow-level grant is contents: read; packages: write belongs to the build job, contents: write to the release job. tests/policy.sh fails if a package permission reappears at workflow level.

BL-10 — Pin GitHub Actions by SHA

Priority: medium · Status: done · Milestone: -

Actions are pinned by major tag (@v4), while invariant 1 of AGENTS.md demands exact versions everywhere. The same class of mutable reference the repository warns about, applied to itself.

Done when: actions are pinned by commit SHA, with a policy for updating them.

✅ Done 2026-09-15. Every action in every workflow is pinned to a 40-character commit SHA with the version kept beside it as a comment, and .github/dependabot.yml bumps those pins weekly — pinning without a way to update trades a supply-chain risk for a staleness one and calls it progress. tests/policy.sh fails if any action returns to a moving reference, or if dependabot stops watching them.

BL-11 — Review oidcify's maintenance once a quarter

Priority: medium · Status: open · Milestone: -

One maintainer, 24 stars. Alive today is not safe forever, and the failure mode is silence: the project simply stops, as kong-oidc did for seven years before saying so.

Done when: a recurring check exists, and its result is recorded next to the M3 decision.

BL-12 — Exclude scripts/ and docs/ from the build context

Priority: low · Status: done · Milestone: -

.dockerignore does not list them, so they are shipped to the daemon on every build for no reason.

Done when: .dockerignore covers them.

✅ Done 2026-09-15, in the same change that closed BL-06 — and marked here only afterwards: the changelog said done while this file still said open, which is precisely the drift the roadmap generator exists to prevent and cannot catch, because it reads this file and believes it.

BL-13 — plugins/ exists only on disk

Priority: low · Status: done · Milestone: -

Git does not track empty directories, so the folder AGENTS.md describes is absent from a fresh clone.

Done when: a .gitkeep is added, or the reference is removed.

✅ Done 2026-09-15. plugins/.gitkeep: the folder now exists in a clone, not only on the author's disk.

Priority: low · Status: done · Milestone: -

Both are installed from third-party LuaRocks manifests (cdbattags, utix) without the provenance note the other plugins get.

Done when: docs/plugins.md states their origin and why those manifests.

✅ Done 2026-09-15. Each supporting rock has its publisher and its reason for being there, including that lua-resty-jwt comes from a fork because the original line is dormant.

BL-15 — CODEOWNERS and branch protection

Priority: low · Status: done · Milestone: -

Nothing records who reviews changes to a repository that publishes authentication images.

Done when: a CODEOWNERS file exists and the protection rules are described in AGENTS.md.

✅ Done 2026-09-15, by half. .github/CODEOWNERS records who reviews what, listing separately the paths where a mistake is published rather than merely committed.

⚠️ GitHub enforces it only when branch protection requires code-owner review. That is a repository setting, not a file — so the other half belongs to whoever administers the repository, and no commit here can supply it.