feat(oci): make registry verifiable against private Forgejo (BUNYIP-31) #27

Merged
nrupard merged 3 commits from feat/bunyip-31-oci-private-forgejo into main 2026-06-02 15:36:49 +02:00
Owner

What

Phase A of BUNYIP-31 (verify the OCI registry proxy against private Forgejo): the code-level gaps that block verification, plus the dev wiring needed to run it. Phase B (the live docker login / pull verification) follows on this branch once a Forgejo service token is available; results land in the runbook's findings log.

Gaps fixed

  • WWW-Authenticate realm was hardcoded to https://{service}/auth/token - docker login could never work for any deployment without TLS on the service hostname, including local verification. New OCI_REGISTRY_REALM env var overrides the full realm URL; the default is unchanged, so production behind TLS needs nothing.
  • Upstream 401/403 was indistinguishable from any other upstream failure. When Forgejo rejects the service token (invalid, or missing read:package), the manifest path now logs an exact operator diagnostic; members still get the generic OCI error envelope.

Wiring + docs

  • compose.dev.yml: distribution env vars (FORGEJO_, OCI_REGISTRY_), port 18081 published, dev-prefixed cache volumes. All disabled until .env provides credentials.
  • .env.example: distribution section documenting the required Forgejo service-token scopes (read:package, read:repository).
  • dev-docs/oci-registry-verification.md: the verification runbook - prerequisites, step-by-step local procedure covering every BUNYIP-31 acceptance criterion (login, entitled pull, pinned-tag enforcement, blob-cache hit, denial envelopes, rate limits), a findings log, and production notes feeding BUNYIP-32. Includes the known caveat that the dunite-oci Forgejo client authenticates with an empty basic-auth username, which live verification must confirm Forgejo accepts.

Verification

Workspace check green; config + bunyip-oci tests pass; zero new clippy violations vs main; changed files rustfmt-clean; compose.dev.yml validates. (The unrelated download_config_enabled_when_forgejo_set test failure is the pre-existing BUNYIP-36 env-var race, reproducible on main.)

## What Phase A of **BUNYIP-31** (verify the OCI registry proxy against private Forgejo): the code-level gaps that block verification, plus the dev wiring needed to run it. Phase B (the live docker login / pull verification) follows on this branch once a Forgejo service token is available; results land in the runbook's findings log. ## Gaps fixed * **WWW-Authenticate realm was hardcoded to `https://{service}/auth/token`** - docker login could never work for any deployment without TLS on the service hostname, including local verification. New `OCI_REGISTRY_REALM` env var overrides the full realm URL; the default is unchanged, so production behind TLS needs nothing. * **Upstream 401/403 was indistinguishable from any other upstream failure.** When Forgejo rejects the service token (invalid, or missing `read:package`), the manifest path now logs an exact operator diagnostic; members still get the generic OCI error envelope. ## Wiring + docs * `compose.dev.yml`: distribution env vars (FORGEJO_*, OCI_REGISTRY_*), port 18081 published, dev-prefixed cache volumes. All disabled until `.env` provides credentials. * `.env.example`: distribution section documenting the required Forgejo service-token scopes (`read:package`, `read:repository`). * `dev-docs/oci-registry-verification.md`: the verification runbook - prerequisites, step-by-step local procedure covering every BUNYIP-31 acceptance criterion (login, entitled pull, pinned-tag enforcement, blob-cache hit, denial envelopes, rate limits), a findings log, and production notes feeding BUNYIP-32. Includes the known caveat that the dunite-oci Forgejo client authenticates with an empty basic-auth username, which live verification must confirm Forgejo accepts. ## Verification Workspace check green; config + bunyip-oci tests pass; zero new clippy violations vs main; changed files rustfmt-clean; compose.dev.yml validates. (The unrelated `download_config_enabled_when_forgejo_set` test failure is the pre-existing BUNYIP-36 env-var race, reproducible on main.)
feat(oci): make registry verifiable against private Forgejo (BUNYIP-31)
Some checks failed
Check / fmt / clippy / build / test (pull_request) Failing after 4s
9b2df2d27e
Code-level gaps found while preparing to verify the OCI proxy against the private Forgejo instance, plus the dev wiring needed to run the verification at all.

- WWW-Authenticate realm is now configurable (OCI_REGISTRY_REALM): the realm hardcoded https://{service}/auth/token, which made docker login impossible for any deployment not serving TLS on the service hostname, including local verification on http://localhost:18081. Default behavior is unchanged.
- Upstream 401/403 from Forgejo (invalid service token or missing read:package scope) now logs a precise operator diagnostic in the manifest path instead of being indistinguishable from any other upstream error; members still receive the generic OCI upstream envelope.
- compose.dev.yml wires the distribution env (FORGEJO_BASE_URL, FORGEJO_API_TOKEN, OCI_REGISTRY_*), publishes port 18081, and adds dev-prefixed cache volumes (dev-bunyip-oci-cache, dev-bunyip-download-cache) per the Docker naming convention. Everything stays disabled until .env provides Forgejo credentials.
- .env.example documents the distribution section, including the required Forgejo service-token scopes (read:package for OCI/generic packages, read:repository for release downloads).
- dev-docs/oci-registry-verification.md is the verification runbook: prerequisites, step-by-step local procedure (docker login, entitled/denied pulls, blob-cache hit, rate limits), a findings log to fill during live verification, and production notes feeding BUNYIP-32 (registry subdomain realm/service requirements, known empty-username basic-auth caveat in the Forgejo client).
- New self-contained unit test for the realm default/override (struct-literal based, no env vars, avoiding the BUNYIP-36 race class).

Verification: workspace check green; config + bunyip-oci tests pass (the unrelated download_config_enabled_when_forgejo_set failure is the pre-existing BUNYIP-36 env-var race, reproducible on main); zero new clippy violations vs main; changed files rustfmt-clean; compose.dev.yml validates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(oci): writable cache volumes + live verification results (BUNYIP-31 Phase B)
Some checks failed
Check / fmt / clippy / build / test (pull_request) Failing after 15s
824769bafd
Live verification against dev.a8n.run (psa-systems-private/bunyip-api:v0.1.1) on the dev stack. Every BUNYIP-31 acceptance criterion passes: docker login via the token endpoint, entitled pull of the pinned tag (multi-arch index -> child manifests -> blobs), blob cache hit on the second pull (rows touched, not re-fetched), MANIFEST_UNKNOWN / NAME_UNKNOWN / unauthorized envelopes for wrong tag / unknown repo / bad credentials / non-member, and the full OCI audit trail. Forgejo accepts the engine's empty-username basic auth for both manifests and blobs, resolving the open caveat.

One real bug found and fixed: fresh named volumes mounted at /var/cache/bunyip-oci and /var/cache/bunyip-downloads are created root-owned while the api container runs as a non-root user, so every blob write failed with Permission denied and docker saw 502 on all blobs (memory-cached manifests still worked, which made the failure look like an upstream auth problem). Both the dev and production api Dockerfiles now pre-create the cache dirs with correct ownership so first-mount volume initialization inherits it.

The runbook findings log records the full matrix, the bug, and two follow-ups discovered along the way: dunite-oci's blob-path error flattening hid the Permission denied entirely (PSA-35), and the bunyip-web dev container crash-loops because bun is installed under /root in the builder image (pre-existing, separate issue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(oci): address code-review findings on the registry verification PR
Some checks failed
Check / fmt / clippy / build / test (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
1de2587036
Fixes from a 10-finding review of PR #27, re-verified live with the new automated check (just verify-oci passes end to end against the running dev stack).

- .env.example no longer ships OCI_REGISTRY_SERVICE/OCI_REGISTRY_REALM uncommented with localhost values: a production .env copied from the example would have advertised a localhost token realm and broken every docker login. Both are now commented documentation; dev defaults come from compose, production derives the realm from the service hostname in code.
- compose.dev.yml derives the advertised service name and token realm from BUNYIP_OCI_PORT (nested interpolation, validated with docker compose config), so overriding the published port no longer desynchronizes the realm and breaks docker login. The port literal coupling across four locations is gone.
- OciConfig::validate() runs at startup when the registry is enabled: a realm that is not a valid URL or contains quotes/control characters (which would corrupt or drop the WWW-Authenticate header) is now a fail-fast startup error instead of an opaque client-side login failure; a realm host that differs from the service host logs a warning. The middleware also logs loudly instead of silently skipping the header if HeaderValue construction ever fails.
- The blob path now logs the flattened upstream/filesystem cause at error level (with the FORGEJO_API_TOKEN scope hint) instead of recording it only in the audit table; this is the blind spot that cost a debugging round-trip during Phase B verification. Typed blob-path errors remain tracked in PSA-35.
- compose.yml carries the named-volume requirement for the distribution caches as an inline comment where the BUNYIP-32 implementer will see it (bind mounts do not inherit image ownership and reproduce the Permission-denied 502s in production).
- compose.dev.yml volume keys follow the file's existing short-key convention (oci-cache, download-cache); names keep the dev-bunyip- prefix.
- Forgejo token-scope documentation deduplicated: .env.example points at the runbook (single source).
- The runbook's cleanup step references the recipe that actually exists (just dev-stop, not dev-down).
- New `just verify-oci` recipe automates the verification matrix (auth challenge, login, entitled pull, pinned-tag enforcement, blob-cache second pull) so the runbook results cannot silently rot; the runbook now points at it and keeps the manual steps for the cases the script does not cover.
- config.rs realm tests rewritten around a small struct-literal helper, adding validate() coverage (sane configs accepted, malformed realms rejected), still entirely env-var free per the BUNYIP-36 race avoidance.

Verification: workspace check green; config (18) + bunyip-oci (7) tests pass; zero new clippy violations vs main; changed files rustfmt-clean; both compose files validate; justfile parses; `just verify-oci` passes live against the dev stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/bunyip-31-oci-private-forgejo 2026-06-02 15:36:49 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/bunyip!27
No description provided.