The first run of the images workflow on main (run 337) built the toolbox image, but the push failed: HEAD .../toolbox/blobs/sha256:5855... 403 Forbidden: Access denied.
Cause (measured read-only on k3s-prod, 2026-10-30)
The 403 comes from Cilium's Gateway Envoy, not from Gitea. Envoy is enforcing buildkitd's own egress policy, not Gitea's ingress.
Gitea's router log has no entry for the push, and no /v2/ request from buildkitd's IP at all. The CI job's own manifest checks, sent from the act-runner pod through the same Gateway, are logged.
An unauthenticated GET https://gitea.home.keithjackson.dev/v2/ returns 401 from Gitea when sent from act-runner (egress unrestricted). The same request from buildkitd returns 403, and Gitea never logs it. No token was involved, so this is not a package-permission problem.
Hubble logs it as buildkit/buildkitd (ingress) -> gateway/cilium-gateway-shared:443 http-request DROPPED, traffic_direction: EGRESS, with the response a 403 in 0 ms.
Gitea's ingress does not need the source namespace. gitea-actions is not in Gitea's ingress allows, yet its requests through the Gateway arrive. Gitea's BPF policy admits them as reserved:ingress.
Since Cilium 1.16.8/1.17.2 (CVE-2025-30162; prod runs 1.20.1), Gateway L7LB re-checks the source pod's egress against the backend identity. buildkitd's egress allowed only world and the ingress entity. Commit 68690e5 fixed the identical failure for the SSO apps' hop to Keycloak.
Change
platform/buildkit/networkpolicy.yaml: buildkitd-allow-egress adds toEndpoints gitea/app.kubernetes.io/name=gitea on TCP 3000.
platform/buildkit/README.md: the policy row now says the same.
decisions/2026-10-30-gitea-admit-buildkit.md: the handoff entry.
Not changed:catalog/gitea.yamlinClusterClients. Adding buildkit there would open the direct ClusterIP path from build code to Gitea and would not fix this.
Gate
Local toolbox gate, run from the worktree at d2c4bd5: rc 3. The only stage that did not run is gitea-settings (no SETTINGS_API_TOKEN locally). spec-immutability passed, with the spec repo mounted.
Not verified
That the 403 becomes a 401/200 once Flux applies this. That needs the policy live. Proving the push works is an operator step after merge (see the handoff entry).
## Problem
The first run of the `images` workflow on main (run 337) built the toolbox image, but the push failed:
`HEAD .../toolbox/blobs/sha256:5855... 403 Forbidden: Access denied`.
## Cause (measured read-only on k3s-prod, 2026-10-30)
The 403 comes from Cilium's Gateway Envoy, not from Gitea. Envoy is enforcing **buildkitd's own egress** policy, not Gitea's ingress.
- Gitea's router log has **no** entry for the push, and no `/v2/` request from buildkitd's IP at all. The CI job's own manifest checks, sent from the act-runner pod through the same Gateway, are logged.
- An unauthenticated `GET https://gitea.home.keithjackson.dev/v2/` returns **401** from Gitea when sent from act-runner (egress unrestricted). The same request from buildkitd returns **403**, and Gitea never logs it. No token was involved, so this is not a package-permission problem.
- Hubble logs it as `buildkit/buildkitd (ingress) -> gateway/cilium-gateway-shared:443 http-request DROPPED`, `traffic_direction: EGRESS`, with the response a `403` in 0 ms.
- Gitea's ingress does not need the source namespace. `gitea-actions` is not in Gitea's ingress allows, yet its requests through the Gateway arrive. Gitea's BPF policy admits them as `reserved:ingress`.
Since Cilium 1.16.8/1.17.2 (CVE-2025-30162; prod runs 1.20.1), Gateway L7LB re-checks the source pod's egress against the backend identity. buildkitd's egress allowed only `world` and the `ingress` entity. Commit 68690e5 fixed the identical failure for the SSO apps' hop to Keycloak.
## Change
- `platform/buildkit/networkpolicy.yaml`: `buildkitd-allow-egress` adds `toEndpoints` gitea/`app.kubernetes.io/name=gitea` on TCP 3000.
- `platform/buildkit/README.md`: the policy row now says the same.
- `decisions/2026-10-30-gitea-admit-buildkit.md`: the handoff entry.
**Not changed:** `catalog/gitea.yaml` `inClusterClients`. Adding buildkit there would open the direct ClusterIP path from build code to Gitea and would not fix this.
## Gate
Local toolbox gate, run from the worktree at d2c4bd5: **rc 3**. The only stage that did not run is `gitea-settings` (no `SETTINGS_API_TOKEN` locally). `spec-immutability` passed, with the spec repo mounted.
## Not verified
That the 403 becomes a 401/200 once Flux applies this. That needs the policy live. Proving the push works is an operator step after merge (see the handoff entry).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Images run 337's push failed with "403 Forbidden: Access denied". Measured
read-only: Gitea never logged the request; an unauthenticated GET /v2/ via the
Gateway gets 401 from Gitea when sent from act-runner (egress unrestricted) but
403 from buildkitd; Hubble logs it as an L7 http-request DROPPED with
traffic_direction EGRESS. Cilium >= 1.16.8 (CVE-2025-30162) re-checks the
source pod's egress against the proxied backend identity, and buildkitd's
egress only allowed world and the ingress entity. Same fix as 68690e5 for
Keycloak. Gitea's ingress already admits reserved:ingress and is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The first run of the
imagesworkflow on main (run 337) built the toolbox image, but the push failed:HEAD .../toolbox/blobs/sha256:5855... 403 Forbidden: Access denied.Cause (measured read-only on k3s-prod, 2026-10-30)
The 403 comes from Cilium's Gateway Envoy, not from Gitea. Envoy is enforcing buildkitd's own egress policy, not Gitea's ingress.
/v2/request from buildkitd's IP at all. The CI job's own manifest checks, sent from the act-runner pod through the same Gateway, are logged.GET https://gitea.home.keithjackson.dev/v2/returns 401 from Gitea when sent from act-runner (egress unrestricted). The same request from buildkitd returns 403, and Gitea never logs it. No token was involved, so this is not a package-permission problem.buildkit/buildkitd (ingress) -> gateway/cilium-gateway-shared:443 http-request DROPPED,traffic_direction: EGRESS, with the response a403in 0 ms.gitea-actionsis not in Gitea's ingress allows, yet its requests through the Gateway arrive. Gitea's BPF policy admits them asreserved:ingress.Since Cilium 1.16.8/1.17.2 (CVE-2025-30162; prod runs 1.20.1), Gateway L7LB re-checks the source pod's egress against the backend identity. buildkitd's egress allowed only
worldand theingressentity. Commit68690e5fixed the identical failure for the SSO apps' hop to Keycloak.Change
platform/buildkit/networkpolicy.yaml:buildkitd-allow-egressaddstoEndpointsgitea/app.kubernetes.io/name=giteaon TCP 3000.platform/buildkit/README.md: the policy row now says the same.decisions/2026-10-30-gitea-admit-buildkit.md: the handoff entry.Not changed:
catalog/gitea.yamlinClusterClients. Adding buildkit there would open the direct ClusterIP path from build code to Gitea and would not fix this.Gate
Local toolbox gate, run from the worktree at
d2c4bd5: rc 3. The only stage that did not run isgitea-settings(noSETTINGS_API_TOKENlocally).spec-immutabilitypassed, with the spec repo mounted.Not verified
That the 403 becomes a 401/200 once Flux applies this. That needs the policy live. Proving the push works is an operator step after merge (see the handoff entry).
🤖 Generated with Claude Code