key-cape/workplans/KEY-WP-0038-forgejo-oidc-client.md
tegwick 834b0d06a1 Review exact offline checker behavior for browser registration
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 22:08:05 +02:00

212 lines
9.9 KiB
Markdown

---
id: KEY-WP-0038
type: workplan
title: "Forgejo as a NetKingdom OIDC client, so the forge can close"
domain: infotech
repo: key-cape
status: active
flavor: implementation
owner: claude-code
topic_slug: railiance
created: "2026-10-08"
updated: "2026-10-16"
depends_on: []
related_repos:
- prj-forgejo-org-refactor
- railiance-apps
- railiance-platform
state_hub_workstream_id: "098e3e2c-a138-560e-b2d3-3a15eb49d8a9"
---
# KEY-WP-0038 - Forgejo as a NetKingdom OIDC client, so the forge can close
## Why
`forgejo.coulomb.social` is closing: private by default, no anonymous access
(prj-forgejo-org-refactor ORGREF-WP-0002, decision
`decisions/forge-visibility-policy.md`). The founder's condition is that Forgejo
sign-in runs through NetKingdom **before** anything closes, so that every human,
agent and service reader has a governed identity rather than an ad-hoc local
account. This plan is ORGREF-WP-0002-T04. Its Forgejo-side counterpart is
railiance-apps `RAPPS-WP-0016` (ORGREF-WP-0002-T05), which owns the Forgejo
Helm release.
## Boundaries
- Registration follows the existing human browser-client pattern
(`docs/approval-engine-provisioning-request.yaml`, `human_registration`) and the
client fields validated in KEY-WP-0028. Redirect URIs are exact, never
wildcards.
- The client secret goes to OpenBao through railiance-platform custody. No secret
value in git, State Hub, logs or chat.
- Founder guardrail for the whole closure: tighten gradually, never lock anyone
out, never risk assets. Nothing in this plan changes who can read the forge
today. It only makes the NetKingdom path available.
## T01 — Define what Forgejo receives
```task
id: KEY-WP-0038-T01
status: done
priority: high
state_hub_task_id: "5f019d13-1fa3-5b88-a27a-75a3339ca96e"
```
Write the claim contract Forgejo consumes, as a non-secret request packet under
`docs/`:
- issuer `https://kc.coulomb.social`; confidential client `forgejo`; authorization
code with PKCE; scopes `openid profile email groups`
- redirect URI
`https://forgejo.coulomb.social/user/oauth2/netkingdom/callback`. The
authentication-source name `netkingdom` must match railiance-apps'
configuration exactly.
- subject and username: canonical directory subject, plus a stable
`preferred_username` that Forgejo can match to the existing local accounts
(founder and agent accounts) for linking
- `groups` naming the Forgejo organisations and teams to map to (for example
`forge:coulomb:owners`, `forge:coulomb:readers`), and `principal_type` so that
human and service principals stay distinguishable
- MFA: forge administrators and owners require assurance with a fresh second
factor; plain readers follow the client-aware MFA policy (KEY-WP-0008)
Acceptance: the founder accepts the packet. The group names are agreed with
railiance-apps.
2026-10-08: packet drafted at `docs/forgejo-oidc-client-request.yaml`. It covers a
confidential client `forgejo` with PKCE and `mfaRequired: true`, the exact
callback, scopes, a custody path, the claims as emitted at this revision, new
LLDAP groups `forge-admins`, `forge-coulomb-owners` and `forge-coulomb-readers`
with a required-claim gate, no automatic account creation, and linking of local
accounts. It also proposes that agents and services use scoped tokens bound to
service identities, not browser sign-in.
2026-10-08: accepted by the founder (in session). railiance-apps' RAPPS-WP-0016
uses the same authentication-source name `netkingdom` and the same group names,
so the agreement condition holds. Before T02, the LLDAP groups `forge-admins`,
`forge-coulomb-owners` and `forge-coulomb-readers` must exist, with the founder
in the first two.
## T02 — Register the client and hand over its secret
```task
id: KEY-WP-0038-T02
status: wait
depends_on: [KEY-WP-0038-T01, NK-WP-0048-T06]
blocking_reason: "Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout."
priority: high
state_hub_task_id: "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
```
**Changed 2026-10-08 (founder direction):** no case-specific helper script.
Registration is the first case of the SecurityCaseType `oidc-browser-relying-party`
(net-kingdom NK-WP-0048, canon `security-case-types_v0.1`). The parameters
accepted in T01 become the case; ops-mason materializes it (MASON-WP-0010) and
ops-warden follows through (WARDEN-WP-0044). The registration capability is
net-kingdom's (`net-kingdom.keycape-client-registration`); key-cape's part is its
field rules and reviewing the type's composition.
The original intent below still describes what must result.
Register `forgejo` from the packet. Mint the client secret straight into custody
(OpenBao path `platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, derived
by the case type since net-kingdom 074b3a0; custody owner railiance-platform,
consumers KeyCape via ESO and Forgejo). Record a receipt with no
secret value.
Acceptance: discovery metadata lists the client's redirect URI. The secret is
readable by Forgejo's custody path only.
2026-10-10: key-cape reviewed the type's composition
(`docs/oidc-browser-relying-party-type-review.md`). net-kingdom fixed the findings
in `61ab6bc`, a 1.0 patch: redirects with `?` or `#` refused, the groups scope
required, and descriptions corrected. A new 1.1 (`e2174ce`, founder go) adds the
`consumer-enforces-groups` verification. For Forgejo, that is the required-claim
setting railiance-apps configures. `security-cases/forgejo-netkingdom-oidc.yaml`
is re-pinned to @1.1, with parameters unchanged. KeyCape added `keycape
check-config` (`c528daa`), which register-client runs before the restart. T02 still
waits on NK-WP-0048-T06, which needs both executors' light paths (MASON-WP-0010-T03,
WARDEN-WP-0044-T03).
### Exact offline checker owner review — 2026-10-16
KeyCape owner review under KEY-WP-0038-T02 endorses only the offline config
validation behavior of source `3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`,
`src` tree `f0010b9cab17dbc3a787628a29657d98b900320f`, compiled artifact
`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
The exact source/archive, 108 source files, 9876 cached Go 1.23.0 toolchain
files and all ten selected module trees matched the candidate closure. Fourteen
actual artifact checks and the cmd/config/policy Go tests passed using only
synthetic inputs. The check dispatches before private-key loading, upstream
adapter construction or server startup and uses placeholder client secrets.
[Owner review](../docs/offline-checker-owner-review-2026-10-16.md) records the
exact evidence and limits: non-secret portal/logout URL environment overlays,
optional/nonempty env-name assertion and policy-store assertion, permissive YAML
loading, and no key/custody/readiness or authentication proof. It does not endorse
the caller's inherited environment, executable storage/custody, the NetKingdom
descriptor/type, Warden admission or any browser/native identity. Parent review
identified caller environment inheritance; NetKingdom owns its correction and
regression checks. T02 remains `wait`; T03 and all live acceptance gates remain
unchanged. No live config, credentials, cluster objects or paid calls were used.
## T03 — Prove the claims for each kind of identity
```task
id: KEY-WP-0038-T03
status: wait
depends_on: [KEY-WP-0038-T02]
blocking_reason: "Claims are verified against the registered client."
priority: high
state_hub_task_id: "40ddaa29-7245-508c-ad48-ddc063e25115"
```
Before Forgejo uses the client, verify the issued ID token and UserInfo for one
human (the founder, with MFA), one agent identity and one service identity:
subject, `preferred_username`, `groups`, `principal_type`, assurance. Record the
claims, minus tokens, as evidence.
Decide with railiance-apps whether agents and services sign in to Forgejo at
all, or receive scoped Forgejo tokens bound to a NetKingdom service identity
(ORGREF-WP-0002-T06). Record the decision.
Acceptance: evidence for all three principals; the agent and service decision is
recorded. Then hand over to RAPPS-WP-0016.
## T04 — Implement the `keycape-client` entry point
```task
id: KEY-WP-0038-T04
status: cancel
priority: high
state_hub_task_id: "583cf1f1-16dc-515f-83bc-1efc45024a89"
```
**Cancelled 2026-10-08: moved to net-kingdom NK-WP-0048-T09 (done).** The live
registration machinery (contained kubectl, cluster and issuer pins) already lives
in net-kingdom `sso-mfa/k8s/keycape/`, where KeyCape is deployed; a copy here
would drift. The entry point is `net-kingdom/sso-mfa/k8s/keycape/keycape-client.py`
and the declaration is `net-kingdom.keycape-client-registration`; key-cape's draft
declaration is removed. key-cape still owns the client field rules (canonical
model) and their enforcement at authorization time.
`capabilities/playbooks/key-cape.oidc-client-registration.yaml` (contract PASS,
`maturity: planned`) declares client registration as a playbook capability, so
that the SecurityCaseType `oidc-browser-relying-party` (net-kingdom NK-WP-0048)
can compose it. Implement the entry point it names, `tools/keycape-client/`:
- `keycape-client register --client-file <rendered-client.yaml>` with modes
`inspect`, `dry-run`, `apply` and `remove`; adds, updates or removes exactly one
client in the live registry; idempotent; never prints or writes secret values;
a confidential client's secret is read from its custody reference
- one implementation replacing the per-client helpers in net-kingdom
`sso-mfa/k8s/keycape/` (register-coulomb-social, portal-client-rollout,
vergabe-client-rollout, secrets-engine-openbao-client-rollout), keeping their
safeguards (byte-preserving merge, compare-and-swap, cluster pin, safe receipts)
and their tests as regression cases
- client field validation by the KEY-WP-0028 rules
- then set the declaration's `maturity` to `implemented`
Longer term, loading clients from a non-secret registry with secrets referenced
from custody would remove the type's only attended step (see the type README,
"Attended steps").