docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
733a35025f
commit
423b815782
50 changed files with 6806 additions and 0 deletions
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
|
||||
"id": "165e29df-5a58-4314-9ec7-f8420eff72f1",
|
||||
"topic_id": null,
|
||||
"task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
|
||||
"decision_id": null,
|
||||
"event_type": "note",
|
||||
"summary": "Published one KEY-WP-0039 T02 Custodian coordinator finalization-review paragraph in exact owner commit40de616e078906e04c692edf7cb7a8bfbc07b5ea. All3taskblocks/frontmatter/UUIDs/dependencies unchanged: T01done,T02wait,T03done; parentactive. Existing source acceptance/registration go/standing attended rule preserved; existing KEY38 then coding-client admission/readiness then attended negative/revoke/cleanup sequence retained, no native acceptance inferred. Required consistency passed; restored generated brief writeback. Normal rmgr sync refused unrelated grandfathered ADHOC-2026-09-27-T02 UUID; no historical identity mutation. Canonical primary railiance01 receiver applied exact published commit with acknowledge_retirements=false; forge, task bindings and clean owner index/worktree independently verified. No credential/native/registration action. Evidence docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/.",
|
||||
"detail": null,
|
||||
"author": "codex",
|
||||
"session_id": null,
|
||||
"created_at": "2026-10-17T11:32:33.188005Z",
|
||||
"workstream_id": "09c3bddf-6058-5603-8488-cccdb7be4f86"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"summary": "Published one KEY-WP-0039 T02 Custodian coordinator finalization-review paragraph in exact owner commit40de616e078906e04c692edf7cb7a8bfbc07b5ea. All3taskblocks/frontmatter/UUIDs/dependencies unchanged: T01done,T02wait,T03done; parentactive. Existing source acceptance/registration go/standing attended rule preserved; existing KEY38 then coding-client admission/readiness then attended negative/revoke/cleanup sequence retained, no native acceptance inferred. Required consistency passed; restored generated brief writeback. Normal rmgr sync refused unrelated grandfathered ADHOC-2026-09-27-T02 UUID; no historical identity mutation. Canonical primary railiance01 receiver applied exact published commit with acknowledge_retirements=false; forge, task bindings and clean owner index/worktree independently verified. No credential/native/registration action. Evidence docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/.",
|
||||
"event_type": "note",
|
||||
"author": "codex",
|
||||
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
|
||||
"task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json,hashlib,stat
|
||||
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1');backup=out/'consistency-backup';backup.mkdir()
|
||||
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
|
||||
head=git('rev-parse','HEAD').strip();assert head.startswith('40de616')
|
||||
assert git('diff-tree','--no-commit-id','--name-only','-r',head).splitlines()==['workplans/KEY-WP-0039-openbao-negative-probe-tokens.md']
|
||||
assert not git('status','--porcelain=v1')
|
||||
paths=sorted({p.relative_to(root).as_posix() for p in (root/'workplans').rglob('*.md')}|{n for n in ('.custodian-brief.md','WORK-RECORDS.md') if (root/n).exists()})
|
||||
original={}
|
||||
for rel in paths:
|
||||
p=root/rel;assert p.is_file() and not p.is_symlink()
|
||||
dest=backup/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(p.read_bytes())
|
||||
original[rel]={'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'mode':stat.S_IMODE(p.stat().st_mode),'index':git('ls-files','--stage','--',rel)}
|
||||
(backup/'originals.json').write_text(json.dumps(original,indent=2)+'\n')
|
||||
results=[];changed=[]
|
||||
try:
|
||||
for label,argv in [('consistency',['statehub','fix-consistency','--repo','key-cape','--json']),('normal-sync',['/home/worsch/repo-manager/.venv/bin/rmgr','sync','--path',str(root),'--push'])]:
|
||||
cp=subprocess.run(argv,cwd=root,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=240)
|
||||
(out/(label+'.stdout.json')).write_bytes(cp.stdout);(out/(label+'.stderr.txt')).write_bytes(cp.stderr)
|
||||
results.append({'command':argv,'exit_code':cp.returncode,'label':label})
|
||||
finally:
|
||||
assert git('rev-parse','HEAD').strip()==head,'unexpected commit; stop for review'
|
||||
for rel,b in original.items():
|
||||
p=root/rel
|
||||
if not p.exists() or hashlib.sha256(p.read_bytes()).hexdigest()!=b['sha256']:
|
||||
changed.append(rel);p.write_bytes((backup/rel).read_bytes());p.chmod(b['mode'])
|
||||
current=git('ls-files','--stage','--',rel)
|
||||
if current!=b['index']:
|
||||
assert b['index'];mode,oid,stage,path=b['index'].split();assert stage=='0'
|
||||
git('update-index','--cacheinfo',mode,oid,path)
|
||||
assert git('ls-files','--stage','--',rel)==b['index']
|
||||
assert hashlib.sha256(p.read_bytes()).hexdigest()==b['sha256']
|
||||
assert not git('status','--porcelain=v1'),git('status','--porcelain=v1')
|
||||
receipt={'published_candidate_commit':head,'normal_commands':results,'unrelated_writeback_restored':changed,'all_original_index_worktree_fields_preserved':True,'one_path_commit':True,'normal_hooks':'changed-staged-blobs scanner8.30.1 clean','native_or_credential_actions':0}
|
||||
(out/'normal-publication.json').write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
print(json.dumps(receipt))
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
---
|
||||
id: KEY-WP-0039
|
||||
type: workplan
|
||||
title: "Negative-check tokens for the secrets-engine OpenBao JWT role"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-10"
|
||||
updated: "2026-10-17"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- railiance-platform
|
||||
- secrets-engine
|
||||
state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86"
|
||||
---
|
||||
|
||||
# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role
|
||||
|
||||
## Why
|
||||
|
||||
CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role
|
||||
`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong
|
||||
`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT
|
||||
and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine
|
||||
`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked
|
||||
whether KeyCape can issue the negative tokens without touching the
|
||||
`secrets-engine` registration.
|
||||
|
||||
KeyCape signs only what a registration declares. It has no way to mint a token
|
||||
with arbitrary claims, and adding one would create a forging path in the issuer.
|
||||
So the negative tokens must come from real, separate registrations that differ
|
||||
from `secrets-engine` in exactly one property each.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- The `secrets-engine` registration is not changed.
|
||||
- Probe clients are confidential `client_credentials` clients with the minimum
|
||||
60-second lifetime, scopes that grant nothing, and secrets minted into custody
|
||||
like any other client. They are removed after the proof.
|
||||
- The probe script prints only accept or refuse booleans per case, never tokens or
|
||||
secrets. It runs attended.
|
||||
|
||||
## T01 — Define the probe registrations
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8"
|
||||
```
|
||||
|
||||
Write a non-secret request packet under `docs/` with one probe client per claim
|
||||
mismatch, each identical to `secrets-engine` except for:
|
||||
|
||||
- `aud`: a different access-token resource audience
|
||||
- `sub`: a different service subject
|
||||
- `tenant`: a tenant other than `platform`
|
||||
- `roles`: a role other than `secrets-engine`
|
||||
- `scope`: a scope outside the role's bound scope
|
||||
|
||||
Two cases need no probe client. A service token always carries
|
||||
`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong
|
||||
`principal_type` case uses a human access token from an attended login. The
|
||||
coding-agent case uses the existing `codex-railiance-platform` registration (role
|
||||
`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live;
|
||||
T01 confirms that.
|
||||
|
||||
The expired-JWT case reuses any probe client: request a token, wait past its
|
||||
60-second lifetime, then present it.
|
||||
|
||||
Acceptance: railiance-platform agrees the packet covers every refusal that
|
||||
CCR-2026-0038 names.
|
||||
|
||||
2026-10-10: railiance-platform agreed that the case list covers every
|
||||
CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no
|
||||
provider fallback) are theirs and secrets-engine's. Packet written at
|
||||
`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two
|
||||
ways, both sent back for re-agreement:
|
||||
|
||||
- **Expired case.** A token from another probe already fails a claim, so its
|
||||
refusal does not show that expiry was checked. It needs a sixth client,
|
||||
`openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That
|
||||
makes it a second credential able to log in as secrets-engine for the session.
|
||||
OpenBao's `expiration_leeway` (150s by default) also extends the wait.
|
||||
- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is
|
||||
set. Every probe except the aud probe therefore pins
|
||||
`audience: secrets-engine-openbao`, so it fails only on its own claim.
|
||||
|
||||
Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's
|
||||
environment, because KeyCape resolves `secretRef` at startup. The packet uses a
|
||||
transient Kubernetes Secret that is deleted in the same session.
|
||||
|
||||
## T02 — Register the probe clients, run the attended probe, then remove them
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T02
|
||||
status: wait
|
||||
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
|
||||
blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved."
|
||||
priority: medium
|
||||
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
|
||||
```
|
||||
|
||||
2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md.
|
||||
|
||||
Write the attended script, which prints booleans only.
|
||||
|
||||
2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in
|
||||
`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused
|
||||
boolean per case plus the refusal class (`claim_mismatch`, `expired`,
|
||||
`not_service_principal`), and no token, secret, claim value or OpenBao error text.
|
||||
A transport failure or an unexpected response is reported as `inconclusive`, never
|
||||
as a refusal. A token that OpenBao accepts is revoked at once and fails the run.
|
||||
Still waiting: railiance-platform's re-agreement on the two departures above, and
|
||||
an attended registry session with net-kingdom. Register the clients
|
||||
through the normal registration path, hand the run to railiance-platform, record
|
||||
a value-free receipt in `docs/evidence/`, and deregister the clients afterwards.
|
||||
|
||||
Acceptance: every case is refused, the receipt is committed, and the probe clients
|
||||
are gone.
|
||||
|
||||
|
||||
2026-10-11 source preparation: T01 remains progress because the exact expiry
|
||||
client and audience-pin amendments require platform-owner re-agreement; the
|
||||
prior agreement covered the earlier case list, not this revised packet. The
|
||||
prepared runner revision adds a valid expiry positive control and immediate
|
||||
revoke, exact decoded claim-isolation preflight (not signature evidence),
|
||||
semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen
|
||||
local tests pass; original-source semantic regressions demonstrate two false
|
||||
successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner
|
||||
returns, live authority, effective leeway, custody boundaries and cleanup. No
|
||||
live token, registry or OpenBao action occurred. T02 continues to wait for owner
|
||||
re-agreement plus separate attended live authorization. MASON-WP-0009-T05
|
||||
operator/ESO capability and nonmember checks remain independent acceptance.
|
||||
|
||||
2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans.
|
||||
|
||||
|
||||
2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner
|
||||
acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md)
|
||||
confirms all 14 executable/test/producer-decision pins and three Platform source
|
||||
pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing
|
||||
source acceptance, resolved registration go caafe4be and the standing attended
|
||||
rule remain valid; no new protocol or blanket approval request is needed. The
|
||||
remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof,
|
||||
admission of the existing coding-client proposal with sanitized owner readiness
|
||||
returns, then one attended negative/revoke/cleanup receipt committed with probe
|
||||
clients removed. No native acceptance or credential/registration action is
|
||||
inferred from this coordinator review.
|
||||
|
||||
|
||||
## KeyCape issuer-source compatibility inspection for lifecycle v2
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
|
||||
```
|
||||
|
||||
2026-10-11: Completed KeyCape's own issuer-source inspection for
|
||||
KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256
|
||||
6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed.
|
||||
Read AGENTS.md, governing INTENT.md, current client-credentials claim source,
|
||||
registration fragments, native caller-authentication commands and frozen v2
|
||||
issuer transport/probe wiring. Seventeen unchanged runner tests pass locally.
|
||||
This task completes only issuer-source inspection; the resulting narrow
|
||||
source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment).
|
||||
T01 stays done and T02 stays open for actual native registration, token input,
|
||||
refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner
|
||||
checkout was changed during this source inspection. Owner source files were
|
||||
read for the bounded issuer compatibility check.
|
||||
|
||||
Current token.go:306-309 returns HTTP 401 invalid_profile_usage with
|
||||
feature=client_id for a removed confidential client; wrong-secret
|
||||
feature=Authorization cannot count as removed-registration evidence. The
|
||||
proposal and packet comments are narrowly corrected against that current
|
||||
issuer contract. Prior T01/source re-agreement history is preserved; this source
|
||||
acknowledgment supplies no NetKingdom runtime-owner review or founder live go.
|
||||
|
||||
The private token-producer handoff remains a real execution gap under T02.
|
||||
Existing keycape login/service-token commands require a new private output
|
||||
file; login also needs a concrete admitted public PKCE client/callback and
|
||||
attended browser/MFA. The lifecycle expects an in-memory private stdin object
|
||||
containing human_token and coding_agent_token, but no reviewed file-to-process
|
||||
or process-to-process producer, admitted human client/callback, custody-injected
|
||||
coding-agent source, native placement or producer cleanup receipt has been
|
||||
provided. No unattended token producer or admitted secret route is invented.
|
||||
Resolve those exact owner contracts before founder native approval; existing
|
||||
static registration is not live token custody or producer admission.
|
||||
|
||||
|
||||
2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed
|
||||
KeyCape issuer-facing inspection before the new formal acknowledgment in
|
||||
docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4
|
||||
manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8,
|
||||
executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d.
|
||||
NetKingdom's independent V4 source review is approved, receipt SHA256
|
||||
4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb.
|
||||
The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain
|
||||
retained as one correction trial; they are not extra scored successes.
|
||||
|
||||
V4 retains the unchanged issuer runner and six-client claim design, and fixes
|
||||
finite/nonboolean windows, true total request deadlines, late-known OpenBao
|
||||
session private revoke/refusal and guarded bounded final Secret deletion.
|
||||
The issuer-facing acknowledgement covers only stopping late token exchanges,
|
||||
claim source/shape and preserving token cleanup failure/unknown outcomes; it
|
||||
does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41
|
||||
supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass
|
||||
locally. No native tokens, credentials, issuer mutations or live cleanup were
|
||||
performed. T01 stays done; T02 stays open and the genuine private token-producer
|
||||
handoff remains its next source prerequisite before actual founder native go.
|
||||
|
||||
|
||||
2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed
|
||||
KeyCape's own current issuer-source/claim/cleanup-contract inspection before
|
||||
any new formal acknowledgment. Current token.go:306-309 still distinguishes
|
||||
removed-client invalid_profile_usage feature=client_id from wrong-secret
|
||||
feature=Authorization. The six-client design, audience source and expiry
|
||||
positive control remain unchanged, and the original runner's 17 tests pass.
|
||||
Exact V5 manifest SHA256
|
||||
d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285;
|
||||
executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360.
|
||||
V5 has 46 passing supplied lifecycle checks. It adds a single exceptional
|
||||
response-close abort under a fixed20ms cap clipped to nonresettable aggregate
|
||||
cleanup, and to positive original request time remaining, with expired aggregate
|
||||
zero calls, fixed private errors and truthful known-session revoke/unknown
|
||||
outcomes. This inspection covers issuer-facing token lifetime and claim
|
||||
interfaces only; NetKingdom and Platform independently own their runtime and
|
||||
backend review decisions. Previous V2/V3 Net source refusals and Platform V4
|
||||
exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are
|
||||
historical narrow source inspections, never aggregate native approval.
|
||||
T01 remains done, T02 open. No native actions or credentials occurred. The real
|
||||
private token-producer/client/callback/coding-agent custody/cleanup handoff gap
|
||||
stays the next prerequisite; no unattended admission is inferred.
|
||||
|
||||
Exact independent V5 source decisions were then verified: NetKingdom approved
|
||||
receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc
|
||||
(46 supplied +17 runner +21 independent checks); Platform approved receipt
|
||||
SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
|
||||
(46 supplied +5 independent +17 previously verified runner checks). Those are
|
||||
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
|
||||
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
|
||||
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
|
||||
|
||||
|
||||
2026-10-11 T02 continuing source preparation: private producer decision packet
|
||||
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
|
||||
flows, selected existing netkingdom-bootstrap-console literal-loopback human
|
||||
client subject to owner purpose/MFA/listener admission, and the existing
|
||||
codex-railiance-platform source identity subject to actual live/custody/consumer
|
||||
admission. No existing coding-agent delivery receipt was found; AppRole tokens
|
||||
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
|
||||
CLI's two caller-owned private files has 18 passing synthetic checks, with
|
||||
identity-guarded explicit file consumption and fixed value-free failure output.
|
||||
Decoded claim shape does not prove signatures or authenticated admission.
|
||||
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
|
||||
choices and missing receipts. The exact trusted workstation-to-node launcher,
|
||||
private-file allowance and railiance01 Bao route/public-health guard still need
|
||||
source review and founder disposition before any token/temporary credential/
|
||||
mutation/restart. No registration, custody, login, native action or credential
|
||||
request occurred. V5 and failed review histories retained; T01 stays done and
|
||||
T02 open. No new work record or native acceptance is inferred.
|
||||
|
||||
|
||||
2026-10-11 same T02 source-preparation correction for
|
||||
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
|
||||
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
|
||||
V2 has source-pinned private launch/ready handshake compatible with exact
|
||||
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
|
||||
ANY caller-token file load/consume/write; separate local data/control pipes
|
||||
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
|
||||
only clips downward after readiness. 31 distinct producer/launcher synthetic
|
||||
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
|
||||
credential access was performed. Stat/unlink remains non-atomic and requires
|
||||
continuously exclusive sameUID custody; the retained race test is a custody
|
||||
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
|
||||
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
|
||||
trusted launcher/directSSH profile/private tmpfs allowance, selected human
|
||||
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
|
||||
unadmitted owner choices. Native node route, role/leeway/window/founder decision
|
||||
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
|
||||
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
|
||||
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
|
||||
now propagates into sticky remote consumer uncertainty before launcher early
|
||||
return; local SSH termination never proves remote no-effects/recovery. Actual
|
||||
full-payload then interrupted confirmation and4096-byte partialpipe failures
|
||||
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
|
||||
bytes unchanged; new source reviews required, not implicit repin. Native
|
||||
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
|
||||
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
|
||||
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
|
||||
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
|
||||
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
|
||||
a renewed launcher allowance. 40 own source tests plus9 retained paired
|
||||
wrapper/child handshake regressions pass with synthetic/no native transport.
|
||||
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
|
||||
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
|
||||
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
|
||||
credentials/native/owner checkout changes; no task/plan duplication. T01done
|
||||
and T02open; source correction is not native acceptance.
|
||||
|
||||
|
||||
2026-10-11 T02 source-design disposition checkpoint: founder accepted exact
|
||||
source memo `infd-20261011-key39-source-design-r1`, review
|
||||
`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`.
|
||||
Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval
|
||||
`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject
|
||||
`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL:
|
||||
`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`.
|
||||
Root's authenticated confirmed readback is
|
||||
`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`,
|
||||
SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed
|
||||
`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256
|
||||
`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256
|
||||
`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction.
|
||||
|
||||
Acceptance covers recommended source preparation for existing bootstrap-console
|
||||
human-client purpose reuse, returning existing coding-agent custody admission or
|
||||
preparing a narrow owner proposal, proposed founder directSSH source profile,
|
||||
and conditional exclusive-owner tmpfs files. It does not establish actual
|
||||
registration/MFA/listener, coding-agent custody/delivery, transport/file admission,
|
||||
node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh
|
||||
attended native go. The published choice documents remain historical exact
|
||||
pending proposals; this appended receipt records their later source disposition.
|
||||
T02 remains wait on external owner custody/admission and separate attended
|
||||
execution authorization. Source acceptance permits further source preparation;
|
||||
actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03
|
||||
remain done. No native or credential action is authorized by this source receipt.
|
||||
See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact
|
||||
owner returns and safe metadata evidence needed before preparing a runnable
|
||||
attended execution proposal.
|
||||
|
||||
2026-10-11 ordered-goal qualification (T02 only): the final source runner already
|
||||
exists and its 17 local semantic tests pass. No new helper or source protocol is
|
||||
needed to repeat accepted preparation. Native execution follows the first
|
||||
Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03);
|
||||
only those task metadata were checked, not wait-task bodies. NetKingdom owns the
|
||||
registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns
|
||||
leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is
|
||||
already recorded resolved; do not ask again. Attended human authentication and
|
||||
owner-held existing coding-agent custody go on the single attended checklist.
|
||||
No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred.
|
||||
|
||||
2026-10-17 Platform owner returns (source-prepared, not admitted):
|
||||
railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md`
|
||||
(commit `0a9617a`). The coding client `codex-railiance-platform` has no
|
||||
admission, custody or delivery record. The existing narrow proposal
|
||||
`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`)
|
||||
remains the one to admit, so the coding-agent case stays unrunnable until owners
|
||||
admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches
|
||||
the packet's claim design. It declares no `expiration_leeway`, so the expiry
|
||||
probe must assume OpenBao's 150s default until an attended sanitized read
|
||||
confirms the live value. The node-local Bao origin is not declared in source and
|
||||
also needs that attended read. T02 stays wait.
|
||||
|
|
@ -0,0 +1,368 @@
|
|||
---
|
||||
id: KEY-WP-0039
|
||||
type: workplan
|
||||
title: "Negative-check tokens for the secrets-engine OpenBao JWT role"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-10"
|
||||
updated: "2026-10-17"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- railiance-platform
|
||||
- secrets-engine
|
||||
state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86"
|
||||
---
|
||||
|
||||
# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role
|
||||
|
||||
## Why
|
||||
|
||||
CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role
|
||||
`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong
|
||||
`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT
|
||||
and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine
|
||||
`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked
|
||||
whether KeyCape can issue the negative tokens without touching the
|
||||
`secrets-engine` registration.
|
||||
|
||||
KeyCape signs only what a registration declares. It has no way to mint a token
|
||||
with arbitrary claims, and adding one would create a forging path in the issuer.
|
||||
So the negative tokens must come from real, separate registrations that differ
|
||||
from `secrets-engine` in exactly one property each.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- The `secrets-engine` registration is not changed.
|
||||
- Probe clients are confidential `client_credentials` clients with the minimum
|
||||
60-second lifetime, scopes that grant nothing, and secrets minted into custody
|
||||
like any other client. They are removed after the proof.
|
||||
- The probe script prints only accept or refuse booleans per case, never tokens or
|
||||
secrets. It runs attended.
|
||||
|
||||
## T01 — Define the probe registrations
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8"
|
||||
```
|
||||
|
||||
Write a non-secret request packet under `docs/` with one probe client per claim
|
||||
mismatch, each identical to `secrets-engine` except for:
|
||||
|
||||
- `aud`: a different access-token resource audience
|
||||
- `sub`: a different service subject
|
||||
- `tenant`: a tenant other than `platform`
|
||||
- `roles`: a role other than `secrets-engine`
|
||||
- `scope`: a scope outside the role's bound scope
|
||||
|
||||
Two cases need no probe client. A service token always carries
|
||||
`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong
|
||||
`principal_type` case uses a human access token from an attended login. The
|
||||
coding-agent case uses the existing `codex-railiance-platform` registration (role
|
||||
`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live;
|
||||
T01 confirms that.
|
||||
|
||||
The expired-JWT case reuses any probe client: request a token, wait past its
|
||||
60-second lifetime, then present it.
|
||||
|
||||
Acceptance: railiance-platform agrees the packet covers every refusal that
|
||||
CCR-2026-0038 names.
|
||||
|
||||
2026-10-10: railiance-platform agreed that the case list covers every
|
||||
CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no
|
||||
provider fallback) are theirs and secrets-engine's. Packet written at
|
||||
`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two
|
||||
ways, both sent back for re-agreement:
|
||||
|
||||
- **Expired case.** A token from another probe already fails a claim, so its
|
||||
refusal does not show that expiry was checked. It needs a sixth client,
|
||||
`openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That
|
||||
makes it a second credential able to log in as secrets-engine for the session.
|
||||
OpenBao's `expiration_leeway` (150s by default) also extends the wait.
|
||||
- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is
|
||||
set. Every probe except the aud probe therefore pins
|
||||
`audience: secrets-engine-openbao`, so it fails only on its own claim.
|
||||
|
||||
Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's
|
||||
environment, because KeyCape resolves `secretRef` at startup. The packet uses a
|
||||
transient Kubernetes Secret that is deleted in the same session.
|
||||
|
||||
## T02 — Register the probe clients, run the attended probe, then remove them
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T02
|
||||
status: wait
|
||||
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
|
||||
blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved."
|
||||
priority: medium
|
||||
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
|
||||
```
|
||||
|
||||
2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md.
|
||||
|
||||
Write the attended script, which prints booleans only.
|
||||
|
||||
2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in
|
||||
`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused
|
||||
boolean per case plus the refusal class (`claim_mismatch`, `expired`,
|
||||
`not_service_principal`), and no token, secret, claim value or OpenBao error text.
|
||||
A transport failure or an unexpected response is reported as `inconclusive`, never
|
||||
as a refusal. A token that OpenBao accepts is revoked at once and fails the run.
|
||||
Still waiting: railiance-platform's re-agreement on the two departures above, and
|
||||
an attended registry session with net-kingdom. Register the clients
|
||||
through the normal registration path, hand the run to railiance-platform, record
|
||||
a value-free receipt in `docs/evidence/`, and deregister the clients afterwards.
|
||||
|
||||
Acceptance: every case is refused, the receipt is committed, and the probe clients
|
||||
are gone.
|
||||
|
||||
|
||||
2026-10-11 source preparation: T01 remains progress because the exact expiry
|
||||
client and audience-pin amendments require platform-owner re-agreement; the
|
||||
prior agreement covered the earlier case list, not this revised packet. The
|
||||
prepared runner revision adds a valid expiry positive control and immediate
|
||||
revoke, exact decoded claim-isolation preflight (not signature evidence),
|
||||
semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen
|
||||
local tests pass; original-source semantic regressions demonstrate two false
|
||||
successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner
|
||||
returns, live authority, effective leeway, custody boundaries and cleanup. No
|
||||
live token, registry or OpenBao action occurred. T02 continues to wait for owner
|
||||
re-agreement plus separate attended live authorization. MASON-WP-0009-T05
|
||||
operator/ESO capability and nonmember checks remain independent acceptance.
|
||||
|
||||
2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans.
|
||||
|
||||
|
||||
2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner
|
||||
acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md)
|
||||
confirms all 14 executable/test/producer-decision pins and three Platform source
|
||||
pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing
|
||||
source acceptance remains valid. Resolved registration go caafe4be covers only
|
||||
the original secrets-engine-openbao client (KEY-WP-0037-T03, deployed 6f79a2af,
|
||||
NK-WP-0046 exact-match helper); it does not authorize new probe clients or
|
||||
coding-client custody, and no renewed original-client approval is requested.
|
||||
The standing attended rule remains separate, for reviewed commands within
|
||||
existing scope; no new protocol or blanket approval request is introduced. The
|
||||
remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof,
|
||||
admission of the existing coding-client proposal with sanitized owner readiness
|
||||
returns, then one attended negative/revoke/cleanup receipt committed with probe
|
||||
clients removed. No native acceptance or credential/registration action is
|
||||
inferred from this coordinator review.
|
||||
|
||||
|
||||
## KeyCape issuer-source compatibility inspection for lifecycle v2
|
||||
|
||||
```task
|
||||
id: KEY-WP-0039-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
|
||||
```
|
||||
|
||||
2026-10-11: Completed KeyCape's own issuer-source inspection for
|
||||
KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256
|
||||
6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed.
|
||||
Read AGENTS.md, governing INTENT.md, current client-credentials claim source,
|
||||
registration fragments, native caller-authentication commands and frozen v2
|
||||
issuer transport/probe wiring. Seventeen unchanged runner tests pass locally.
|
||||
This task completes only issuer-source inspection; the resulting narrow
|
||||
source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment).
|
||||
T01 stays done and T02 stays open for actual native registration, token input,
|
||||
refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner
|
||||
checkout was changed during this source inspection. Owner source files were
|
||||
read for the bounded issuer compatibility check.
|
||||
|
||||
Current token.go:306-309 returns HTTP 401 invalid_profile_usage with
|
||||
feature=client_id for a removed confidential client; wrong-secret
|
||||
feature=Authorization cannot count as removed-registration evidence. The
|
||||
proposal and packet comments are narrowly corrected against that current
|
||||
issuer contract. Prior T01/source re-agreement history is preserved; this source
|
||||
acknowledgment supplies no NetKingdom runtime-owner review or founder live go.
|
||||
|
||||
The private token-producer handoff remains a real execution gap under T02.
|
||||
Existing keycape login/service-token commands require a new private output
|
||||
file; login also needs a concrete admitted public PKCE client/callback and
|
||||
attended browser/MFA. The lifecycle expects an in-memory private stdin object
|
||||
containing human_token and coding_agent_token, but no reviewed file-to-process
|
||||
or process-to-process producer, admitted human client/callback, custody-injected
|
||||
coding-agent source, native placement or producer cleanup receipt has been
|
||||
provided. No unattended token producer or admitted secret route is invented.
|
||||
Resolve those exact owner contracts before founder native approval; existing
|
||||
static registration is not live token custody or producer admission.
|
||||
|
||||
|
||||
2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed
|
||||
KeyCape issuer-facing inspection before the new formal acknowledgment in
|
||||
docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4
|
||||
manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8,
|
||||
executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d.
|
||||
NetKingdom's independent V4 source review is approved, receipt SHA256
|
||||
4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb.
|
||||
The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain
|
||||
retained as one correction trial; they are not extra scored successes.
|
||||
|
||||
V4 retains the unchanged issuer runner and six-client claim design, and fixes
|
||||
finite/nonboolean windows, true total request deadlines, late-known OpenBao
|
||||
session private revoke/refusal and guarded bounded final Secret deletion.
|
||||
The issuer-facing acknowledgement covers only stopping late token exchanges,
|
||||
claim source/shape and preserving token cleanup failure/unknown outcomes; it
|
||||
does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41
|
||||
supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass
|
||||
locally. No native tokens, credentials, issuer mutations or live cleanup were
|
||||
performed. T01 stays done; T02 stays open and the genuine private token-producer
|
||||
handoff remains its next source prerequisite before actual founder native go.
|
||||
|
||||
|
||||
2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed
|
||||
KeyCape's own current issuer-source/claim/cleanup-contract inspection before
|
||||
any new formal acknowledgment. Current token.go:306-309 still distinguishes
|
||||
removed-client invalid_profile_usage feature=client_id from wrong-secret
|
||||
feature=Authorization. The six-client design, audience source and expiry
|
||||
positive control remain unchanged, and the original runner's 17 tests pass.
|
||||
Exact V5 manifest SHA256
|
||||
d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285;
|
||||
executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360.
|
||||
V5 has 46 passing supplied lifecycle checks. It adds a single exceptional
|
||||
response-close abort under a fixed20ms cap clipped to nonresettable aggregate
|
||||
cleanup, and to positive original request time remaining, with expired aggregate
|
||||
zero calls, fixed private errors and truthful known-session revoke/unknown
|
||||
outcomes. This inspection covers issuer-facing token lifetime and claim
|
||||
interfaces only; NetKingdom and Platform independently own their runtime and
|
||||
backend review decisions. Previous V2/V3 Net source refusals and Platform V4
|
||||
exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are
|
||||
historical narrow source inspections, never aggregate native approval.
|
||||
T01 remains done, T02 open. No native actions or credentials occurred. The real
|
||||
private token-producer/client/callback/coding-agent custody/cleanup handoff gap
|
||||
stays the next prerequisite; no unattended admission is inferred.
|
||||
|
||||
Exact independent V5 source decisions were then verified: NetKingdom approved
|
||||
receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc
|
||||
(46 supplied +17 runner +21 independent checks); Platform approved receipt
|
||||
SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
|
||||
(46 supplied +5 independent +17 previously verified runner checks). Those are
|
||||
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
|
||||
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
|
||||
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
|
||||
|
||||
|
||||
2026-10-11 T02 continuing source preparation: private producer decision packet
|
||||
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
|
||||
flows, selected existing netkingdom-bootstrap-console literal-loopback human
|
||||
client subject to owner purpose/MFA/listener admission, and the existing
|
||||
codex-railiance-platform source identity subject to actual live/custody/consumer
|
||||
admission. No existing coding-agent delivery receipt was found; AppRole tokens
|
||||
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
|
||||
CLI's two caller-owned private files has 18 passing synthetic checks, with
|
||||
identity-guarded explicit file consumption and fixed value-free failure output.
|
||||
Decoded claim shape does not prove signatures or authenticated admission.
|
||||
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
|
||||
choices and missing receipts. The exact trusted workstation-to-node launcher,
|
||||
private-file allowance and railiance01 Bao route/public-health guard still need
|
||||
source review and founder disposition before any token/temporary credential/
|
||||
mutation/restart. No registration, custody, login, native action or credential
|
||||
request occurred. V5 and failed review histories retained; T01 stays done and
|
||||
T02 open. No new work record or native acceptance is inferred.
|
||||
|
||||
|
||||
2026-10-11 same T02 source-preparation correction for
|
||||
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
|
||||
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
|
||||
V2 has source-pinned private launch/ready handshake compatible with exact
|
||||
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
|
||||
ANY caller-token file load/consume/write; separate local data/control pipes
|
||||
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
|
||||
only clips downward after readiness. 31 distinct producer/launcher synthetic
|
||||
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
|
||||
credential access was performed. Stat/unlink remains non-atomic and requires
|
||||
continuously exclusive sameUID custody; the retained race test is a custody
|
||||
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
|
||||
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
|
||||
trusted launcher/directSSH profile/private tmpfs allowance, selected human
|
||||
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
|
||||
unadmitted owner choices. Native node route, role/leeway/window/founder decision
|
||||
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
|
||||
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
|
||||
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
|
||||
now propagates into sticky remote consumer uncertainty before launcher early
|
||||
return; local SSH termination never proves remote no-effects/recovery. Actual
|
||||
full-payload then interrupted confirmation and4096-byte partialpipe failures
|
||||
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
|
||||
bytes unchanged; new source reviews required, not implicit repin. Native
|
||||
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
|
||||
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
|
||||
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
|
||||
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
|
||||
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
|
||||
a renewed launcher allowance. 40 own source tests plus9 retained paired
|
||||
wrapper/child handshake regressions pass with synthetic/no native transport.
|
||||
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
|
||||
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
|
||||
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
|
||||
credentials/native/owner checkout changes; no task/plan duplication. T01done
|
||||
and T02open; source correction is not native acceptance.
|
||||
|
||||
|
||||
2026-10-11 T02 source-design disposition checkpoint: founder accepted exact
|
||||
source memo `infd-20261011-key39-source-design-r1`, review
|
||||
`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`.
|
||||
Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval
|
||||
`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject
|
||||
`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL:
|
||||
`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`.
|
||||
Root's authenticated confirmed readback is
|
||||
`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`,
|
||||
SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed
|
||||
`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256
|
||||
`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256
|
||||
`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction.
|
||||
|
||||
Acceptance covers recommended source preparation for existing bootstrap-console
|
||||
human-client purpose reuse, returning existing coding-agent custody admission or
|
||||
preparing a narrow owner proposal, proposed founder directSSH source profile,
|
||||
and conditional exclusive-owner tmpfs files. It does not establish actual
|
||||
registration/MFA/listener, coding-agent custody/delivery, transport/file admission,
|
||||
node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh
|
||||
attended native go. The published choice documents remain historical exact
|
||||
pending proposals; this appended receipt records their later source disposition.
|
||||
T02 remains wait on external owner custody/admission and separate attended
|
||||
execution authorization. Source acceptance permits further source preparation;
|
||||
actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03
|
||||
remain done. No native or credential action is authorized by this source receipt.
|
||||
See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact
|
||||
owner returns and safe metadata evidence needed before preparing a runnable
|
||||
attended execution proposal.
|
||||
|
||||
2026-10-11 ordered-goal qualification (T02 only): the final source runner already
|
||||
exists and its 17 local semantic tests pass. No new helper or source protocol is
|
||||
needed to repeat accepted preparation. Native execution follows the first
|
||||
Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03);
|
||||
only those task metadata were checked, not wait-task bodies. NetKingdom owns the
|
||||
registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns
|
||||
leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is
|
||||
already recorded resolved; do not ask again. Attended human authentication and
|
||||
owner-held existing coding-agent custody go on the single attended checklist.
|
||||
No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred.
|
||||
|
||||
2026-10-17 Platform owner returns (source-prepared, not admitted):
|
||||
railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md`
|
||||
(commit `0a9617a`). The coding client `codex-railiance-platform` has no
|
||||
admission, custody or delivery record. The existing narrow proposal
|
||||
`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`)
|
||||
remains the one to admit, so the coding-agent case stays unrunnable until owners
|
||||
admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches
|
||||
the packet's claim design. It declares no `expiration_leeway`, so the expiry
|
||||
probe must assume OpenBao's 150s default until an attended sanitized read
|
||||
confirms the live value. The node-local Bao origin is not declared in source and
|
||||
also needs that attended read. T02 stays wait.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json,hashlib,re,yaml,difflib
|
||||
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1');out.mkdir()
|
||||
rel='workplans/KEY-WP-0039-openbao-negative-probe-tokens.md';p=root/rel
|
||||
|
||||
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
|
||||
base=git('rev-parse','HEAD').strip();assert base=='40de616e078906e04c692edf7cb7a8bfbc07b5ea';assert not git('status','--porcelain=v1')
|
||||
before=p.read_text();old='''source acceptance, resolved registration go caafe4be and the standing attended
|
||||
rule remain valid; no new protocol or blanket approval request is needed. The
|
||||
''';new='''source acceptance remains valid. Resolved registration go caafe4be covers only
|
||||
the original secrets-engine-openbao client (KEY-WP-0037-T03, deployed 6f79a2af,
|
||||
NK-WP-0046 exact-match helper); it does not authorize new probe clients or
|
||||
coding-client custody, and no renewed original-client approval is requested.
|
||||
The standing attended rule remains separate, for reviewed commands within
|
||||
existing scope; no new protocol or blanket approval request is introduced. The
|
||||
''';assert before.count(old)==1;after=before.replace(old,new)
|
||||
blocks=lambda text:re.findall(r'^```task\n.*?^```',text,re.M|re.S)
|
||||
assert blocks(before)==blocks(after);assert before.split('---',2)[1]==after.split('---',2)[1]
|
||||
(out/'KEY39.preimage.md').write_text(before);(out/'KEY39.proposed.md').write_text(after)
|
||||
readback=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/registration-approval-readback.json')
|
||||
baseline={'base_commit':base,'branch':git('branch','--show-current').strip(),'status':git('status','--porcelain=v1'),'index':git('ls-files','--stage'),'path':rel,'before_sha256':hashlib.sha256(before.encode()).hexdigest(),'after_sha256':hashlib.sha256(after.encode()).hexdigest(),'frontmatter':yaml.safe_load(before.split('---',2)[1]),'task_blocks':[yaml.safe_load(t.split('\n',1)[1].rsplit('```',1)[0]) for t in blocks(before)],'approval_scope_source':{'path':str(readback),'sha256':hashlib.sha256(readback.read_bytes()).hexdigest()},'assessment':'Clarification needed because previous coordinator wording grouped original-client go with standing rule without naming its object; no historical approval revoked or expanded.'};(out/'baseline.json').write_text(json.dumps(baseline,indent=2)+'\n')
|
||||
p.write_text(after);(out/'one-path.patch').write_text(git('diff','--',rel));assert git('diff','--name-only').splitlines()==[rel]
|
||||
print(json.dumps({'status':'current paragraph scope clarified','frontmatter_and_three_blocks_unchanged':True,'base':base}))
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"schema": "state-hub.repository-projection-reconcile.v1",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "26ec9bb9b195ad377448570d6b161fd682311a0b",
|
||||
"derived_commit": "26ec9bb9b195ad377448570d6b161fd682311a0b",
|
||||
"outcome": {
|
||||
"schema": "state-hub.projection-reset.v1",
|
||||
"repo_slug": "key-cape",
|
||||
"commit": "26ec9bb9b195ad377448570d6b161fd682311a0b",
|
||||
"status": "applied",
|
||||
"counts": {
|
||||
"created": 0,
|
||||
"updated": 44,
|
||||
"retired": 0,
|
||||
"refused": 0,
|
||||
"released": 0,
|
||||
"created_tasks": 0,
|
||||
"updated_tasks": 0,
|
||||
"cancelled_tasks": 0,
|
||||
"created_edges": 0,
|
||||
"removed_edges": 0,
|
||||
"unresolved_edges": 1,
|
||||
"external_edges": 8
|
||||
},
|
||||
"created": [],
|
||||
"updated": [
|
||||
"ADHOC-2026-09-05",
|
||||
"ADHOC-2026-09-07",
|
||||
"ADHOC-2026-09-27",
|
||||
"KEY-WP-0001",
|
||||
"KEY-WP-0002",
|
||||
"KEY-WP-0003",
|
||||
"KEY-WP-0004",
|
||||
"KEY-WP-0005",
|
||||
"KEY-WP-0006",
|
||||
"KEY-WP-0007",
|
||||
"KEY-WP-0008",
|
||||
"KEY-WP-0009",
|
||||
"KEY-WP-0010",
|
||||
"KEY-WP-0011",
|
||||
"KEY-WP-0012",
|
||||
"KEY-WP-0013",
|
||||
"KEY-WP-0014",
|
||||
"KEY-WP-0015",
|
||||
"KEY-WP-0016",
|
||||
"KEY-WP-0017",
|
||||
"KEY-WP-0018",
|
||||
"KEY-WP-0019",
|
||||
"KEY-WP-0020",
|
||||
"KEY-WP-0021",
|
||||
"KEY-WP-0022",
|
||||
"KEY-WP-0023",
|
||||
"KEY-WP-0024",
|
||||
"KEY-WP-0025",
|
||||
"KEY-WP-0026",
|
||||
"KEY-WP-0027",
|
||||
"KEY-WP-0028",
|
||||
"KEY-WP-0029",
|
||||
"KEY-WP-0030",
|
||||
"KEY-WP-0031",
|
||||
"KEY-WP-0032",
|
||||
"KEY-WP-0033",
|
||||
"KEY-WP-0034",
|
||||
"KEY-WP-0035",
|
||||
"KEY-WP-0036",
|
||||
"KEY-WP-0037",
|
||||
"KEY-WP-0038",
|
||||
"KEY-WP-0039",
|
||||
"KEY-WP-0040",
|
||||
"KEY-WP-0041"
|
||||
],
|
||||
"retired": [],
|
||||
"released": [],
|
||||
"created_tasks": [],
|
||||
"updated_tasks": [],
|
||||
"cancelled_tasks": [],
|
||||
"created_edges": [],
|
||||
"removed_edges": [],
|
||||
"unresolved_edges": [
|
||||
{
|
||||
"workplan": "KEY-WP-0010",
|
||||
"from": "KEY-WP-0010",
|
||||
"target": "RAILIANCE-WP-0027",
|
||||
"reason": "missing"
|
||||
}
|
||||
],
|
||||
"external_edges": [
|
||||
{
|
||||
"workplan": "KEY-WP-0038",
|
||||
"from": "KEY-WP-0038-T03",
|
||||
"target": "KEY-WP-0038-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
|
||||
"from_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0038",
|
||||
"from": "KEY-WP-0038-T02",
|
||||
"target": "KEY-WP-0038-T01",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
|
||||
"from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "5f019d13-1fa3-5b88-a27a-75a3339ca96e"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0038",
|
||||
"from": "KEY-WP-0038-T02",
|
||||
"target": "NK-WP-0048-T06",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
|
||||
"from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0039",
|
||||
"from": "KEY-WP-0039-T02",
|
||||
"target": "KEY-WP-0038-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
|
||||
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0039",
|
||||
"from": "KEY-WP-0039-T02",
|
||||
"target": "KEY-WP-0039-T01",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
|
||||
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0039",
|
||||
"from": "KEY-WP-0039-T02",
|
||||
"target": "KEY-WP-0038-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
|
||||
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0007",
|
||||
"from": "KEY-WP-0007",
|
||||
"target": "KEY-WP-0005",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "24db4178-705c-5b09-b7db-ee7a345d71a0",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "711a8303-69aa-5db6-9043-92530c6dc81b",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "KEY-WP-0009",
|
||||
"from": "KEY-WP-0009",
|
||||
"target": "KEY-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "c1a9b1cc-2ff0-566a-b544-1a2ef967fc0d",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d",
|
||||
"to_task_id": null
|
||||
}
|
||||
],
|
||||
"refused": [],
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from pathlib import Path
|
||||
import hashlib,json,subprocess,urllib.request,urllib.error,yaml,re
|
||||
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1');base='http://127.0.0.1:8000';head='26ec9bb9b195ad377448570d6b161fd682311a0b'
|
||||
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
|
||||
assert git('rev-parse','HEAD').strip()==head
|
||||
assert git('ls-remote','origin','refs/heads/main').split()[0]==head
|
||||
assert not git('status','--porcelain=v1')
|
||||
def get(path):
|
||||
with urllib.request.urlopen(base+path,timeout=30) as response:return json.load(response)
|
||||
health=get('/state/health');assert (health['instance_role'],health['instance_label'])==('primary','railiance01')
|
||||
request_path='/repos/key-cape/work-record-projection/reconcile';body={'expected_commit':head,'acknowledge_retirements':False}
|
||||
digest=hashlib.sha256(json.dumps({'method':'POST','path':request_path,'body':body},sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
||||
request=urllib.request.Request(base+request_path,data=json.dumps(body).encode(),method='POST',headers={'Content-Type':'application/json','Idempotency-Key':'rmgr-projection:v2:'+digest,'X-StateHub-Source-Agent':'custodian-codex'})
|
||||
try:
|
||||
with urllib.request.urlopen(request,timeout=120) as response:receipt=json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
raw=error.read();(out/'canonical-receiver-refusal.json').write_bytes(raw);raise
|
||||
(out/'canonical-receiver.json').write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
assert receipt['expected_commit']==receipt['derived_commit']==head
|
||||
assert (receipt['instance_role'],receipt['instance_label'])==('primary','railiance01')
|
||||
assert receipt['outcome']['status'] in ('applied','noop'),receipt['outcome']['status']
|
||||
snapshot=get('/repos/key-cape/work-record-projection/snapshot');baseline=json.loads((out/'baseline.json').read_text())
|
||||
planid=baseline['frontmatter']['state_hub_workstream_id'];plan=next(w for w in snapshot['workplans'] if w['id']==planid)
|
||||
rows=[]
|
||||
for block in baseline['task_blocks']:
|
||||
task=next(t for t in snapshot['tasks'] if t['id']==block['state_hub_task_id'])
|
||||
assert task['status']==block['status'];rows.append({'record_id':block['id'],'state_hub_task_id':task['id'],'status':task['status'],'workplan_id':task.get('workplan_id'),'depends_on_source':block.get('depends_on',[])})
|
||||
current=(root/baseline['path']).read_text();assert current==(out/'KEY39.proposed.md').read_text()
|
||||
assert re.findall(r'^```task\n.*?^```',current,re.M|re.S)==re.findall(r'^```task\n.*?^```',(out/'KEY39.preimage.md').read_text(),re.M|re.S)
|
||||
verification={'commit':head,'receiver_status':receipt['outcome']['status'],'acknowledge_retirements':False,'instance_role':receipt['instance_role'],'instance_label':receipt['instance_label'],'workplan':{'id':planid,'status':plan['status'],'record_id':'KEY-WP-0039'},'task_bindings':rows,'all_three_source_blocks_unchanged':True,'frontmatter_unchanged':True,'only_committed_path':baseline['path'],'remote_forge_matches':True,'owner_worktree_and_index_clean':not git('status','--porcelain=v1'),'normal_sync_refusal':'unrelated grandfathered ADHOC-2026-09-27-T02 UUID; not modified','native_acceptance_inferred':False}
|
||||
assert plan['status']==baseline['frontmatter']['status']
|
||||
(out/'final-verification.json').write_text(json.dumps(verification,indent=2)+'\n');print(json.dumps(verification,indent=2))
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"commit": "26ec9bb9b195ad377448570d6b161fd682311a0b",
|
||||
"parent": "40de616e078906e04c692edf7cb7a8bfbc07b5ea",
|
||||
"paths": [
|
||||
"workplans/KEY-WP-0039-openbao-negative-probe-tokens.md"
|
||||
],
|
||||
"normal_hook": "scanner8.30.1 clean",
|
||||
"native_or_credential_action": false
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — key-cape
|
||||
|
||||
**Domain:** infotech
|
||||
**Last synced:** 2026-10-17 02:16 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
### Negative-check tokens for the secrets-engine OpenBao JWT role
|
||||
Progress: 2/3 done | workplan_id: `09c3bddf-6058-5603-8488-cccdb7be4f86`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T01 — Define the probe registrations `03da4c38`
|
||||
*(wait: Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.)*
|
||||
|
||||
### Forgejo as a NetKingdom OIDC client, so the forge can close
|
||||
Progress: 2/4 done | workplan_id: `098e3e2c-a138-560e-b2d3-3a15eb49d8a9`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T01 — Define what Forgejo receives `ddcfb482`
|
||||
*(wait: Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout.)*
|
||||
- ! T02 — Register the client and hand over its secret `40ddaa29`
|
||||
*(wait: Claims are verified against the registered client.)*
|
||||
|
||||
## Inbox Hygiene
|
||||
|
||||
**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start.
|
||||
**Missing thread_id:** 2 unread message(s) lack supersession chains.
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
||||
If the state-hub MCP server is reachable, call:
|
||||
`get_domain_summary("infotech")`
|
||||
This provides richer cross-domain context.
|
||||
If the MCP call fails, use this file as your orientation source.
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
# Work Records — key-cape
|
||||
|
||||
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
|
||||
> stage 3). Do not edit by hand — edit the source file/block listed for
|
||||
> each record and re-run fix-consistency to refresh this index. Archived
|
||||
> workplans are omitted; closed decisions/intakes/engagements stay listed
|
||||
> so recently-resolved work is still visible. [auto]
|
||||
|
||||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | ADHOC-2026-09-05 | finished | — | workplans/ADHOC-2026-09-05.md |
|
||||
| workplan | ADHOC-2026-09-07 | finished | — | workplans/ADHOC-2026-09-07.md |
|
||||
| workplan | ADHOC-2026-09-27 | finished | — | workplans/ADHOC-2026-09-27.md |
|
||||
| workplan | KEY-WP-0001 | finished | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| workplan | KEY-WP-0002 | finished | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| workplan | KEY-WP-0003 | finished | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| workplan | KEY-WP-0004 | finished | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| workplan | KEY-WP-0005 | finished | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
|
||||
| workplan | KEY-WP-0006 | finished | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| workplan | KEY-WP-0007 | finished | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
|
||||
| workplan | KEY-WP-0008 | finished | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
|
||||
| workplan | KEY-WP-0009 | finished | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
|
||||
| workplan | KEY-WP-0010 | finished | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md |
|
||||
| workplan | KEY-WP-0011 | finished | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
||||
| workplan | KEY-WP-0012 | finished | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
|
||||
| workplan | KEY-WP-0013 | finished | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| workplan | KEY-WP-0014 | finished | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
|
||||
| workplan | KEY-WP-0015 | finished | — | workplans/KEY-WP-0015-scope-intent-assessment.md |
|
||||
| workplan | KEY-WP-0016 | finished | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||
| workplan | KEY-WP-0017 | finished | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||
| workplan | KEY-WP-0018 | finished | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
||||
| workplan | KEY-WP-0019 | finished | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| workplan | KEY-WP-0020 | finished | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
|
||||
| workplan | KEY-WP-0021 | finished | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
|
||||
| workplan | KEY-WP-0022 | finished | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| workplan | KEY-WP-0023 | finished | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| workplan | KEY-WP-0024 | finished | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
|
||||
| workplan | KEY-WP-0025 | finished | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
|
||||
| workplan | KEY-WP-0026 | finished | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| workplan | KEY-WP-0027 | finished | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
|
||||
| workplan | KEY-WP-0028 | finished | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
|
||||
| workplan | KEY-WP-0029 | finished | — | workplans/KEY-WP-0029-scope-reassessment.md |
|
||||
| workplan | KEY-WP-0030 | finished | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
|
||||
| workplan | KEY-WP-0031 | finished | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
|
||||
| workplan | KEY-WP-0032 | finished | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
|
||||
| workplan | KEY-WP-0033 | finished | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
|
||||
| workplan | KEY-WP-0034 | finished | — | workplans/KEY-WP-0034-account-recovery.md |
|
||||
| workplan | KEY-WP-0035 | finished | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| workplan | KEY-WP-0036 | finished | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
|
||||
| workplan | KEY-WP-0037 | finished | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
|
||||
| workplan | KEY-WP-0038 | active | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
|
||||
| workplan | KEY-WP-0039 | active | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
|
||||
| workplan | KEY-WP-0040 | finished | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
|
||||
| workplan | KEY-WP-0041 | finished | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
|
||||
| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md |
|
||||
| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md |
|
||||
| task | ADHOC-2026-09-27-T01 | done | — | workplans/ADHOC-2026-09-27.md |
|
||||
| task | ADHOC-2026-09-27-T02 | done | — | workplans/ADHOC-2026-09-27.md |
|
||||
| task | KEY-WP-0001-T01 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T02 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T03 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T04 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T05 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T06 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T07 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T08 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T09 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T10 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T11 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T12 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T13 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T14 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T15 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T16 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T17 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T18 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T19 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T20 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T21 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T22 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0001-T23 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||
| task | KEY-WP-0002-T01 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0002-T02 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0002-T03 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0002-T04 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0002-T05 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0002-T06 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
|
||||
| task | KEY-WP-0003-T01 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| task | KEY-WP-0003-T02 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| task | KEY-WP-0003-T03 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| task | KEY-WP-0003-T04 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| task | KEY-WP-0003-T05 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
|
||||
| task | KEY-WP-0004-T01 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T02 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T03 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T04 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T05 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T06 | cancel | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0004-T07 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
|
||||
| task | KEY-WP-0005-T01 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
|
||||
| task | KEY-WP-0005-T02 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
|
||||
| task | KEY-WP-0005-T03 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
|
||||
| task | KEY-WP-0006-T01 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| task | KEY-WP-0006-T02 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| task | KEY-WP-0006-T03 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| task | KEY-WP-0006-T04 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| task | KEY-WP-0006-T05 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
|
||||
| task | KEY-WP-0007-T01 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
|
||||
| task | KEY-WP-0007-T02 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
|
||||
| task | KEY-WP-0007-T03 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
|
||||
| task | KEY-WP-0008-T01 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
|
||||
| task | KEY-WP-0008-T02 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
|
||||
| task | KEY-WP-0008-T03 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
|
||||
| task | KEY-WP-0008-T04 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
|
||||
| task | KEY-WP-0009-T01 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
|
||||
| task | KEY-WP-0009-T02 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
|
||||
| task | KEY-WP-0009-T03 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
|
||||
| task | KEY-WP-0009-T04 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
|
||||
| task | KEY-WP-0010-T01 | done | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md |
|
||||
| task | KEY-WP-0011-T01 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
||||
| task | KEY-WP-0011-T02 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
||||
| task | KEY-WP-0011-T03 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
||||
| task | KEY-WP-0012-T01 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
|
||||
| task | KEY-WP-0012-T02 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
|
||||
| task | KEY-WP-0012-T03 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
|
||||
| task | KEY-WP-0013-T01 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T02 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T03 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T04 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T05 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T06 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0013-T07 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
|
||||
| task | KEY-WP-0014-T01 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
|
||||
| task | KEY-WP-0014-T02 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
|
||||
| task | KEY-WP-0014-T03 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
|
||||
| task | KEY-WP-0014-T04 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
|
||||
| task | KEY-WP-0015-T01 | done | — | workplans/KEY-WP-0015-scope-intent-assessment.md |
|
||||
| task | KEY-WP-0016-T01 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||
| task | KEY-WP-0016-T02 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||
| task | KEY-WP-0016-T03 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||
| task | KEY-WP-0016-T04 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||
| task | KEY-WP-0017-T01 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||
| task | KEY-WP-0017-T02 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||
| task | KEY-WP-0017-T03 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||
| task | KEY-WP-0017-T04 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||
| task | KEY-WP-0018-T01 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
||||
| task | KEY-WP-0018-T02 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
||||
| task | KEY-WP-0018-T03 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
||||
| task | KEY-WP-0019-T01 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| task | KEY-WP-0019-T02 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| task | KEY-WP-0019-T03 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| task | KEY-WP-0019-T04 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| task | KEY-WP-0019-T05 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||
| task | KEY-WP-0020-T01 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
|
||||
| task | KEY-WP-0020-T02 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
|
||||
| task | KEY-WP-0020-T03 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
|
||||
| task | KEY-WP-0021-T01 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
|
||||
| task | KEY-WP-0021-T02 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
|
||||
| task | KEY-WP-0021-T03 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
|
||||
| task | KEY-WP-0022-T01 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| task | KEY-WP-0022-T02 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| task | KEY-WP-0022-T03 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| task | KEY-WP-0022-T04 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| task | KEY-WP-0022-T05 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
|
||||
| task | KEY-WP-0023-T01 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| task | KEY-WP-0023-T02 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| task | KEY-WP-0023-T03 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| task | KEY-WP-0023-T04 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| task | KEY-WP-0023-T05 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
|
||||
| task | KEY-WP-0024-T01 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
|
||||
| task | KEY-WP-0024-T02 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
|
||||
| task | KEY-WP-0025-T01 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
|
||||
| task | KEY-WP-0025-T02 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
|
||||
| task | KEY-WP-0025-T03 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
|
||||
| task | KEY-WP-0025-T04 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
|
||||
| task | KEY-WP-0026-T01 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0026-T02 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0026-T03 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0026-T04 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0026-T05 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0026-T06 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
|
||||
| task | KEY-WP-0027-T01 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
|
||||
| task | KEY-WP-0027-T02 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
|
||||
| task | KEY-WP-0027-T03 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
|
||||
| task | KEY-WP-0027-T04 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
|
||||
| task | KEY-WP-0028-T01 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
|
||||
| task | KEY-WP-0028-T02 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
|
||||
| task | KEY-WP-0029-T01 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
|
||||
| task | KEY-WP-0029-T02 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
|
||||
| task | KEY-WP-0029-T03 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
|
||||
| task | KEY-WP-0030-T01 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
|
||||
| task | KEY-WP-0030-T02 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
|
||||
| task | KEY-WP-0031-T01 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
|
||||
| task | KEY-WP-0031-T02 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
|
||||
| task | KEY-WP-0032-T01 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
|
||||
| task | KEY-WP-0032-T02 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
|
||||
| task | KEY-WP-0033-T01 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
|
||||
| task | KEY-WP-0033-T02 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
|
||||
| task | KEY-WP-0034-T01 | done | — | workplans/KEY-WP-0034-account-recovery.md |
|
||||
| task | KEY-WP-0034-T02 | done | — | workplans/KEY-WP-0034-account-recovery.md |
|
||||
| task | KEY-WP-0035-T01 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| task | KEY-WP-0035-T02 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| task | KEY-WP-0035-T03 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| task | KEY-WP-0035-T04 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| task | KEY-WP-0035-T05 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
|
||||
| task | KEY-WP-0036-T01 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
|
||||
| task | KEY-WP-0036-T02 | cancel | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
|
||||
| task | KEY-WP-0036-T03 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
|
||||
| task | KEY-WP-0037-T01 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
|
||||
| task | KEY-WP-0037-T02 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
|
||||
| task | KEY-WP-0037-T03 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
|
||||
| task | KEY-WP-0037-T04 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
|
||||
| task | KEY-WP-0038-T01 | done | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
|
||||
| task | KEY-WP-0038-T02 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
|
||||
| task | KEY-WP-0038-T03 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
|
||||
| task | KEY-WP-0038-T04 | cancel | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
|
||||
| task | KEY-WP-0039-T01 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
|
||||
| task | KEY-WP-0039-T02 | wait | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
|
||||
| task | KEY-WP-0039-T03 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
|
||||
| task | KEY-WP-0040-T01 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
|
||||
| task | KEY-WP-0040-T02 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
|
||||
| task | KEY-WP-0041-T01 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
|
||||
| task | KEY-WP-0041-T02 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
|
||||
| intake | KEY-IN-0001 | closed | — | intakes/intakes.md |
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
{
|
||||
".custodian-brief.md": {
|
||||
"sha256": "96d84539b3a92786bff37fcc70161adec4648b9547287e52e6b817bc080a080a",
|
||||
"mode": 436,
|
||||
"index": "100644 d3b36c3bffbeb922769a2a788e35b4113fccaa3f 0\t.custodian-brief.md\n"
|
||||
},
|
||||
"WORK-RECORDS.md": {
|
||||
"sha256": "431842547bd82750f284f8c1c9f519892f1bb1c1a684b9b1e38adc6fdfdc299d",
|
||||
"mode": 420,
|
||||
"index": "100644 d86b29bbc480acfa6d12d5052039578d7d4e380c 0\tWORK-RECORDS.md\n"
|
||||
},
|
||||
"workplans/ADHOC-2026-09-05.md": {
|
||||
"sha256": "76123d22d0c1efc51212f8914e386f078b3c6d908959af6978ae73874e492b04",
|
||||
"mode": 420,
|
||||
"index": "100644 f7e78675df62c9b48e29f882fa99298d6d8ce7f1 0\tworkplans/ADHOC-2026-09-05.md\n"
|
||||
},
|
||||
"workplans/ADHOC-2026-09-07.md": {
|
||||
"sha256": "c335ebfe91dad291277e8f17034ac8f785b3bc767f281f205c446c08f03d7978",
|
||||
"mode": 420,
|
||||
"index": "100644 17401073c053c9710e6e680243bbd313b08d1fc1 0\tworkplans/ADHOC-2026-09-07.md\n"
|
||||
},
|
||||
"workplans/ADHOC-2026-09-27.md": {
|
||||
"sha256": "2794d1940fc3c5e4a2df57de9d4b742fa6aa8602b67f1d3177c0d96bc5aaf314",
|
||||
"mode": 420,
|
||||
"index": "100644 0b755547aa7d5589f0898df2f102a45b6aa24838 0\tworkplans/ADHOC-2026-09-27.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0001-keycape-implementation.md": {
|
||||
"sha256": "29a2bf4892b7185e28e9126a3d8311d0fed34a876f416a3e75c605915fde0969",
|
||||
"mode": 420,
|
||||
"index": "100644 74334411cd056e7c7253e84f1c4b40c42eb13de3 0\tworkplans/KEY-WP-0001-keycape-implementation.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0002-container-image-gitea.md": {
|
||||
"sha256": "76fee187185b30219bc257ccc9b587661df4489574a2bb57703a365a5d803cec",
|
||||
"mode": 420,
|
||||
"index": "100644 418f477c0ede13d7c003d744abf234347f5d5e31 0\tworkplans/KEY-WP-0002-container-image-gitea.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md": {
|
||||
"sha256": "5e949c696e587540d305b682eb1f8390ec544bbec6a566be8d04e73fb1b254ca",
|
||||
"mode": 420,
|
||||
"index": "100644 9896c67d78f0efdd03325535eb41295250be3a19 0\tworkplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md": {
|
||||
"sha256": "260ba7d3116db3bc5a72e5cd8b9984137e7e372c8128d21073614dbd948c10c2",
|
||||
"mode": 420,
|
||||
"index": "100644 a50e910558ee43073c92cb27865a1aceac990dd5 0\tworkplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0005-iam-profile-core-claims.md": {
|
||||
"sha256": "c6702b644cdd046c0d3bba8ba8837050826e4dc3d855bd73327ae94218ba5b57",
|
||||
"mode": 420,
|
||||
"index": "100644 648d2405699dcf25b3121df6c46d871597100d7b 0\tworkplans/KEY-WP-0005-iam-profile-core-claims.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0006-client-credentials-service-tokens.md": {
|
||||
"sha256": "b3227849978c95cad89e02afb74bf2217b6188ae65c5b0af6a21703d65477701",
|
||||
"mode": 420,
|
||||
"index": "100644 632c4c2cad39cbde55c2ae1f2ba5ecf2f445b1a0 0\tworkplans/KEY-WP-0006-client-credentials-service-tokens.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0007-user-engine-portal-oidc-client.md": {
|
||||
"sha256": "5408ff6dfeb58a8a7756b48fe4f54f7c7b98d7940cb9aaa5c7864f1247e58150",
|
||||
"mode": 420,
|
||||
"index": "100644 f1b04e1a61b398426c23d8d9ca655b3937d7bd10 0\tworkplans/KEY-WP-0007-user-engine-portal-oidc-client.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md": {
|
||||
"sha256": "bfc9b2118f0712e92a86bbb0f14a2857fafc3a42a3c14c0e2535002aaa57a2f5",
|
||||
"mode": 420,
|
||||
"index": "100644 438ab5480de3bd66d75b6ec7e09639d1b93e3e5f 0\tworkplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md": {
|
||||
"sha256": "d1daac9815930104a392ea96c14922e34f947720a31d1b0ac45a770997e0f476",
|
||||
"mode": 420,
|
||||
"index": "100644 c9e585db65424c7c4e3b5adf37a68de2bdfbdc1c 0\tworkplans/KEY-WP-0009-provider-capabilities-and-service-identities.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0010-openbao-operator-loopback-callback.md": {
|
||||
"sha256": "41d28a16bef9313b776c5c6305316a9e7d4f5528b7fb7427b8384a0ed0ad35c4",
|
||||
"mode": 420,
|
||||
"index": "100644 81614eeb8c98205d6e5eb3201da10e36b76a21e9 0\tworkplans/KEY-WP-0010-openbao-operator-loopback-callback.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0011-live-secret-exposure-recovery.md": {
|
||||
"sha256": "3c5458180fe81a04e357f8db737e4287f79640b5ab02682bcadc595b73f846cd",
|
||||
"mode": 420,
|
||||
"index": "100644 f7b906f552f6e3279895a90afbe216b6e78ee5ff 0\tworkplans/KEY-WP-0011-live-secret-exposure-recovery.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md": {
|
||||
"sha256": "68d34d75c914e7d2d8177000e800d6372cccda2b850c03ab85da42b90a66b91c",
|
||||
"mode": 420,
|
||||
"index": "100644 0f3deb626e5de9a9f898754ef81d3f2ee5282792 0\tworkplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0013-approval-engine-resource-audience.md": {
|
||||
"sha256": "55c88bf4ed9a456db59b319bbaf960dfe6f4147437e339f950e0583d7582057a",
|
||||
"mode": 420,
|
||||
"index": "100644 7d77aee3caf3d79fe6d6c1369249c6d94e0ae123 0\tworkplans/KEY-WP-0013-approval-engine-resource-audience.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0014-native-credential-lane-handoff.md": {
|
||||
"sha256": "22aac43ae5039aebd11f9f0aa57a882ad009ad6ed119ec08eb85b32a5af48cd7",
|
||||
"mode": 420,
|
||||
"index": "100644 bacf04c07f59125a90c519d3c306a7414cb9a1a3 0\tworkplans/KEY-WP-0014-native-credential-lane-handoff.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0015-scope-intent-assessment.md": {
|
||||
"sha256": "60b9f2265eff880d916c3ee8dba36aec9984ec310804de776cebf89cc98fdb95",
|
||||
"mode": 420,
|
||||
"index": "100644 82b952a216346ffb7f5cab413623fe4a88f7700e 0\tworkplans/KEY-WP-0015-scope-intent-assessment.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0016-authorization-code-protocol-hardening.md": {
|
||||
"sha256": "ed48128a1cb51cbe3d154727bafd5a5ffb3b5b618c20649b2a0e7db154cf3a0c",
|
||||
"mode": 420,
|
||||
"index": "100644 52839bd0f83d97656c8ad97a8ffc338d9019239a 0\tworkplans/KEY-WP-0016-authorization-code-protocol-hardening.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md": {
|
||||
"sha256": "8700e0e5aa07dcd7f651e792cca12a0330b7ee9d2f6064b4cc3fcf4c7d509340",
|
||||
"mode": 420,
|
||||
"index": "100644 00eb03971aae75724afbf7b8a4946ce6b7eda3af 0\tworkplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0018-export-completeness-evidence.md": {
|
||||
"sha256": "15026b708d116f803b0b2fa43f30a4dbced76e4d8137b5fcb5b4275e593d7c7b",
|
||||
"mode": 420,
|
||||
"index": "100644 325fc483d62dbad53b92b986de56112168a92b4a 0\tworkplans/KEY-WP-0018-export-completeness-evidence.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0019-upstream-provider-token-verification.md": {
|
||||
"sha256": "0d2d8b5bda1e0d333935812d8d3749cb7b0c5523a3987d1b26185f9f6d0f0fa2",
|
||||
"mode": 420,
|
||||
"index": "100644 dfeb92296f46d4ce2ac7df1fedcc61e9ccc5d6b3 0\tworkplans/KEY-WP-0019-upstream-provider-token-verification.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0020-migration-contract-preservation.md": {
|
||||
"sha256": "cc842d15bf42ddc4d0f4056bc19b3176cab2c5f3c1a2fe6d648e5dbd077be8b3",
|
||||
"mode": 420,
|
||||
"index": "100644 7b6f3be976293469dee2180c10e16f313876d640 0\tworkplans/KEY-WP-0020-migration-contract-preservation.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0021-snapshot-attribute-validation.md": {
|
||||
"sha256": "b603248693073a9b9b25b4d0a4147d8d88c16a07dc9936e48fa1f3c76fe8bab6",
|
||||
"mode": 420,
|
||||
"index": "100644 1f6efeb32a6eb8fcbc52f4d3b591e2fc6b9e006d 0\tworkplans/KEY-WP-0021-snapshot-attribute-validation.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md": {
|
||||
"sha256": "8c135e84be81de31b4949f52ac28dccd404f2e4c3eaa576d733bd50160427a53",
|
||||
"mode": 420,
|
||||
"index": "100644 7c69629ecd20d0158aa1332d9d1442b796f6a7af 0\tworkplans/KEY-WP-0022-replacement-harness-and-external-conformance.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0023-live-migration-proof.md": {
|
||||
"sha256": "e581cd11f3905805b5e8ba8e2ede85092577c7d086e1761f2a2d033fdead4e47",
|
||||
"mode": 420,
|
||||
"index": "100644 3e7c83cb12e7fc31e682cf3c27f4c4767ded2905 0\tworkplans/KEY-WP-0023-live-migration-proof.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md": {
|
||||
"sha256": "af7842fa97263bfd46a4b5ae84c5c1ecddb06ab9623586e0292dcc93099fea35",
|
||||
"mode": 420,
|
||||
"index": "100644 baa0bbd195a92708f2befd027b938fcd1812ced7 0\tworkplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md": {
|
||||
"sha256": "0addb34e6d85e376c6a5db7afba597b6f268afa98b86c8c60add9be174dd1b26",
|
||||
"mode": 420,
|
||||
"index": "100644 a53e646761178748e8c4b00dcde888698447a948 0\tworkplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md": {
|
||||
"sha256": "c1bc0de47511ae49102009a172c84d9d5d64e829a18c8d40564fe817935563b3",
|
||||
"mode": 420,
|
||||
"index": "100644 1f96a2ac3ac939c4e6e0f66172477cf73db04e51 0\tworkplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0027-rollout-readiness-and-live-state.md": {
|
||||
"sha256": "f559eae61d433d451f80ea575254bb14a47f2485267b412e42d524b434bb1b59",
|
||||
"mode": 420,
|
||||
"index": "100644 57e01b2db033847cc786504b8795e37112d988a7 0\tworkplans/KEY-WP-0027-rollout-readiness-and-live-state.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0028-browser-client-field-validation.md": {
|
||||
"sha256": "fbd51422fa5e6449000e4520e6639f6135c7393165a5ae02b1e92892edba0480",
|
||||
"mode": 420,
|
||||
"index": "100644 46b0672fea90cd7cb672f7d119706c240474f8ca 0\tworkplans/KEY-WP-0028-browser-client-field-validation.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0029-scope-reassessment.md": {
|
||||
"sha256": "3241f3b94db9cad5e3770cf1fedc5c211288357e4f69b5ba9da57065094bdf0e",
|
||||
"mode": 420,
|
||||
"index": "100644 392f97750f6cb8b12868a6fd016c7d396b58477e 0\tworkplans/KEY-WP-0029-scope-reassessment.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0030-tenant-precondition-guard.md": {
|
||||
"sha256": "c94c3afc7fb4270dfc18f3fe8a32946c524e4a6ba2d02a3d8fda69c9b8441af0",
|
||||
"mode": 420,
|
||||
"index": "100644 0f74e0d5a037eec64b11b57c67b02057a3b0bfef 0\tworkplans/KEY-WP-0030-tenant-precondition-guard.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0031-gate-house-tenant-ruling.md": {
|
||||
"sha256": "0abb135367f0366aa071769c4bd76fdeb321d810ec75ad3b58d718734f78382b",
|
||||
"mode": 420,
|
||||
"index": "100644 d2ede9d494927976ccb9ba6a5aeaf1ed21c30837 0\tworkplans/KEY-WP-0031-gate-house-tenant-ruling.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0032-principal-type-provenance-guard.md": {
|
||||
"sha256": "6a5cd483a40b9477227b02156c42b3469094dac145b33727b336118898aa782a",
|
||||
"mode": 420,
|
||||
"index": "100644 a04f52f817a51efc1d0d0ecb99efacea8dc53a17 0\tworkplans/KEY-WP-0032-principal-type-provenance-guard.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0033-vergabe-fresh-login.md": {
|
||||
"sha256": "c01bb51cf2dcad06ea846040dddef09845041fa7271cd7015e5a34a49e60bb8f",
|
||||
"mode": 420,
|
||||
"index": "100644 5b478f953890339aec796c11a536af0ec3238789 0\tworkplans/KEY-WP-0033-vergabe-fresh-login.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0034-account-recovery.md": {
|
||||
"sha256": "7db9adcd418ccabfbce672aec283249a6e374212492f0349e8c47a568f130eda",
|
||||
"mode": 420,
|
||||
"index": "100644 eb58f99bc26e3c3ac33230dfe432c8b1fabe47ce 0\tworkplans/KEY-WP-0034-account-recovery.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0035-optional-mfa.md": {
|
||||
"sha256": "c60ea8d0c195686c0d2317f657aad2de27c9b9432d311627c0e1d06124cabb49",
|
||||
"mode": 420,
|
||||
"index": "100644 29eb711f656c91808e91f3981a9168c3a0ebc253 0\tworkplans/KEY-WP-0035-optional-mfa.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0036-platform-factor-recovery.md": {
|
||||
"sha256": "5ceb08fe5789b4f02550bb04fcaf8eeb25a43ac3f506dfc62d60a513bcd0013f",
|
||||
"mode": 420,
|
||||
"index": "100644 fd296bc05a6e565b0ef6e88a9ae3daa7c9f81786 0\tworkplans/KEY-WP-0036-platform-factor-recovery.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md": {
|
||||
"sha256": "3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a",
|
||||
"mode": 420,
|
||||
"index": "100644 ad7fed77a27aa8487693ad6fcf342502f418427a 0\tworkplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0038-forgejo-oidc-client.md": {
|
||||
"sha256": "dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064",
|
||||
"mode": 420,
|
||||
"index": "100644 8cd5bc66f7b0115eec7813d6eec994a1bb618668 0\tworkplans/KEY-WP-0038-forgejo-oidc-client.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0039-openbao-negative-probe-tokens.md": {
|
||||
"sha256": "618600946f6dd4128206db7ab23da9cc96e22cb84e0fd301b1b2eacd5c9bdba4",
|
||||
"mode": 420,
|
||||
"index": "100644 b22aa6a1116412ab60586163a4fd759270f1b6bf 0\tworkplans/KEY-WP-0039-openbao-negative-probe-tokens.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0040-mfa-attended-run-findings.md": {
|
||||
"sha256": "98b23d8810bff047067169232213cb313ecc14ff79751224842723afe808fbd2",
|
||||
"mode": 420,
|
||||
"index": "100644 8e8730c8e7733b13d6a9f42689a9d8e407769e81 0\tworkplans/KEY-WP-0040-mfa-attended-run-findings.md\n"
|
||||
},
|
||||
"workplans/KEY-WP-0041-sign-in-diagnosis-page.md": {
|
||||
"sha256": "7e44f46acd6b3942c1852b49e0d64b205dfd7d9bb378aed5241b1bf22d7ce88a",
|
||||
"mode": 420,
|
||||
"index": "100644 56fd05819e3acd37b8e6cb468ec3d62307f1ae7a 0\tworkplans/KEY-WP-0041-sign-in-diagnosis-page.md\n"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: ADHOC-2026-09-05
|
||||
type: workplan
|
||||
title: "Repository review and layer declaration"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-05"
|
||||
state_hub_workstream_id: "6f5a12f0-5dac-5e02-bbd9-9ab2263264ba"
|
||||
---
|
||||
|
||||
## Resolve declaration intake and normalize repository records
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-05-T01
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "54c89abe-2539-550d-bc3a-649af8d4580a"
|
||||
```
|
||||
|
||||
Reviewed KEY-IN-0001 against local security-layer-model_v0.4. Declared Tooling
|
||||
and the authentication/authorization boundary in INTENT.md and closed the intake.
|
||||
Normalized two legacy completed workplan frontmatter statuses to finished.
|
||||
Ignored the generated .repo-manager workstation cache. Existing task statuses
|
||||
already conform to the State Hub task lifecycle canon.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
id: ADHOC-2026-09-07
|
||||
type: workplan
|
||||
title: "Document the KEY-WP-0016 authorization-code bindings for consumers"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "c2fdc5cc-89be-561d-b832-f0b3019ea1c7"
|
||||
---
|
||||
|
||||
## Publish the consumer-facing rollout note
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-07-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "2187cef2-07c0-5ac1-89ca-3bb25a160fa4"
|
||||
```
|
||||
|
||||
KEY-WP-0016 changed `/token` and `/userinfo` behaviour without a consumer-facing
|
||||
note; no document in `docs/` mentioned `redirect_uri` at all, so the change would
|
||||
have reached a deployment silently.
|
||||
|
||||
Published `docs/authorization-code-bindings.md` stating what an exchange must
|
||||
send, who is affected and how to roll out. Verified the blast radius rather than
|
||||
assuming it: all five browser registrations in `config/dev-config.yaml`
|
||||
(`openbao-admin`, `demo-app`, `netkingdom-bootstrap-console`,
|
||||
`user-engine-portal`, `coulomb-social`) are `clientType: public` with
|
||||
`grantTypes: ["authorization_code"]`, so the grant-type and confidential-client
|
||||
bindings cannot affect them and only the `redirect_uri` requirement can. Named
|
||||
the realistic failure — a client that sends `redirect_uri` to `/authorize` but
|
||||
omits it at `/token` — and said plainly that it has not been observed against a
|
||||
live consumer. Referenced the note from SCOPE.md.
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
id: ADHOC-2026-09-27
|
||||
type: workplan
|
||||
title: "Fresh MFA no longer backdated to a stale kc_login cookie (INFD-IN-0005)"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
created: "2026-09-27"
|
||||
updated: "2026-09-27"
|
||||
state_hub_workstream_id: "5217dd06-73cb-50f8-ba64-528e421b29de"
|
||||
---
|
||||
|
||||
## Stop completeAuthorization from reusing a stale prior session's IssuedAt after real MFA
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-27-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ce98e4af-cfaa-5620-8f21-15ade18ee9e8"
|
||||
```
|
||||
|
||||
`informed-decision` intake `INFD-IN-0005` (open, `origin_ref: SECRETS-WP-0010`):
|
||||
platform-root's approval of `infd-20260927-b01` was refused by flex-auth's
|
||||
`compact_sitting` policy with `policy_denied`, despite platform-root having just
|
||||
completed a fresh key-cape MFA sign-in. flex-auth's 900-second review-freshness
|
||||
check reads `assurance.at`, which key-cape derives from the PKCE session's
|
||||
`AuthTime`.
|
||||
|
||||
Root cause in `src/internal/server/oidc/authorize.go`,
|
||||
`AuthorizeHandler.completeAuthorization`: whenever a `kc_login` cookie already
|
||||
existed for the same username, its `IssuedAt` was unconditionally reused as
|
||||
`authTime` — even on the two call sites that had just called
|
||||
`MFA.ValidateMFAToken` successfully in this exact request (the callback's
|
||||
`mfaToken != ""` branch and `serveMFASubmission`). A user who had an older
|
||||
browser session and then stepped up with a fresh MFA challenge got the *old*
|
||||
session's timestamp stamped onto the *new*, just-proven authentication —
|
||||
understating freshness for every downstream binding-grade check, exactly as
|
||||
`INFD-IN-0005` describes ("completeAuthorization currently copies a prior
|
||||
timestamp... only after actual reauthentication").
|
||||
|
||||
The reuse itself is correct when MFA was **not** just performed in this
|
||||
request (`decision.MFAVerified` carried forward from an existing AAL2 session,
|
||||
via `domain.DecideAssurance`) — reporting mint time there would overstate
|
||||
freshness (`TestAssuranceReportsAuthenticationTimeNotMintTime`). The two cases
|
||||
were conflated under a single `mfaVerified` bool.
|
||||
|
||||
Fix: added a `freshMFA bool` parameter to `completeAuthorization`, `true` only
|
||||
at the two call sites that just validated an MFA token in this request, `false`
|
||||
at the carried-forward-assurance call site. `authTime` now falls back to the
|
||||
prior session's `IssuedAt` only when `!freshMFA`.
|
||||
|
||||
Added `TestAuthorizeCallback_MFASubmission_FreshEvenWithStalePriorLoginCookie`,
|
||||
which seeds a 4-hour-old `kc_login` cookie for the same user, then submits a
|
||||
valid MFA token and asserts the resulting `AuthTime` is within a minute of now.
|
||||
Confirmed it fails against the pre-fix code (`AuthTime` ~4h old) and passes
|
||||
after. Full suite (`go test ./...`) green, no other test needed adjustment —
|
||||
the carried-forward-assurance case (`TestPolicy_...`, `TestLogout_...`,
|
||||
`assurance_time_test.go`) is unaffected since none of it exercises a fresh MFA
|
||||
submission with a pre-existing cookie for the same user.
|
||||
|
||||
Does not touch flex-auth (its 900s window is correct; it was consuming a bad
|
||||
timestamp) or informed-decision (correctly refuses to interpret the PDP's
|
||||
denial). `INFD-IN-0005` can close once informed-decision confirms platform-root
|
||||
can approve after a fresh sign-in against this build.
|
||||
|
||||
## Discovery/JWKS fetch was missing the split-horizon origin headers
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-27-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Rolling out T01 (image digest `sha256:248e449a...`) broke every KeyCape login,
|
||||
including unrelated clients (`user-engine-portal`), with `auth_failure` /
|
||||
`provider_metadata_unavailable` at `/api/oidc/token` on the KeyCape side and
|
||||
Authelia logging `invalid X-Forwarded-Proto header value 'http'` on
|
||||
`GET /.well-known/openid-configuration` from KeyCape's pod IP.
|
||||
|
||||
Root cause, unrelated to T01's change: `AutheliaAdapter.setPublicOriginHeaders`
|
||||
(the NK-WP-0041-T02/path-B fix, key-cape@3b0446e) annotates only the token
|
||||
exchange (`newTokenRequest`) with `X-Forwarded-Proto`/`X-Forwarded-Host` so
|
||||
Authelia can derive the public issuer for a split-horizon (in-cluster HTTP,
|
||||
public HTTPS) deployment. `idTokenVerifier.get` in `verify.go` — used for the
|
||||
provider-metadata and JWKS fetches that verify every upstream ID token — never
|
||||
got the same treatment; it built a bare request with only an `Accept` header.
|
||||
Authelia 4.38 fails closed on the resulting unmarked in-cluster request. This
|
||||
gap predates today's session (present since 3b0446e); it surfaced now because
|
||||
this was the first login attempted since that image went live.
|
||||
|
||||
Fix: extracted `setPublicOriginHeaders` from an `AutheliaAdapter` method to a
|
||||
package-level function taking the target public base URL explicitly, and gave
|
||||
`idTokenVerifier` its own resolved `publicBaseURL` (same
|
||||
`BrowserBaseURL`-else-`BaseURL` fallback as the adapter) so `get()` applies
|
||||
the same headers. Added `TestDiscoveryFetch_CarriesPublicOriginHeaders`
|
||||
(confirmed it fails without the fix, passes with it). Full suite green.
|
||||
|
||||
New image `key-cape@<pending>` still needs a rollout to `sso-mfa/k8s/keycape/deployment.yaml`
|
||||
before platform-root can sign in again.
|
||||
|
|
@ -0,0 +1,499 @@
|
|||
---
|
||||
id: KEY-WP-0001
|
||||
type: workplan
|
||||
title: "KeyCape Implementation — Lightweight IAM Profile"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: Bernd
|
||||
topic_slug: netkingdom
|
||||
topic_id: a6c6e745-bf54-4465-9340-1534a2be493e
|
||||
repo_id: 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c
|
||||
created: 2026-03-13
|
||||
updated: 2026-03-13
|
||||
spec_refs:
|
||||
- wiki/KeyCapeSpecification_v0.1.md
|
||||
- wiki/KeyCapeSpecificationPack_v0.1.md
|
||||
decisions:
|
||||
- id: ADR-0001
|
||||
title: "Implementation language: Go"
|
||||
hub_decision_id: 620beb04-fa3f-4a9d-9806-02890a7a2b0d
|
||||
status: accepted
|
||||
ref: docs/adr/ADR-0001-choose-go-for-keycape.md
|
||||
state_hub_workstream_id: "0d34dfc1-7ccb-4bd5-b872-5c7379b9adce"
|
||||
---
|
||||
|
||||
# KEY-WP-0001 — KeyCape Implementation
|
||||
|
||||
Implements the **NetKingdom IAM Profile** via KeyCape: a stateless, profile-constrained OIDC
|
||||
server orchestrating Authelia, LLDAP, and privacyIDEA. Replaceable by Keycloak without
|
||||
application changes.
|
||||
|
||||
## Language Decision
|
||||
|
||||
**Go** — decided 2026-03-13 by Bernd. See `docs/adr/ADR-0001-choose-go-for-keycape.md`.
|
||||
|
||||
KeyCape is an orchestrating boundary service (HTTP, adapters, JWT via library, CLI tooling) —
|
||||
Go's strongest domain. Rust revisitable if a subcomponent needs stronger guarantees later.
|
||||
Mandatory guardrails: typed domain models, narrow adapter interfaces, layered architecture,
|
||||
fuzz tests on validator/redirect/claim-mapping, no cleverness.
|
||||
|
||||
## Repo Structure (from spec §12)
|
||||
|
||||
```
|
||||
src/
|
||||
server/
|
||||
oidc/ # Profile endpoints
|
||||
telemetry/ # Structured event emission
|
||||
errors/ # Error taxonomy + enforcement middleware
|
||||
adapters/
|
||||
authelia/ # Auth flow delegation
|
||||
lldap/ # Identity directory reads
|
||||
privacyidea/ # MFA enforcement
|
||||
validator/ # Canonical LDAP schema validator binary
|
||||
migration/
|
||||
lldap-export/ # LLDAP → canonical
|
||||
keycape-to-keycloak/ # Canonical → Keycloak realm import
|
||||
lldap-to-ldap/ # LLDAP → OpenLDAP/389DS/AD LDIF
|
||||
spec/
|
||||
canonical-model.yaml # Source of truth for all identity data
|
||||
ldap-schema.yaml # Canonical LDAP schema rules
|
||||
tests/
|
||||
profile/ # Scenario A — lightweight baseline
|
||||
negative/ # Scenario D — unsupported feature rejection
|
||||
migration/ # Scenarios B & C — replacement
|
||||
```
|
||||
|
||||
## Dependency Order
|
||||
|
||||
```
|
||||
T01 (project setup)
|
||||
└─ T02 (canonical model) T04 (error taxonomy)
|
||||
└─ T03 (LDAP validator) └─ T13 (telemetry)
|
||||
└─ T10 (LLDAP adapter) └─ T14 (enforcement layer)
|
||||
└─ T11 (Authelia) │
|
||||
└─ T12 (privacyIDEA) │
|
||||
│ │
|
||||
T05 ─ T06 ─ T07 ─ T08 ─┴─ T09 (OIDC server)
|
||||
│
|
||||
T18 (profile tests / Scenario A)
|
||||
T21 (negative tests / Scenario D)
|
||||
│
|
||||
T15 → T16 → T19 (Scenario B)
|
||||
T15 → T17 → T20 (Scenario C)
|
||||
│
|
||||
T22 (dev stack)
|
||||
T23 (production packaging)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Foundations
|
||||
|
||||
## T01 — Project setup: Go module, repo layout, CI skeleton
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T01
|
||||
hub_task_id: 25613e3f-2a65-409e-afaa-d23ded0bc256
|
||||
priority: high
|
||||
status: done
|
||||
state_hub_task_id: "38822bc0-4189-4909-874e-ea40e5771250"
|
||||
```
|
||||
|
||||
Initialise language module in `src/`. Create directory skeleton per spec §12. Add Makefile
|
||||
targets: `build`, `test`, `lint`. Set up CI (build + test on every push). Scaffolding only —
|
||||
no application code. **Agent must call `record_decision()` with chosen language (Go or Rust).**
|
||||
|
||||
## T02 — Canonical identity model: machine-readable schema
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T02
|
||||
hub_task_id: deee2929-9386-41db-bf91-fbd9ad646c28
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T01]
|
||||
state_hub_task_id: "940c118b-c1e6-4dda-bd4c-4fac105822be"
|
||||
```
|
||||
|
||||
Write `spec/canonical-model.yaml`. Six entities: User, Group, Role, Client, Membership,
|
||||
MFAEnrollment (fields per spec §2). Include JSON Schema or CUE schema for programmatic
|
||||
validation. This file is the **source of truth** — all other code derives from it.
|
||||
|
||||
## T03 — Canonical LDAP schema + validator
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T03
|
||||
hub_task_id: 02592c65-db23-474b-b06b-019e95df8146
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T01, T02]
|
||||
state_hub_task_id: "c1715d70-f10f-45e9-b73a-b54a3d360342"
|
||||
```
|
||||
|
||||
Write `spec/ldap-schema.yaml`: tree layout (`ou=users`, `ou=groups`, `ou=clients` under
|
||||
`dc=netkingdom,dc=local`), object classes (`inetOrgPerson`, `groupOfNames`), required/optional
|
||||
attributes. Implement `validator/` binary. Structural rules: valid DN, required attrs, no unknown
|
||||
attrs, valid group memberships. Semantic rules: referenced users exist, no cycles, usernames
|
||||
unique, email format valid. Validator runs in `--mode=ci|provisioning|migration`. Emits
|
||||
machine-readable report.
|
||||
|
||||
## T04 — Error taxonomy: types, JSON format, middleware
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T04
|
||||
hub_task_id: 46870fd6-0672-432b-8824-6bc2e24811b3
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T01]
|
||||
state_hub_task_id: "6e3b6b97-ac77-44c5-959e-be12751f1b63"
|
||||
```
|
||||
|
||||
Implement four error types (spec §5):
|
||||
- `feature_not_supported_by_profile`
|
||||
- `available_in_keycloak_mode_only`
|
||||
- `rejected_for_profile_safety`
|
||||
- `invalid_profile_usage`
|
||||
|
||||
JSON format: `{"error": "...", "description": "...", "feature": "..."}`. HTTP middleware wraps
|
||||
all handler errors. Error type strings are stable and test-assertable.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — OIDC Server
|
||||
|
||||
## T05 — OIDC discovery endpoint (/.well-known/openid-configuration)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T05
|
||||
hub_task_id: 92eb8916-cb22-4786-9f16-a8a07272f818
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T04]
|
||||
state_hub_task_id: "0dbc08e3-c465-4c37-a219-832a580bedfd"
|
||||
```
|
||||
|
||||
`GET /.well-known/openid-configuration`. Advertise **only** profile-supported features:
|
||||
`authorization_code`, S256 PKCE, RS256, static scopes. Must NOT advertise: dynamic registration,
|
||||
implicit flow. Issuer configurable. Cacheable response.
|
||||
|
||||
## T06 — Authorization endpoint with PKCE and redirect URI validation
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T06
|
||||
hub_task_id: c3df620b-9864-4ff1-ba6d-26057c6f4d59
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T04, T11, T12, T13, T14]
|
||||
state_hub_task_id: "cdb4b06d-3d54-49dd-ac05-ca9ed6d7322f"
|
||||
```
|
||||
|
||||
`GET/POST /authorize`. Validate: `client_id` (static config), `redirect_uri` (exact match —
|
||||
wildcard → `rejected_for_profile_safety`), `response_type=code`, `scope` contains `openid`,
|
||||
`code_challenge` present (missing → `invalid_profile_usage`), `code_challenge_method=S256`.
|
||||
Delegate to Authelia adapter. Store PKCE state server-side. No implicit or hybrid flow.
|
||||
|
||||
## T07 — Token endpoint: JWT/RS256, canonical claim mapping
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T07
|
||||
hub_task_id: d3248f4d-e0e9-4144-9844-c9768dc896d6
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T06, T08, T10]
|
||||
state_hub_task_id: "534d8616-90de-4d32-961c-c2ef719642e4"
|
||||
```
|
||||
|
||||
`POST /token`. Validate PKCE `code_verifier`. Issue RS256 JWT via standard library (no custom
|
||||
crypto). Mandatory claims: `iss`, `sub` (canonical user ID), `aud`, `exp`, `iat`. Optional
|
||||
claims: `preferred_username` (LDAP `uid`), `email` (LDAP `mail`), `groups` (groupOfNames),
|
||||
`roles`. Short, explicitly configured token lifetime. Any other grant type →
|
||||
`feature_not_supported_by_profile`.
|
||||
|
||||
## T08 — JWKS endpoint (/jwks)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T08
|
||||
hub_task_id: 58a1d705-b788-4d66-8c0a-33edff63a885
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T01]
|
||||
state_hub_task_id: "7e2167be-bcc7-49c2-8681-e518abd5bc0c"
|
||||
```
|
||||
|
||||
`GET /jwks`. RS256 public key in JWK Set format. Key loaded from config. Key rotation: serve
|
||||
multiple keys during rotation window, keyed by `kid`. Standard library key generation only.
|
||||
|
||||
## T09 — Userinfo endpoint (/userinfo)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T09
|
||||
hub_task_id: 742d3924-21e9-4304-86e9-0400af0e81ee
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T07, T10]
|
||||
state_hub_task_id: "78094ca5-a831-4443-9ccf-fc476ff87b91"
|
||||
```
|
||||
|
||||
`GET /userinfo`. Optional per spec — implement if any registered client requires it. Validate
|
||||
Bearer token (RS256 + expiry). Return claim subset scoped to granted scopes. Claims must be
|
||||
identical to ID token for same scopes. If no client needs it: stub returning
|
||||
`available_in_keycloak_mode_only`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Backend Adapters
|
||||
|
||||
## T10 — LLDAP adapter: user and group reads
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T10
|
||||
hub_task_id: 2043b10a-6822-45f8-abcc-4e233d918fb0
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T02, T03]
|
||||
state_hub_task_id: "97d19662-f482-4ea5-84fd-9fccb84ff317"
|
||||
```
|
||||
|
||||
`adapters/lldap`. LDAP protocol connection to LLDAP. Interface: `LookupUser(username) → canonical
|
||||
User`, `LookupGroups(userDN) → []Group`, `ValidatePassword(username, password) → bool`. Attribute
|
||||
map: `uid→username`, `cn→displayName`, `mail→email`, `memberOf→groups`. Run canonical LDAP schema
|
||||
validator on every read. No LDAP internals exposed to `server/`.
|
||||
|
||||
## T11 — Authelia adapter: session and auth flow delegation
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T11
|
||||
hub_task_id: ad129a14-1552-4717-b1dd-b529d18ce681
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T04, T13]
|
||||
state_hub_task_id: "6461865b-f57c-4591-9cf3-68c79af22723"
|
||||
```
|
||||
|
||||
`adapters/authelia`. Initiate auth redirect to Authelia, receive callback, extract authenticated
|
||||
user identity, hand off to MFA check (T12). Must not leak Authelia session tokens/cookies into
|
||||
profile layer. Unavailable Authelia → fail closed (`auth_failure` event).
|
||||
|
||||
## T12 — privacyIDEA adapter: MFA enforcement
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T12
|
||||
hub_task_id: 1ef196e6-2304-4cf6-b205-47ac1da879ec
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T02, T13]
|
||||
state_hub_task_id: "e403a783-c856-4d6d-b859-a9cad7545fe1"
|
||||
```
|
||||
|
||||
`adapters/privacyidea`. **KeyCape must NOT implement MFA logic.** Interface:
|
||||
`CheckMFARequired(user) → bool`, `ValidateMFAToken(user, token) → bool`. MFA failure → no token
|
||||
issued + `auth_failure` telemetry. MFA enrollment state from canonical `MFAEnrollment` entity.
|
||||
privacyIDEA remains stable across lightweight → expanded migration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Telemetry & Enforcement
|
||||
|
||||
## T13 — Telemetry pipeline: structured event emission
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T13
|
||||
hub_task_id: 704146bf-cd60-4922-b18b-3d209cff3ac3
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T01]
|
||||
state_hub_task_id: "4df7bda1-5b84-4b4c-9b16-bcb1d3cca096"
|
||||
```
|
||||
|
||||
`server/telemetry`. Event types (spec §6.1): `auth_start`, `auth_success`, `auth_failure`,
|
||||
`token_issued`, `unsupported_feature`, `invalid_request`, `migration_event`. Required fields
|
||||
(spec §6.2): `timestamp`, `client_id`, `endpoint`, `feature`, `result`, `error_type`, `scopes`,
|
||||
`grant_type`, `environment`, `trace_id`. Pluggable outputs: structured log (default), Prometheus
|
||||
metrics endpoint. Every auth and error path emits an event — **no silent paths**.
|
||||
|
||||
## T14 — Unsupported feature enforcement layer
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T14
|
||||
hub_task_id: 71f44886-ab61-4160-a435-72b35af472a0
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T04, T13]
|
||||
state_hub_task_id: "ae16fba9-5bb4-4780-ac77-558e3ed7e1dd"
|
||||
```
|
||||
|
||||
`server/errors` enforcement middleware. Intercept any parameter, grant type, scope, or client
|
||||
config exceeding the profile. Return correct error type + emit `unsupported_feature` telemetry.
|
||||
Maintain a **registry** of unsupported features (adding new ones requires no handler changes):
|
||||
`dynamic_client_registration`, `identity_broker`, `wildcard_redirect_uri`, `implicit_flow`, etc.
|
||||
Every registry entry must have a corresponding test in T21.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Migration Tooling
|
||||
|
||||
## T15 — Migration: LLDAP → canonical export
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T15
|
||||
hub_task_id: 1bd13f76-2d62-429d-b230-d785ef6a3f2f
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T02, T03, T10]
|
||||
state_hub_task_id: "f7549cd7-33f0-4407-a656-ab8f5a184e64"
|
||||
```
|
||||
|
||||
`migration/lldap-export` tool. Read all users, groups, memberships, attributes from LLDAP. Map
|
||||
to canonical model. Validate against LDAP schema validator before writing. Output:
|
||||
`canonical-export.yaml`. Emit `migration_event` telemetry. Idempotent. Include incompatibility
|
||||
report for unmappable LLDAP data.
|
||||
|
||||
## T16 — Migration: canonical → Keycloak import
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T16
|
||||
hub_task_id: f3d50e80-f6b5-4c0c-a5f3-08308ea1a95e
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T15]
|
||||
state_hub_task_id: "96486c41-9f33-42a5-b7b6-ad0a9eb2bdee"
|
||||
```
|
||||
|
||||
`migration/keycape-to-keycloak` tool. Read canonical export (T15). Transform to Keycloak realm
|
||||
import format: users, groups, clients, roles, scope mappings. Preserve: same issuer, same claims,
|
||||
same scopes, same client behavior. Output: `keycloak-realm-import.json`. Emit `migration_event`.
|
||||
Include round-trip validation report.
|
||||
|
||||
## T17 — Migration: LLDAP → full LDAP (OpenLDAP / 389DS / AD)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T17
|
||||
hub_task_id: 044d99c8-39cb-4f35-9308-912ae829bd22
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T15]
|
||||
state_hub_task_id: "1ec335a2-80ca-4c34-b08e-211f537e4214"
|
||||
```
|
||||
|
||||
`migration/lldap-to-ldap` tool. Export via T15 canonical export. Generate LDIF for target
|
||||
(`--target=openldap|389ds|ad`). Migrate: users, groups, memberships, attributes. Run validator
|
||||
on output LDIF before import. Produce validation report. **Orthogonal to T16** — the two
|
||||
migration dimensions are independent (spec §14.1).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Replacement Tests
|
||||
|
||||
## T18 — Profile test suite: Scenario A (lightweight baseline)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T18
|
||||
hub_task_id: 76abc3f6-9c4e-4aca-9995-72b728925812
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T05, T06, T07, T08, T09, T22]
|
||||
state_hub_task_id: "1b0e9f26-d441-42b8-b532-1eb713fb355d"
|
||||
```
|
||||
|
||||
`tests/profile`. Provision canonical fixtures into LLDAP + Authelia + KeyCape. Test categories
|
||||
(spec §15.3): discovery, login flow (PKCE), token claim assertions (all mandatory + optional),
|
||||
redirect validation, client config, MFA policy, logout (if implemented). Tests are
|
||||
**backend-agnostic** — same suite runs in T19 and T20. Must pass for Scenario A conformance.
|
||||
|
||||
## T19 — Replacement test suite: Scenario B (IAM swap, same directory)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T19
|
||||
hub_task_id: 56d03e89-934b-4992-bfe4-b32f275882e3
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T18, T16]
|
||||
state_hub_task_id: "a02d24e7-32de-4be6-935c-896c10dde020"
|
||||
```
|
||||
|
||||
Run T18 suite against Keycloak + LLDAP (configured from T16 canonical export). **No test code
|
||||
changes allowed.** Migration successful only if all T18 tests pass. Proves IAM replaceability
|
||||
without directory migration.
|
||||
|
||||
## T20 — Replacement test suite: Scenario C (full expansion)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T20
|
||||
hub_task_id: ec3cae5c-9942-4be7-acc0-1eb9f02aba45
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T19, T17]
|
||||
state_hub_task_id: "545f319f-053d-48bd-8d94-c8c05cd56736"
|
||||
```
|
||||
|
||||
Apply T17 LLDAP→OpenLDAP migration, then T16 Keycloak import. Run T18 suite. Migration successful
|
||||
only if all tests pass. privacyIDEA must remain stable (no MFA re-enrollment required).
|
||||
|
||||
## T21 — Negative profile tests: Scenario D (unsupported feature rejection)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T21
|
||||
hub_task_id: a5112b63-121f-4d17-ac1a-fb46d160413e
|
||||
priority: high
|
||||
status: done
|
||||
depends_on: [T14]
|
||||
state_hub_task_id: "5856afe0-2a9e-4489-b057-35e59f86c359"
|
||||
```
|
||||
|
||||
`tests/negative`. For every entry in T14 unsupported-feature registry: attempt usage, assert
|
||||
correct `error.error` string, assert `unsupported_feature` telemetry event emitted. Covered
|
||||
cases: `dynamic_client_registration`, `implicit_flow`, wildcard redirect URIs, identity brokering,
|
||||
missing PKCE, unknown scopes, unknown grant types. **Must run in CI.** Proves enforcement layer
|
||||
is complete.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Operations
|
||||
|
||||
## T22 — Dev mode stack: docker-compose with LLDAP + Authelia
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T22
|
||||
hub_task_id: e840963f-cd19-4b38-857a-7c40df165d3d
|
||||
priority: medium
|
||||
status: done
|
||||
depends_on: [T01]
|
||||
state_hub_task_id: "b98f2671-a20a-4438-99c9-fbe0e5324534"
|
||||
```
|
||||
|
||||
`docker-compose.dev.yml`: KeyCape, LLDAP, Authelia, privacyIDEA (or stub). Pre-seeded with
|
||||
canonical fixtures from T02. Makefile targets: `make dev` (start + verify basic auth flow),
|
||||
`make seed` (re-apply fixtures without full restart). Deterministic: same seed → same state.
|
||||
Test environment for T18 and T21.
|
||||
|
||||
## T23 — Slim production packaging: binary + config + health
|
||||
|
||||
```task
|
||||
id: KEY-WP-0001-T23
|
||||
hub_task_id: d5723683-f739-4362-b62e-71213dc5a89e
|
||||
priority: low
|
||||
status: done
|
||||
depends_on: [T18, T21]
|
||||
state_hub_task_id: "8c1752c2-7fb3-4da5-aab3-6b7acf12ea64"
|
||||
```
|
||||
|
||||
Single stateless binary. Declarative YAML config: profile version, client definitions, backend
|
||||
connections, LDAP schema rules, privacyIDEA settings, telemetry destination, token lifetime,
|
||||
issuer URL. Static config validation on startup. `/healthz` endpoint. Minimal container image
|
||||
(distroless or Alpine). Config environment-promotable via env var overrides only.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria (from spec §15.4 and §20)
|
||||
|
||||
A release is conformant when:
|
||||
|
||||
- [x] Scenario A tests pass (T18) — `src/tests/profile/profile_test.go` (8 tests)
|
||||
- [x] Scenario D tests pass (T21) — `src/tests/negative/negative_test.go` (8 tests)
|
||||
- [x] Scenario B tests pass (T19) — `src/tests/migration/scenario_b_test.go` (7 tests)
|
||||
- [x] Scenario C tests pass (T20) — `src/tests/migration/scenario_c_test.go` (6 tests)
|
||||
- [x] All error responses use taxonomy types from spec §5 — `internal/errors/taxonomy.go`
|
||||
- [x] All auth/error paths emit structured telemetry (T13) — `internal/server/telemetry/`
|
||||
- [x] Canonical LDAP schema validator passes on all fixtures (T03) — `internal/validator/`
|
||||
- [x] No handwritten cryptography anywhere in the codebase — stdlib `crypto/rsa` only
|
||||
- [x] Config is statically validated at startup (T23) — `internal/config/validate.go`
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
---
|
||||
id: KEY-WP-0002
|
||||
type: workplan
|
||||
title: "KeyCape Container Image — Build & Publish to Gitea OCI Registry"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: netkingdom
|
||||
topic_slug: netkingdom
|
||||
created: "2026-03-22"
|
||||
updated: "2026-03-21"
|
||||
capability_request_id: ""
|
||||
state_hub_workstream_id: "c8843c7a-460a-47a2-b45a-b8d3940f9aa2"
|
||||
---
|
||||
|
||||
# KEY-WP-0002 — KeyCape Container Image — Build & Publish to Gitea OCI Registry
|
||||
|
||||
## Problem
|
||||
|
||||
KeyCape has a `Dockerfile` but no automated build pipeline and no published
|
||||
image. Other services (k3s deployments, local dev) that need to run KeyCape
|
||||
must build locally from source. There is no versioned artefact to reference
|
||||
in Helm charts or manifests.
|
||||
|
||||
The capability request for this work was originally misrouted to railiance.
|
||||
It belongs here: KeyCape owns its own image.
|
||||
|
||||
## Goal
|
||||
|
||||
Produce a versioned OCI image for KeyCape, published to the Gitea container
|
||||
registry on CoulombCore (`92.205.130.254:32166`), triggered automatically on
|
||||
every merge to `main` and on semver tags (`v*`).
|
||||
|
||||
**Gitea OCI registry endpoint:** `92.205.130.254:32166`
|
||||
**Image name:** `92.205.130.254:32166/coulomb/key-cape`
|
||||
|
||||
> **Why Gitea, not GHCR?**
|
||||
> The net-kingdom cluster is self-hosted. Keeping images in Gitea (also
|
||||
> self-hosted on CoulombCore) avoids any external registry dependency and
|
||||
> keeps image pulls within the cluster network. GHCR is a future option
|
||||
> once public distribution is needed.
|
||||
|
||||
## Design
|
||||
|
||||
### Image naming & tagging
|
||||
|
||||
| Trigger | Tags applied |
|
||||
|---------|-------------|
|
||||
| push to `main` | `latest`, `main-<short-sha>` |
|
||||
| tag `v1.2.3` | `1.2.3`, `1.2`, `1`, `latest` |
|
||||
|
||||
### Build
|
||||
|
||||
Multi-stage Dockerfile already present — no changes needed to the build
|
||||
itself. The image builds to a distroless static binary (~10 MB).
|
||||
|
||||
### Registry auth
|
||||
|
||||
Gitea issues a personal access token (or machine account token) with
|
||||
`write:packages` scope. Stored as Gitea Actions secret `REGISTRY_TOKEN`;
|
||||
username stored as `REGISTRY_USER`.
|
||||
|
||||
For local `make push`, credentials are passed via `docker login` before
|
||||
the push target runs.
|
||||
|
||||
### Makefile targets
|
||||
|
||||
```makefile
|
||||
IMAGE_REGISTRY ?= 92.205.130.254:32166
|
||||
IMAGE_REPO ?= coulomb/key-cape
|
||||
IMAGE_TAG ?= latest
|
||||
IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG)
|
||||
|
||||
image:
|
||||
docker build -t $(IMAGE) .
|
||||
|
||||
push: image
|
||||
docker push $(IMAGE)
|
||||
|
||||
image-tag:
|
||||
docker tag $(IMAGE) $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG)
|
||||
```
|
||||
|
||||
### Gitea Actions workflow
|
||||
|
||||
`.gitea/workflows/image.yaml` — triggers on push to `main` and on `v*` tags:
|
||||
- Checkout
|
||||
- Set up Docker Buildx
|
||||
- Login to `92.205.130.254:32166` using secrets
|
||||
- Build and push with metadata-action tags
|
||||
- (Optional) sign with cosign if available
|
||||
|
||||
### k3s insecure registry
|
||||
|
||||
Gitea runs over plain HTTP on port 32166 (NodePort). k3s must be configured
|
||||
to treat this endpoint as an insecure registry so image pulls work from
|
||||
within the cluster:
|
||||
|
||||
```yaml
|
||||
# /etc/rancher/k3s/registries.yaml (on CoulombCore)
|
||||
mirrors:
|
||||
"92.205.130.254:32166":
|
||||
endpoint:
|
||||
- "http://92.205.130.254:32166"
|
||||
```
|
||||
|
||||
k3s picks this up on restart (or SIGHUP). Worker nodes (if any) need the
|
||||
same file.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### T01 — Makefile: image, push, image-tag targets
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "749472fc-edb9-4948-9ebc-58d5f38327ee"
|
||||
```
|
||||
|
||||
Add `image`, `push`, and `image-tag` targets to `Makefile` with
|
||||
`IMAGE_REGISTRY`, `IMAGE_REPO`, `IMAGE_TAG` variables defaulting to the
|
||||
Gitea endpoint and `coulomb/key-cape:latest`.
|
||||
|
||||
Gate: `make image` builds successfully locally; `IMAGE_TAG=dev make image`
|
||||
produces a differently-tagged image.
|
||||
|
||||
---
|
||||
|
||||
### T02 — Gitea Actions workflow
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8ecf18cc-a3bb-4ede-a09c-fcd0d26d7f9d"
|
||||
```
|
||||
|
||||
Create `.gitea/workflows/image.yaml`:
|
||||
- Trigger: `push` to `main`, `push` tags matching `v*`
|
||||
- Runner: `act_runner` label (or `ubuntu-latest` if configured)
|
||||
- Steps: checkout → docker buildx → login → build+push
|
||||
- Tags via `docker/metadata-action`: `latest` on main, semver on tags
|
||||
|
||||
Secrets required (document in README.md under "CI"):
|
||||
- `REGISTRY_USER` — Gitea username or machine account
|
||||
- `REGISTRY_TOKEN` — Gitea personal access token with `write:packages`
|
||||
|
||||
Gate: workflow file is syntactically valid; documented in README.
|
||||
|
||||
---
|
||||
|
||||
### T03 — k3s insecure registry config on CoulombCore
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2dde67f9-944f-418d-a2e9-7367bc556425"
|
||||
```
|
||||
|
||||
On CoulombCore, create/update `/etc/rancher/k3s/registries.yaml` to add
|
||||
the Gitea NodePort as an HTTP mirror. Restart k3s (or send SIGHUP) and
|
||||
verify `crictl pull 92.205.130.254:32166/coulomb/key-cape:latest` works.
|
||||
|
||||
Gate: image pull from within the cluster succeeds without TLS errors.
|
||||
|
||||
---
|
||||
|
||||
### T04 — Create Gitea machine account & token
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "25775e10-3164-4adb-9c41-835c86fde5f8"
|
||||
```
|
||||
|
||||
In Gitea (http://92.205.130.254:32166), create a machine account
|
||||
`ci-netkingdom` (or reuse an existing service account) with access to
|
||||
the `netkingdom` organisation. Generate a token with `write:packages`
|
||||
scope and store it in:
|
||||
- Gitea Actions secrets on the `key-cape` repo: `REGISTRY_USER`, `REGISTRY_TOKEN`
|
||||
- The net-kingdom credential store (SOPS-encrypted) under
|
||||
`credentials/gitea-ci-token.enc.yaml`
|
||||
|
||||
Gate: `docker login 92.205.130.254:32166` succeeds with the token;
|
||||
secret is in the credential store.
|
||||
|
||||
---
|
||||
|
||||
### T05 — Smoke test: push and pull a dev image
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0f6ab38f-6d34-41af-9180-f19c687947b5"
|
||||
```
|
||||
|
||||
Manually trigger a build-and-push:
|
||||
```bash
|
||||
docker login 92.205.130.254:32166
|
||||
IMAGE_TAG=dev make push
|
||||
```
|
||||
|
||||
Then verify the image is pullable from CoulombCore:
|
||||
```bash
|
||||
# on CoulombCore
|
||||
crictl pull 92.205.130.254:32166/coulomb/key-cape:dev
|
||||
```
|
||||
|
||||
Gate: pull succeeds; image is listed in Gitea -> Packages -> coulomb/key-cape.
|
||||
|
||||
---
|
||||
|
||||
### T06 — Update README with registry & CI docs
|
||||
|
||||
```task
|
||||
id: KEY-WP-0002-T06
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "946cd34d-94da-4fa9-a781-ed36f6c827a3"
|
||||
```
|
||||
|
||||
Add a "Container Image" section to `README.md` documenting:
|
||||
- Registry URL and image name
|
||||
- How to pull (`docker pull 92.205.130.254:32166/coulomb/key-cape:latest`)
|
||||
- How to build and push locally (Makefile targets)
|
||||
- CI secrets required for the Actions workflow
|
||||
|
||||
Gate: README section present and accurate.
|
||||
|
||||
---
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- [ ] `make image` and `make push` work locally
|
||||
- [ ] `.gitea/workflows/image.yaml` present and documented
|
||||
- [ ] k3s can pull the image from Gitea without TLS errors
|
||||
- [ ] Machine account token stored in credential store
|
||||
- [ ] Smoke test: `dev` image pushed and pulled successfully
|
||||
- [ ] README updated
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
id: KEY-WP-0003
|
||||
type: workplan
|
||||
title: "Bootstrap Console OIDC Login and MFA Verification"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-05-24"
|
||||
updated: "2026-05-24"
|
||||
state_hub_workstream_id: "02990009-a2b3-44f6-a579-487fbacae41a"
|
||||
---
|
||||
|
||||
# KEY-WP-0003 - Bootstrap Console OIDC Login and MFA Verification
|
||||
|
||||
## Problem
|
||||
|
||||
The NetKingdom security bootstrap console now acts as a local OIDC client
|
||||
callback so the operator can verify the dedicated platform-root login before
|
||||
approving custody mode. The current live KeyCape deployment rejects that flow
|
||||
with:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid_profile_usage",
|
||||
"description": "redirect_uri does not match any registered URI",
|
||||
"feature": "redirect_uri"
|
||||
}
|
||||
```
|
||||
|
||||
That error is correct profile enforcement: KeyCape only accepts exact
|
||||
registered redirect URIs. The live `demo-app` registration has not yet been
|
||||
updated to allow the local bootstrap console callback:
|
||||
|
||||
- `http://127.0.0.1:8876/oidc/callback`
|
||||
- `http://localhost:8876/oidc/callback`
|
||||
|
||||
After that is fixed, there is a second usability/security gap. KeyCape checks
|
||||
privacyIDEA MFA after the Authelia callback, but the browser flow currently
|
||||
expects an `mfa_token` query parameter instead of presenting a proper OTP
|
||||
challenge page to the human operator.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the bootstrap console's "Start demo OIDC login" button a real
|
||||
end-to-end verification path for the current lightweight IAM stack:
|
||||
|
||||
1. KeyCape accepts the bootstrap console callback URI by exact registration.
|
||||
2. The browser leaves KeyCape for the public Authelia login URL.
|
||||
3. After password login, KeyCape presents a minimal MFA challenge when
|
||||
privacyIDEA requires one.
|
||||
4. KeyCape issues an OIDC authorization code to the bootstrap console callback.
|
||||
5. The console can exchange the code and let the operator mark
|
||||
`OIDC login verified` without exposing tokens or secrets.
|
||||
|
||||
This keeps KeyCape's security posture intact: no wildcard redirect URIs, no
|
||||
dynamic client registration, no token display, and no storage of OTP material.
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Prefer a dedicated public client named `netkingdom-bootstrap-console` for
|
||||
long-lived clarity. Reusing `demo-app` is acceptable for the immediate
|
||||
unblock only if the deployment/runbook clearly labels it as a bootstrap test
|
||||
client.
|
||||
- The bootstrap callback is local-only and operator-attended. It must be an
|
||||
exact URI in config, not a wildcard or dynamic registration exception.
|
||||
- Browser-facing Authelia redirects must use the public Authelia base URL
|
||||
(`https://auth.coulomb.social`) so the human login page opens correctly.
|
||||
- KeyCape may still need an internal service URL for back-channel token
|
||||
exchange. If so, split the current single Authelia URL into browser-facing
|
||||
authorize URL and internal token URL instead of making the browser use an
|
||||
in-cluster hostname.
|
||||
- The MFA prompt should collect only a one-time code, post it back to KeyCape,
|
||||
validate with privacyIDEA, and then continue the normal OIDC code flow.
|
||||
- This work unblocks the NetKingdom custody gate in
|
||||
`NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**2026-05-24:** Implemented in source:
|
||||
|
||||
- added `netkingdom-bootstrap-console` as a public OIDC client in the sample
|
||||
KeyCape config, while keeping the local callback registered on `demo-app`
|
||||
for compatibility,
|
||||
- split Authelia browser redirects from server-side token exchange via
|
||||
`browserBaseURL` and `tokenBaseURL`,
|
||||
- added a browser MFA challenge page at `POST /authorize/callback` that
|
||||
validates the one-time code with privacyIDEA before issuing the downstream
|
||||
OIDC authorization code,
|
||||
- updated NetKingdom's `keycape-config` generation template and bootstrap
|
||||
console to use the dedicated client,
|
||||
- added regression tests for callback registration, split Authelia URLs, MFA
|
||||
challenge rendering, valid OTP continuation, and invalid OTP failure.
|
||||
|
||||
Live use still requires deployment: build/publish the updated KeyCape image,
|
||||
refresh the live `keycape-config` Secret through the custodian age-key unlock
|
||||
ceremony, and restart the KeyCape deployment.
|
||||
|
||||
---
|
||||
|
||||
## T01 - Register the bootstrap console callback client
|
||||
|
||||
```task
|
||||
id: KEY-WP-0003-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b396c99f-d711-475a-9cba-4f03a1db561d"
|
||||
```
|
||||
|
||||
Add a KeyCape client registration for the bootstrap console. Either create a
|
||||
dedicated `netkingdom-bootstrap-console` public client or update `demo-app`
|
||||
temporarily with these exact redirect URIs:
|
||||
|
||||
- `http://127.0.0.1:8876/oidc/callback`
|
||||
- `http://localhost:8876/oidc/callback`
|
||||
|
||||
Update the sample config, tests, and deployment/runbook references so the
|
||||
registered client is reproducible and not just a live-cluster patch.
|
||||
|
||||
Gate: an authorize request using the local callback no longer returns
|
||||
`invalid_profile_usage` for `redirect_uri`.
|
||||
|
||||
## T02 - Separate browser-facing and internal Authelia URLs if needed
|
||||
|
||||
```task
|
||||
id: KEY-WP-0003-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "46172e6d-3e11-493c-b223-79c2fc321aec"
|
||||
```
|
||||
|
||||
Confirm whether the current `authelia.baseURL` is safe to use for both browser
|
||||
redirects and server-side token exchange. If not, add explicit configuration
|
||||
for the browser authorize base URL and internal token/userinfo base URL.
|
||||
|
||||
Gate: the first browser redirect leaves `https://kc.coulomb.social` for
|
||||
`https://auth.coulomb.social/...`; server-side token exchange still works from
|
||||
inside the deployment.
|
||||
|
||||
## T03 - Add a browser MFA challenge step
|
||||
|
||||
```task
|
||||
id: KEY-WP-0003-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "92fca4d0-6215-4ea6-9f80-9178ae183acb"
|
||||
```
|
||||
|
||||
When `CheckMFARequired` returns true after the Authelia callback, render a
|
||||
minimal KeyCape MFA challenge page instead of requiring `mfa_token` in the
|
||||
callback query string. The page should:
|
||||
|
||||
- show the authenticated username and client display name,
|
||||
- collect only the OTP code,
|
||||
- preserve the pending OIDC state server-side,
|
||||
- validate with privacyIDEA,
|
||||
- continue to issue the normal authorization code on success,
|
||||
- fail closed with the existing telemetry on invalid MFA.
|
||||
|
||||
Gate: a user enrolled in privacyIDEA can complete password + OTP in the
|
||||
browser and is returned to the registered downstream callback.
|
||||
|
||||
## T04 - Add end-to-end profile tests for the bootstrap login path
|
||||
|
||||
```task
|
||||
id: KEY-WP-0003-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "079a5929-1864-4461-a64c-746cebca469d"
|
||||
```
|
||||
|
||||
Add tests that cover:
|
||||
|
||||
- local bootstrap callback registration,
|
||||
- rejection of unregistered callbacks remains intact,
|
||||
- Authelia browser redirect uses the expected public URL,
|
||||
- MFA-required login presents a challenge instead of immediate failure,
|
||||
- invalid OTP fails closed,
|
||||
- valid OTP produces an authorization code bound to the original PKCE session.
|
||||
|
||||
Gate: `make test` passes and the negative redirect URI tests remain green.
|
||||
|
||||
## T05 - Document the live rollout ceremony
|
||||
|
||||
```task
|
||||
id: KEY-WP-0003-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "1d67225d-a20b-4e36-9b2e-20836be2f439"
|
||||
```
|
||||
|
||||
Document the deployment path for updating live KeyCape config without
|
||||
regenerating unrelated secrets. The runbook must fit the NetKingdom custodian
|
||||
age-key model: decrypt or unlock only during an attended ceremony, apply the
|
||||
updated client registration/config, restart KeyCape, and remove plaintext
|
||||
secret material afterward.
|
||||
|
||||
Gate: an operator can update the live `keycape-config` Secret and verify the
|
||||
bootstrap console OIDC login without printing or committing secrets.
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
---
|
||||
id: KEY-WP-0004
|
||||
type: workplan
|
||||
title: "Binky Hedgehog GmbH as first NetKingdom tenant + qonto runtime workload identity"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-07-23"
|
||||
updated: "2026-07-29"
|
||||
state_hub_workstream_id: "817709be-5756-5ef7-9ccd-dcb84b6b9c64"
|
||||
---
|
||||
|
||||
# Binky Hedgehog GmbH as first NetKingdom tenant + qonto runtime workload identity
|
||||
|
||||
Binky Hedgehog GmbH ("binky") becomes the **first real tenant** provisioned in
|
||||
the NetKingdom IAM stack, per the NetKingdom IAM Profile v0.2's tenant claim
|
||||
model (`net-kingdom/canon/standards/iam-profile_v0.2.md` §"Tenant Claim") —
|
||||
distinct from `tenant:platform` (control plane) and `tenant:coulomb` (the
|
||||
ecosystem-development tenant this workstation already operates under).
|
||||
|
||||
This closes **Gap #1** flagged in `qonto-assistant`'s
|
||||
`workplans/QONTO-WP-0003-mcp-surface.md` closure: no OIDC issuer existed
|
||||
anywhere in the fleet, so `qonto-assistant`'s MCP surface shipped with an
|
||||
interim shared-secret bearer token instead of real workload identity.
|
||||
`key-cape` is "Implementation complete (v0.1)" and already implements the
|
||||
profile's `client_credentials`/workload-identity grant for service accounts
|
||||
— it's the component that should close this gap, not `user-engine`
|
||||
(downstream user/profile data layer, not an identity provider) or
|
||||
`identity-canon` (terminology research only).
|
||||
|
||||
**Depends on:** `key-cape` v0.1 (done, `KEY-WP-0001`), NetKingdom IAM Profile
|
||||
v0.2 tenant + service-account claim rules (already ratified canon — no new
|
||||
profile work needed, this is provisioning against an existing contract), and
|
||||
the cross-repo runtime direction now tracked in
|
||||
`railiance-master/workplans/RMASTER-WP-0019-knative-qonto-runtime-on-reef-railiance.md`.
|
||||
|
||||
This workplan no longer targets a permanent direct `qonto-assistant`
|
||||
deployment shape. Its workload-identity lane is intended for the future
|
||||
`rapp-qonto` runtime on `rail-knative` hosted by `reef-railiance`, with
|
||||
current `qonto-assistant` consumption allowed only as a migration bridge until
|
||||
that package/rail path exists.
|
||||
|
||||
## Task: Confirm tenant identifier and provision the tenant plane
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c3bdbdb6-6b3f-51b5-8521-d67f113f1c3d"
|
||||
```
|
||||
|
||||
**Decided 2026-07-23 (Bernd):** the IAM profile's `tenant:customer:<name>` /
|
||||
`tenant:sandbox:<name>` suggested identifiers name a tenant by **capability
|
||||
role** (customer, vendor, ...) — but tenants already known to hold multiple
|
||||
roles at once (see `net-kingdom/docs/princedom-isolation-exploration.md`'s
|
||||
unratified `PLTF`/`IAM`/`VEN`/`CUS` role model; Binky itself is both `CUS`
|
||||
and, later, `VEN`). Using a role word as the tenant *grouping* would collide
|
||||
with that separate role dimension. Decision: classify tenants by an
|
||||
onboarding-risk / entity-shape **grouping**, orthogonal to capability role:
|
||||
|
||||
```text
|
||||
trial — test/trial/showcase tenants only
|
||||
friendly — known, easily reached, tolerant of experimentation/instability
|
||||
single — one-person business entities (freelance consultants)
|
||||
small — up to 10 employees at time of onboarding (attoo)
|
||||
medium — up to 100 employees (attoo)
|
||||
large — up to 1000 employees (attoo)
|
||||
enterprise — 1001+ employees (attoo)
|
||||
consumer — private individuals
|
||||
family — a legal family
|
||||
community — a non-legal group of people
|
||||
association — a legal association of people
|
||||
agentic — financially enabled AI entities
|
||||
```
|
||||
|
||||
`tenant:<grouping>:<name>` replaces `tenant:customer:<name>` /
|
||||
`tenant:sandbox:<name>` as the identifier shape. **Binky maps to `friendly`**
|
||||
— known, reachable, tolerant of early instability — giving
|
||||
**`tenant:friendly:binky`** (slug matches the OpenBao path already in
|
||||
production, `tenants/binky/qonto-api`).
|
||||
|
||||
**Open question, not yet resolved by this decision:** this grouping list
|
||||
classifies tenants as external-shaped entities: it has no obvious slot for
|
||||
`tenant:coulomb` itself (the platform's own internal/reference tenant,
|
||||
ADR-0006) — none of `trial`/`friendly`/`single`/.../`agentic` fit "the
|
||||
platform building itself." Confirm with Bernd whether `tenant:coulomb` stays
|
||||
an ungrouped special case (alongside `tenant:platform`) or gets folded into
|
||||
this scheme somehow, before treating the grouping list as complete.
|
||||
|
||||
**Also open:** this is a fleet-wide vocabulary decision — the ratified
|
||||
`net-kingdom/canon/standards/iam-profile_v0.2.md` "Tenant Claim" section
|
||||
still documents the old `customer`/`sandbox` suggested identifiers, and
|
||||
NetKingdom owns that canon per ADR-0011. Recording this grouping list only
|
||||
in this key-cape workplan lets it drift from canon. Recommend a follow-up:
|
||||
either a decision record or an ADR in `net-kingdom` adopting this grouping
|
||||
list as the tenant-identifier standard, superseding the old suggested
|
||||
identifiers. Not drafted here — flagging so it isn't lost.
|
||||
|
||||
Provision whatever tenant/org-unit boundary key-cape's current backend
|
||||
(LLDAP/Authelia) needs so tokens issued for subjects under this tenant
|
||||
correctly carry `tenant:friendly:binky` and nothing implies platform-root
|
||||
authority for it (profile rule: tenant administration for a tenant must
|
||||
never imply platform-root authority).
|
||||
|
||||
Done when: `tenant:coulomb`'s place in the grouping scheme is resolved; the
|
||||
`net-kingdom` canon-update question above is answered (drafted or explicitly
|
||||
deferred); a test-subject token carries the `tenant:friendly:binky` claim.
|
||||
|
||||
2026-07-27: IAM Profile v0.2 now preserves `tenant:platform` and
|
||||
`tenant:coulomb` as reserved ungrouped tenants and ratifies the external
|
||||
grouping vocabulary. The live service-token proof carries
|
||||
`tenant:friendly:binky` without platform authority.
|
||||
|
||||
## Task: Provision bernd.worsch@binky-hedgehog.com as tenant-admin
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "202c419e-838f-5c2f-8a52-9b1c36a6d6b9"
|
||||
```
|
||||
|
||||
Create the user in key-cape's current backend, enroll MFA per profile
|
||||
requirements (`assurance` claim, `net-kingdom/canon/standards/iam-profile_v0.2.md`),
|
||||
and assign a tenant-admin role/group scoped to `tenant:friendly:binky` only.
|
||||
|
||||
2026-07-27 direction update: do not complete this through a one-shot operator
|
||||
script. `USER-WP-0020` and `NK-WP-0023` now own a reusable self-service and
|
||||
administration portal plus NetKingdom provisioning adapters. This task is
|
||||
their first production acceptance case.
|
||||
|
||||
The `binky-hedgehog.com` mailbox must exist and be reachable for account
|
||||
verification, but mailbox availability is now an input to the reusable
|
||||
registration flow rather than a manual provisioning procedure.
|
||||
|
||||
Done when: `bernd.worsch@binky-hedgehog.com` completes OIDC/PKCE + MFA login
|
||||
and receives a token carrying `tenant:friendly:binky` and a tenant-admin role
|
||||
claim; the same token does **not** grant access to `tenant:platform` or any
|
||||
other tenant.
|
||||
|
||||
2026-07-28: user-engine's deployed portal API created the tenant-admin
|
||||
membership and provisioned/linked the LLDAP identity through the reusable
|
||||
NetKingdom adapter. The identity is intentionally in
|
||||
`password_setup_required`; LLDAP has no SMTP configuration for its native
|
||||
reset delivery. First-password handoff, MFA enrollment, and final human-token
|
||||
claim/denial evidence remain. No operator-set password or raw credential was
|
||||
used as a shortcut.
|
||||
|
||||
2026-07-28 update: the reusable password handoff is now live at the canonical
|
||||
KeyCape host and is issued/renewed through the user-engine administration UI.
|
||||
Links are opaque, expire after 15 minutes, are single use, and older links are
|
||||
revoked on renewal. A disposable-user live test proved password registration,
|
||||
directory login, replay denial, and cleanup. This task now awaits only the
|
||||
Binky user's own password choice, MFA enrollment, and final scoped-token
|
||||
acceptance.
|
||||
|
||||
2026-07-29: the first portal attempt correctly denied cross-tenant access
|
||||
because KeyCape had not projected the established `net-kingdom-admins` group
|
||||
to the new platform-operator role. Commit `90a2078` adds and tests the explicit
|
||||
`tenant:platform` / `platform-operator` compatibility mapping and is deployed
|
||||
Ready. The operator must start a fresh OIDC session so the corrected claims
|
||||
are minted.
|
||||
|
||||
2026-07-29 human acceptance: the Binky administrator completed the reusable
|
||||
password setup, enrolled a privacyIDEA TOTP factor, and successfully signed in
|
||||
through KeyCape with password plus OTP. Enrollment revealed that privacyIDEA's
|
||||
default QR label exposed only the token serial. The live
|
||||
`coulomb-friendly-token-labels` enrollment policy now emits issuer `Coulomb`
|
||||
and label `{user}@{realm}` for future tokens; existing authenticator entries
|
||||
must be renamed locally because wallets do not accept remote label updates.
|
||||
The live token then carried `tenant:friendly:binky`, roles `user` and
|
||||
`tenant-admin`, only the Binky users/admins groups, and AAL2 assurance with
|
||||
`pwd` + `otp` and `mfa: true`. The same session was denied access to both
|
||||
`tenant:platform` and `tenant:coulomb` administration. Acceptance complete.
|
||||
|
||||
## Task: Register a workload-identity OIDC client for the qonto runtime
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "0d3ccd35-bad5-5c30-95a3-61fce584425d"
|
||||
```
|
||||
|
||||
Register an OIDC client per the profile's service-account/workload-identity
|
||||
flow (`client_credentials` or the documented workload-token-exchange
|
||||
equivalent — §"Service Account Flow"), scoped to `tenant:friendly:binky`,
|
||||
with the minimum role/scope the qonto runtime surface actually needs. The
|
||||
intended steady-state consumer is the future `rapp-qonto` runtime on
|
||||
`rail-knative`; current `qonto-assistant` callers may use the same lane only
|
||||
as a documented migration bridge until that package split exists. No
|
||||
interactive login for this client — it's for agent-harness sessions and other
|
||||
automated callers, not humans.
|
||||
|
||||
The resulting `client_secret` must never be pasted into chat, Git, this
|
||||
workplan, or a State Hub message or progress event — hand-off happens only
|
||||
through T04's secrets-engine/OpenBao lane.
|
||||
|
||||
Done when: a `client_credentials` token exchange against key-cape succeeds
|
||||
for this client and produces a short-lived service token carrying
|
||||
`tenant:friendly:binky`; the `client_id`, granted scopes, and intended future
|
||||
binding to `rapp-qonto` are documented here; no secret material appears
|
||||
anywhere in this repo's history.
|
||||
|
||||
2026-07-26: Added `docs/qonto-runtime-identity-contract.md` with stable
|
||||
principal/client names, least-privilege claims, non-secret evidence, and the
|
||||
automation/custody boundary. Actual client issuance remains open.
|
||||
|
||||
2026-07-27: `KEY-WP-0006` delivered `rapp-qonto-client`. Live exchange
|
||||
produced only the documented tenant, role, and `qonto:read` scope; wrong
|
||||
secret and excessive scope were denied.
|
||||
|
||||
## Task: Route client_secret custody through secrets-engine + OpenBao
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9cb09dbf-1fd0-55c5-9c45-b3b7296ecd91"
|
||||
```
|
||||
|
||||
Coordinate with `secrets-engine` to create a scoped OpenBao lane for this
|
||||
`client_secret`, mirroring the existing `tenants/binky/qonto-api` pattern
|
||||
already in production use for the Qonto bank credential itself. The steady
|
||||
target is a dedicated future runtime role for `rapp-qonto`; if a temporary
|
||||
bridge via the current `qonto-assistant` runtime role is needed before the
|
||||
package split lands, it must be recorded as temporary rather than treated as
|
||||
the final contract.
|
||||
|
||||
Done when: the intended qonto runtime role can fetch the `client_secret` via
|
||||
`secrets-engine`/OpenBao; any temporary `qonto-assistant` bridge is explicitly
|
||||
marked transitional; Bernd never handles the raw value at any point in this
|
||||
flow.
|
||||
|
||||
2026-07-26: `warden plan` classified first-time provisioning as one explicit
|
||||
founder authority act through `warden desk`. No secret was requested or read.
|
||||
All repeatable verification and revocation work is designated for sanctioned
|
||||
agent execution and later functional automation.
|
||||
|
||||
2026-07-27: Custody is established at
|
||||
`platform/workloads/rapp-qonto/keycape-client`, with runtime injection through
|
||||
`sso/keycape-rapp-qonto-client`. No raw value entered evidence or human
|
||||
handling.
|
||||
|
||||
## Task: Register the credential lane with ops-warden
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T05
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "39f1f803-732e-5905-97d9-c7493c66c8ee"
|
||||
```
|
||||
|
||||
This is the task that directly minimizes Bernd's ongoing engagement: add an
|
||||
entry to ops-warden's routing catalog (`registry/routing/catalog.yaml`)
|
||||
pointing at this new OIDC client / login lane, following the pattern already
|
||||
documented in `ops-warden/SCOPE.md`'s "Issue vs route" table ("Login / OIDC /
|
||||
MFA → key-cape / Keycloak → Assist — route; proxy `login` lane when
|
||||
`exec_capable`"). ops-warden does not mint tokens or take custody here — it
|
||||
routes/assists, same as every other credential lane it fronts. The lane should
|
||||
describe the future `rapp-qonto` runtime path on `rail-knative`, not only the
|
||||
current source repo.
|
||||
|
||||
Once the OpenBao lane from T04 exists, run ops-warden's workload security
|
||||
posture conformance checker
|
||||
(`ops-warden/scripts/check_secret_posture_conformance.py`) against the qonto
|
||||
runtime workload that will later become `rapp-qonto`; until the split lands,
|
||||
`qonto-assistant` may be used as the migration subject if that temporary state
|
||||
is recorded. Confirm it meets the expected maturity tier (dev/test/prod
|
||||
posture, M0–M3 workload maturity) before this goes live with real
|
||||
credentials, and record the result (pass or documented exception).
|
||||
|
||||
Done when: `warden route show` (or equivalent) resolves this lane correctly;
|
||||
posture conformance check passes or exceptions are explicitly recorded; from
|
||||
this point on, discovering and using this credential lane needs `warden
|
||||
access`, not direct key-cape/OpenBao admin knowledge.
|
||||
|
||||
2026-07-26: Routing requirements and required conformance evidence are now
|
||||
documented. Catalog publication waits for the real client and OpenBao role so
|
||||
the route does not advertise an unusable lane.
|
||||
|
||||
2026-07-27: Published route `rapp-qonto-keycape-client`; route resolution and
|
||||
the metadata-only M3/prod posture check both pass.
|
||||
|
||||
## Task: automation@binky-hedgehog.com service identity (deferred)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T06
|
||||
status: cancel
|
||||
priority: low
|
||||
state_hub_task_id: "fbee11fc-7a60-5dd7-8522-f80ddb4aea55"
|
||||
```
|
||||
|
||||
Forward-looking per Bernd's framing ("I guess it will later be helpful") —
|
||||
**do not provision until a concrete automation workflow actually needs its
|
||||
own identity** distinct from the qonto runtime workload client from T03.
|
||||
Dormant service-account credentials are themselves a posture risk (unused
|
||||
attack surface, nothing to rotate against), so this stays parked rather than
|
||||
front-loaded.
|
||||
|
||||
When a real consuming workflow exists: same pattern as T02 but scoped to an
|
||||
automation-appropriate role (not tenant-admin), likely paired with its own
|
||||
`client_credentials` registration for whichever agent/service authenticates
|
||||
as it, following T03/T04/T05's pattern rather than inventing a new one.
|
||||
|
||||
Done when: explicitly triggered by a named consuming workflow — until then
|
||||
this task stays `todo` and does not block closing the rest of this workplan.
|
||||
|
||||
## Task: Closure review
|
||||
|
||||
```task
|
||||
id: KEY-WP-0004-T07
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "77b0556f-62fd-5046-9ba1-718246151a6f"
|
||||
```
|
||||
|
||||
Mark workplan finished when T01–T05 are done (T06 may legitimately remain
|
||||
`todo`/parked by design — see its own task note). Confirm end to end: a
|
||||
human token (bernd.worsch@binky-hedgehog.com, tenant-admin) and a service
|
||||
token (future qonto runtime client, with any temporary `qonto-assistant`
|
||||
bridge explicitly recorded) both resolve to `tenant:friendly:binky` and
|
||||
nothing else; the credential lane is discoverable via `warden access` without
|
||||
Bernd touching key-cape/OpenBao admin surfaces directly. The runtime follow-up
|
||||
then continues in `QONTO-WP-0004` and `RMASTER-WP-0019`. Run
|
||||
`statehub fix-consistency`.
|
||||
|
||||
2026-07-29 closure: T01–T05 are complete, T06 is intentionally cancelled,
|
||||
the human Binky flow passed AAL2 and tenant-isolation acceptance, and the qonto
|
||||
workload client passed its positive-scope and negative credential/scope checks.
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
---
|
||||
id: KEY-WP-0005
|
||||
type: workplan
|
||||
title: "IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-07-23"
|
||||
updated: "2026-07-23"
|
||||
state_hub_workstream_id: "711a8303-69aa-5db6-9043-92530c6dc81b"
|
||||
---
|
||||
|
||||
# IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles
|
||||
|
||||
Discovered while planning `tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md`:
|
||||
`key-cape`'s token issuance (`src/internal/server/oidc/token.go`) does not
|
||||
currently emit `tenant`, `principal_type`, `groups`, `roles`, or
|
||||
`assurance` — none of `net-kingdom/canon/standards/iam-profile_v0.3.md`'s
|
||||
Core Claims are implemented yet, only `preferred_username`/`email`/`groups`
|
||||
(LDAP-sourced, not the profile's normalized `groups` claim) and OIDC's own
|
||||
base claims (`nonce`, etc.).
|
||||
|
||||
This is bigger than "add the `tenant_roles` claim on top of existing tenant
|
||||
support" — `key-cape` isn't yet profile-conformant at all on the tenant/
|
||||
principal/assurance axis. `tenant_roles` (the reason this workplan exists)
|
||||
is the last, optional claim in that same set (v0.3's addition on top of
|
||||
v0.2's baseline) — implementing it in isolation without the claims it
|
||||
depends on for meaning (`tenant`, `principal_type`) would be incoherent.
|
||||
|
||||
**This workplan is registered, not implemented, by the same session that
|
||||
found the gap.** Token issuance is security-sensitive code in a Go codebase
|
||||
with its own adapter conventions (`internal/adapters/{lldap,privacyidea,authelia}`)
|
||||
that deserves dedicated context before changing, not a rushed edit from a
|
||||
first read of `token.go`. Scoped and sequenced here so the work isn't lost.
|
||||
|
||||
**Depends on:** `net-kingdom/canon/standards/iam-profile_v0.3.md` (ratified).
|
||||
**Related:** `tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md`
|
||||
(the consumer waiting on `tenant_roles`); `key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md`
|
||||
(Binky's onboarding, which needs `tenant` claim support to produce a
|
||||
correct `tenant:friendly:binky` token at all).
|
||||
|
||||
## Task: Emit `tenant`, `principal_type`, `groups`, `roles`, `assurance`
|
||||
|
||||
```task
|
||||
id: KEY-WP-0005-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "32d5fbdc-cd2b-5bc8-9d2b-1a410b55e806"
|
||||
```
|
||||
|
||||
Bring `token.go`'s claim assembly up to `iam-profile_v0.3.md`'s Core Claims
|
||||
table baseline (the v0.2-originated set):
|
||||
|
||||
- `tenant`: resolve from whatever user/service/agent-to-tenant mapping
|
||||
`key-cape`'s LLDAP/Authelia backend can provide today; if no such mapping
|
||||
exists yet, that's itself a sub-task — this claim cannot be stamped from
|
||||
nothing.
|
||||
- `principal_type`: `human` for the existing Authorization Code + PKCE flow;
|
||||
`service` for client-credentials flows (check whether `key-cape` already
|
||||
issues client-credentials tokens at all — `grant_types_supported` in the
|
||||
discovery contract claims it does, but that needs verifying against real
|
||||
code, the same way this workplan verified `tenant` claim absence).
|
||||
- `groups`, `roles`: normalize from LDAP group/role data into the profile's
|
||||
flat string-array shape.
|
||||
- `assurance`: `{level, methods, mfa, source, at}` per the profile —
|
||||
`privacyIDEA`-backed MFA evidence maps to `aal2`; single-factor maps to
|
||||
`aal1`.
|
||||
|
||||
Done when: `tools/iam-profile-conformance/` (already exists) passes against
|
||||
these claims for both human and service token issuance paths.
|
||||
|
||||
**Done 2026-07-24, human flow only — scope split made explicit, not
|
||||
silently narrowed:** verified first (as flagged): `grant_types_supported`
|
||||
advertises `client_credentials` in `discovery.go`, but `token.go`'s
|
||||
`ServeHTTP` only ever accepted `grant_type == "authorization_code"` — there
|
||||
is no service-token issuance path at all, so "verifying against service
|
||||
token issuance" isn't possible until that path exists. Building a
|
||||
`client_credentials` grant handler from scratch is a distinct, materially
|
||||
larger piece of work than extending the existing human flow (new endpoint
|
||||
logic, new client-authentication semantics, no existing tests to extend)
|
||||
— **not attempted in this pass**, left open below rather than declared done.
|
||||
|
||||
What shipped for the human Authorization Code + PKCE flow:
|
||||
|
||||
- `domain.User.Tenant` (new field, `omitempty`, empty-safe for existing
|
||||
YAML configs) + `token.go`'s `effectiveTenant()`: falls back to
|
||||
`tenant:coulomb` (this workstation's actual current tenant, ADR-0006)
|
||||
when unset — never emits an empty `tenant` claim, never silently assigns
|
||||
a user to a tenant they weren't given.
|
||||
- `principal_type: "human"`, unconditional.
|
||||
- `groups`/`roles`: promoted from scope-gated (only `groups`, only with the
|
||||
`groups` scope) to unconditional core claims, always present as `[]`
|
||||
when empty, never `null` (`nonNilStrings()`). One pre-existing test
|
||||
asserted the old scope-gated behavior for `groups`
|
||||
(`TestTokenHandler_ScopeFiltering_ProfileScope`) — updated to assert the
|
||||
new, intentional behavior, not silently left failing or reverted.
|
||||
- `assurance`: built from whether MFA was *actually verified in this
|
||||
session* (`PKCESession.MFAVerified`, new field, threaded through
|
||||
`completeAuthorization`'s two call sites in `authorize.go`), not from
|
||||
static enrollment state — `aal2` only when MFA was required and passed
|
||||
this time, `aal1` otherwise.
|
||||
|
||||
Verified for real: `go build ./...` and `go vet ./...` clean; `go test
|
||||
./...` green across the whole repo (one pre-existing test updated to match
|
||||
the new intentional behavior, described above, not silently broken). Two
|
||||
new `authorize_test.go` cases directly assert `PKCESession.MFAVerified` is
|
||||
set correctly on both the MFA-required and MFA-not-required paths.
|
||||
`tests/profile/profile_test.go`'s `TestCompleteTokenFlow` — the repo's own
|
||||
full HTTP integration test (real server, real PKCE flow, real JWT decode)
|
||||
— extended with real assertions for all five new/changed claims, not just
|
||||
presence checks: `tenant == "tenant:coulomb"` (the fallback path, since
|
||||
the test's `testuser` has no explicit tenant), `principal_type == "human"`,
|
||||
`groups == ["developers"]`, `roles == []`, and
|
||||
`assurance == {level: "aal1", mfa: false, source: "key-cape", ...}` (this
|
||||
test's `mockMFA` never requires MFA, so `aal1` is the correct expected
|
||||
value, not `aal2`).
|
||||
|
||||
Not run: `net-kingdom/tools/iam-profile-conformance`'s Python conformance
|
||||
tool against a live minted token — it needs a reachable issuer, which would
|
||||
mean standing up the full `docker-compose.dev.yml` stack
|
||||
(Authelia+LLDAP+privacyIDEA) beyond what's practical in this pass. The
|
||||
equivalent claim-shape and value checks it would perform are covered by
|
||||
`TestCompleteTokenFlow`'s real HTTP round trip instead — not a like-for-like
|
||||
substitute, but real coverage, not an assumption.
|
||||
|
||||
**Explicitly still open** (not this task, not silently dropped):
|
||||
`client_credentials` grant handling — service-token issuance for `key-cape`
|
||||
callers with no human in the loop (`tenant-engine`'s own eventual key-cape
|
||||
integration, and any other service caller). Needs its own scoped follow-up:
|
||||
a new `principal_type: "service"` code path in `token.go` (or a sibling
|
||||
handler), client secret/authentication semantics, and its own test suite —
|
||||
this workplan's `T02` (`tenant_roles`) doesn't strictly require it, since
|
||||
`tenant_roles` attaches to whatever principal_type a token already carries,
|
||||
human included.
|
||||
|
||||
## Task: Emit `tenant_roles` (optional, cached)
|
||||
|
||||
```task
|
||||
id: KEY-WP-0005-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c3ee6021-b28d-50a5-be60-ecb5631f56e9"
|
||||
```
|
||||
|
||||
At token-issuance time, call `tenant-engine`'s cache-read endpoint
|
||||
(`GET /tenants/{tenant_id}/roles`) using the `tenant` claim resolved in T01,
|
||||
and stamp the result as the optional `tenant_roles` claim
|
||||
(`iam-profile_v0.3.md`, "Tenant Roles" section). Follows the same adapter
|
||||
pattern as `internal/adapters/{lldap,privacyidea,authelia}` — a new
|
||||
`internal/adapters/tenantengine` package, not inline HTTP calls in
|
||||
`token.go`.
|
||||
|
||||
Fail open on this claim specifically, not closed: `tenant_roles` is
|
||||
documented as a cache callers must not trust for privileged decisions
|
||||
anyway (`flex-auth` re-validates live). If `tenant-engine` is unreachable at
|
||||
issuance time, omit the claim rather than fail the entire token issuance —
|
||||
losing a performance optimization is acceptable; blocking login because a
|
||||
cache source is down is not. This is the one place in the whole `tenant_roles`
|
||||
design where fail-open is correct, precisely because the live-lookup path
|
||||
exists as the real gate.
|
||||
|
||||
Done when: token issuance still succeeds with `tenant_roles` omitted when
|
||||
`tenant-engine` is unreachable (test simulates the outage); present and
|
||||
correct when reachable, for a tenant with known role grants.
|
||||
|
||||
**Done 2026-07-24:** New `internal/adapters/tenantengine` package
|
||||
(`Client.Roles(ctx, tenantID) ([]string, bool)`), mirroring
|
||||
`internal/adapters/{lldap,privacyidea,authelia}`'s shape exactly. Fails
|
||||
open by construction — unreachable, non-200, or malformed body all return
|
||||
`(nil, false)`, never an error the caller has to specially handle; a `nil
|
||||
*Client` also fails open safely rather than panicking, so `TokenHandler`
|
||||
doesn't need a separate "is this configured" branch. Wired into
|
||||
`TokenHandler.TenantEngine` (nil by default — existing tests and call
|
||||
sites are unaffected); `token.go` stamps `tenant_roles` only when `ok`.
|
||||
|
||||
7 adapter-level tests (success, non-200, malformed body, connection
|
||||
failure, timeout, nil client, empty base URL). 3 new `token_test.go` cases
|
||||
prove the actual required behavior end-to-end through `TokenHandler`, not
|
||||
just at the adapter layer: present and correct when reachable; **token
|
||||
issuance still returns 200 with every other required core claim intact**
|
||||
when unreachable, `tenant_roles` simply absent (the literal done-criteria);
|
||||
absent when `TenantEngine` isn't configured at all. `go build`/`vet`
|
||||
clean, `go test ./...` green repo-wide.
|
||||
|
||||
**A real bug found and fixed along the way, not worked around:** the first
|
||||
live cross-process check (real `flex-auth`, real `tenant-engine`, this
|
||||
adapter) returned `tenant_not_found` for a tenant that genuinely existed.
|
||||
`tenant-engine`'s `GET /tenants/{tenant_id}/roles` was keyed by its
|
||||
internal `tenant_id`, but `key-cape` (like any external caller) only ever
|
||||
has the tenant's profile *identifier* (the `tenant` claim value) — it has
|
||||
no way to know tenant-engine's internal id. Fixed at the source:
|
||||
`tenant-engine/workplans/ADHOC-2026-07-24.md` (identifier-or-id resolution
|
||||
added to `InMemoryTenantStore`), re-verified with the exact same live
|
||||
three-process chain afterward — `roles=[IAM] ok=true`, resolved by
|
||||
identifier. This is exactly why T02 insisted on a live-server-reachable
|
||||
test rather than only an `httptest` fake: the fake would have used
|
||||
whatever id shape the test author happened to pick and never caught this.
|
||||
|
||||
## Task: Closure review
|
||||
|
||||
```task
|
||||
id: KEY-WP-0005-T03
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "c88a3457-2964-5c44-8220-76f82df3a753"
|
||||
```
|
||||
|
||||
Confirm T01–T02 done; conformance suite green for both tasks together.
|
||||
Note in closure: this unblocks `KEY-WP-0004`'s Binky onboarding actually
|
||||
producing conformant tokens, and unblocks `flex-auth`'s side of the
|
||||
`tenant_roles` picture receiving a real claim to reason about (though
|
||||
`flex-auth`'s live-lookup path doesn't depend on this claim existing — only
|
||||
the cache-read/performance path does). Run `statehub fix-consistency`.
|
||||
|
||||
**Closed 2026-07-24.** T01–T02 done. `go build ./...`, `go vet ./...` clean;
|
||||
`go test ./...` green across the whole repo. Python
|
||||
`net-kingdom/tools/iam-profile-conformance` not run against a live instance
|
||||
(needs the full Authelia+LLDAP+privacyIDEA stack, impractical in this
|
||||
pass) — real coverage instead came from `tests/profile`'s own full HTTP
|
||||
integration test (`TestCompleteTokenFlow`) with genuine claim-value
|
||||
assertions, and from three separate live cross-process checks against a
|
||||
real `tenant-engine` (one of which found and fixed a real bug in
|
||||
`tenant-engine` itself, `ADHOC-2026-07-24`).
|
||||
|
||||
**What this closes:** `key-cape`'s human Authorization Code + PKCE flow now
|
||||
emits every IAM Profile v0.3 core claim (`tenant`, `principal_type`,
|
||||
`groups`, `roles`, `assurance`) plus the optional `tenant_roles` cache,
|
||||
correctly sourced from a real `assurance` signal (MFA actually verified
|
||||
this session, not static enrollment) and a real `tenant-engine` call
|
||||
(fail-open, never blocking login). `KEY-WP-0004`'s Binky onboarding can now
|
||||
produce conformant tokens for human logins.
|
||||
|
||||
**What stays explicitly open, not silently dropped:**
|
||||
- `client_credentials` / service-token issuance — no such flow exists in
|
||||
`token.go` at all yet (confirmed in T01); needed before any *service*
|
||||
caller (not a human) can get a token with `principal_type: "service"` and
|
||||
its own `tenant`/`tenant_roles`. A materially larger, separate piece of
|
||||
work than either task in this workplan.
|
||||
- `flex-auth`'s side of `tenant_roles` (`FLEX-WP-0008`, already closed
|
||||
separately) doesn't depend on this claim existing at all — its
|
||||
live-lookup path calls `tenant-engine` directly, so this workplan doesn't
|
||||
block it, but also doesn't complete the full `tenant_roles` picture on
|
||||
its own until service tokens exist too.
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
id: KEY-WP-0006
|
||||
type: workplan
|
||||
title: "Client credentials and service-token issuance"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-07-27"
|
||||
updated: "2026-07-27"
|
||||
state_hub_workstream_id: "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d"
|
||||
---
|
||||
|
||||
# KEY-WP-0006 - Client credentials and service tokens
|
||||
|
||||
Implement the missing service-principal half of the IAM Profile so
|
||||
`KEY-WP-0004` can issue a least-privilege identity to `rapp-qonto`.
|
||||
|
||||
## T01 - Model confidential service clients
|
||||
|
||||
```task
|
||||
id: KEY-WP-0006-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "993913a7-f951-50ee-9b10-d5b9e67b523c"
|
||||
```
|
||||
|
||||
Extend static client registration with an explicit service subject, tenant,
|
||||
roles, and an environment-backed secret reference. Refuse startup when a
|
||||
`client_credentials` client is public, lacks its identity fields, or cannot
|
||||
resolve its secret. Never serialize or log the resolved secret.
|
||||
|
||||
2026-07-27: Added explicit service subject, tenant, roles, and in-memory-only
|
||||
resolved secret fields. Configuration validation rejects public or incomplete
|
||||
service clients, and startup resolves only `env:` secret references while
|
||||
reporting the variable name—not its value—on failure.
|
||||
|
||||
## T02 - Implement secure client_credentials exchange
|
||||
|
||||
```task
|
||||
id: KEY-WP-0006-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "801a6d55-2f91-51b1-88e4-cc7f0867238d"
|
||||
```
|
||||
|
||||
Accept confidential client authentication through HTTP Basic, compare secrets
|
||||
in constant time, restrict requested scopes to the registered allowlist, and
|
||||
issue a short-lived access token with `principal_type=service`,
|
||||
tenant-specific subject/roles, empty groups, `aal1` client-secret assurance,
|
||||
and optional cached `tenant_roles`. Do not issue an ID token for this
|
||||
non-human grant.
|
||||
|
||||
2026-07-27: Implemented HTTP Basic confidential-client authentication with
|
||||
fixed-length SHA-256 constant-time comparison, allowlisted scopes, short-lived
|
||||
service claims, optional tenant-role cache, token-issued telemetry, and no ID
|
||||
token.
|
||||
|
||||
## T03 - Prove positive and negative conformance
|
||||
|
||||
```task
|
||||
id: KEY-WP-0006-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e5c2a8b1-0879-57fa-a197-250a34c75bdb"
|
||||
```
|
||||
|
||||
Cover valid exchange, unknown client, public client, missing/wrong secret,
|
||||
unsupported grant, excess scope, tenant isolation, telemetry redaction, and
|
||||
discovery metadata. Run the full Go build, vet, and test suite.
|
||||
|
||||
2026-07-27: Added positive service-claim and negative wrong-secret/excess-scope
|
||||
tests, updated discovery to advertise only implemented grants/auth methods,
|
||||
and verified `go test ./...`, `go vet ./...`, and `go build ./...` across the
|
||||
module.
|
||||
|
||||
## T04 - Provision and verify rapp-qonto
|
||||
|
||||
```task
|
||||
id: KEY-WP-0006-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "31af6b8c-96ff-5060-87db-3803b9a9dd12"
|
||||
```
|
||||
|
||||
Generate the client secret without disclosure, store it through the approved
|
||||
OpenBao lane, register the static `rapp-qonto` client, deploy KeyCape, and
|
||||
verify a real exchange yields only the documented Binky service claims.
|
||||
Publish non-secret evidence to `KEY-WP-0004-T03` through T05.
|
||||
|
||||
2026-07-27: Provisioned the confidential client secret at
|
||||
`platform/workloads/rapp-qonto/keycape-client`, injected it into
|
||||
`sso/keycape-rapp-qonto-client`, registered `rapp-qonto-client`, and deployed
|
||||
image `main-e877d27-2` on railiance01. A real exchange produced only
|
||||
`sub=rapp-qonto`, `tenant=tenant:friendly:binky`, `principal_type=service`,
|
||||
`roles=[qonto-reader]`, empty groups, and `scope=qonto:read`; excessive scope
|
||||
and a wrong secret were denied. Public DNS still resolves
|
||||
`kc.coulomb.social` to the older CoulombCore endpoint, so the deployment proof
|
||||
used TLS-preserving direct resolution to railiance01 (`92.205.62.239`).
|
||||
|
||||
## T05 - Closure review
|
||||
|
||||
```task
|
||||
id: KEY-WP-0006-T05
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "8c72d7d9-4086-5e64-b939-a12bff076124"
|
||||
```
|
||||
|
||||
Close after T01-T04 pass and the repeatable verification path is documented.
|
||||
|
||||
2026-07-27: Closure review passed. Build, vet, unit/profile tests, live
|
||||
positive claims, negative authentication/scope checks, and discovery metadata
|
||||
all passed. The stale public DNS target is handed off as routing work rather
|
||||
than weakening the service-token contract.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
id: KEY-WP-0007
|
||||
type: workplan
|
||||
title: "User-engine portal OIDC relying-party integration"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-07-27"
|
||||
updated: "2026-07-29"
|
||||
depends_on:
|
||||
- KEY-WP-0005
|
||||
- USER-WP-0020
|
||||
state_hub_workstream_id: "24db4178-705c-5b09-b7db-ee7a345d71a0"
|
||||
---
|
||||
|
||||
# KEY-WP-0007 - User-engine portal OIDC relying-party integration
|
||||
|
||||
Register and prove the reusable user-engine portal as a strict KeyCape public
|
||||
client. This is the authentication edge required by `NK-WP-0023-T03`; it does
|
||||
not move user-domain or authorization ownership into KeyCape.
|
||||
|
||||
## Register the static portal client
|
||||
|
||||
```task
|
||||
id: KEY-WP-0007-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7d722cae-bf6d-52e0-80f4-cc51e9fb42f0"
|
||||
```
|
||||
|
||||
Register `user-engine-portal` for authorization code with mandatory S256 PKCE,
|
||||
exact callback URIs and only `openid profile email groups`. No wildcard,
|
||||
implicit flow, client secret, or dynamic registration is allowed.
|
||||
|
||||
## Deploy and verify the live client
|
||||
|
||||
```task
|
||||
id: KEY-WP-0007-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "0b222062-4352-5ae6-9e85-95787af08409"
|
||||
```
|
||||
|
||||
Update the railiance01 KeyCape configuration without exposing its signing key
|
||||
or backend credentials. Verify discovery, authorize redirect validation,
|
||||
unregistered callback denial and successful token exchange through the portal.
|
||||
|
||||
## Prove claims and MFA isolation
|
||||
|
||||
```task
|
||||
id: KEY-WP-0007-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "859e7322-71dc-58cc-9f44-6f869c2d0b59"
|
||||
```
|
||||
|
||||
Prove issuer, audience, tenant, groups, roles and assurance claims are verified
|
||||
by the portal and that tenant administration does not imply platform-root.
|
||||
Complete the Binky user/MFA acceptance through the reusable browser path.
|
||||
|
||||
2026-07-27: The live client accepts only the exact portal callback and rejects
|
||||
an unregistered callback with `invalid_profile_usage`. The portal begins an
|
||||
S256 PKCE flow and hands authentication to Authelia. LLDAP tenant envelope
|
||||
mapping now recognizes unambiguous `tenant:<kind>:<slug>:users|admins` groups;
|
||||
ambiguous multi-tenant directory envelopes fail closed to no explicit tenant.
|
||||
|
||||
2026-07-28: suspension now fails closed at both token exchange and
|
||||
`/userinfo`; the deployed `key-cape:909bb32` image refuses subjects carrying
|
||||
the `netkingdom-suspended` directory group. The portal admin surface uses this
|
||||
through the provider-neutral lifecycle API. The human Binky MFA/claims
|
||||
acceptance remains the final part of this task.
|
||||
|
||||
2026-07-29 complete: the reusable browser path issued the Binky password
|
||||
handoff, completed privacyIDEA TOTP enrollment, and returned through the
|
||||
portal authorization-code flow. `/api/v1/me` verified the canonical issuer,
|
||||
`user-engine-portal` audience, `tenant:friendly:binky`, Binky-only groups,
|
||||
roles `user` + `tenant-admin`, and AAL2 assurance with `pwd` + `otp` and
|
||||
`mfa: true`. The same session was denied administration of
|
||||
`tenant:platform` and `tenant:coulomb`. All workplan tasks are complete.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
id: KEY-WP-0008
|
||||
type: workplan
|
||||
title: "Registration handoff and client-aware MFA policy"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: grok
|
||||
topic_slug: netkingdom
|
||||
created: "2026-08-09"
|
||||
updated: "2026-08-16"
|
||||
depends_on:
|
||||
- NK-WP-0025
|
||||
state_hub_workstream_id: "23eb67d4-cf10-57a1-92f6-90146a294bda"
|
||||
---
|
||||
|
||||
# KEY-WP-0008 - registration handoff and client-aware MFA
|
||||
|
||||
Let a registered OIDC client offer NetKingdom signup and request step-up
|
||||
without making KeyCape an account store or weakening high-assurance clients.
|
||||
|
||||
## T01 - Add a safe registration handoff
|
||||
|
||||
```task
|
||||
id: KEY-WP-0008-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e5fb4d73-1e19-565c-80d5-e0eee274ce63"
|
||||
```
|
||||
|
||||
Define an allow-listed registration URL for eligible clients and preserve
|
||||
client ID, redirect URI, PKCE intent, tenant hint, and return context in a
|
||||
signed, expiring state envelope. Registration completion must restart the
|
||||
normal authorization flow and must not mint a token directly.
|
||||
|
||||
Done when unknown users can choose signup from an eligible authorization flow
|
||||
without open redirect, client substitution, or state replay.
|
||||
|
||||
Implemented `prompt=create` and `/authorize/register` against the client's
|
||||
static `registrationUrl`, plus HMAC-signed `kc_handoff` envelopes consumed
|
||||
once at `/authorize/return`. Return restarts `/authorize` and never mints a
|
||||
code. Ineligible clients get no signup link. Live registration entry remains
|
||||
user-engine-owned per NK-WP-0025; KeyCape only issues the return envelope.
|
||||
|
||||
## T02 - Replace global MFA with client-aware minimum assurance
|
||||
|
||||
```task
|
||||
id: KEY-WP-0008-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1ba50454-b6f5-578e-a526-da74b8082a89"
|
||||
```
|
||||
|
||||
Replace the single require-for-all switch with policy that combines client
|
||||
minimum assurance, requested ACR/step-up, tenant policy, protected action, and
|
||||
current session assurance. Preserve mandatory MFA for platform/admin clients.
|
||||
Allow coulomb-social ordinary login at password assurance when no stronger
|
||||
rule applies.
|
||||
|
||||
Done when one low-assurance client cannot suppress MFA for another client or
|
||||
reuse an under-assured session for a high-assurance request.
|
||||
|
||||
Implemented with nullable per-client `mfaRequired`: an explicit client value
|
||||
overrides the provider default only for that client. Absent values preserve
|
||||
the existing provider-driven policy.
|
||||
|
||||
2026-08-16: `DecideAssurance` now combines client minimum, requested ACR,
|
||||
provider default, `max_age`, `prompt=login`, and current KeyCape login-session
|
||||
level. An AAL1 session cannot satisfy an AAL2 client or `acr_values=aal2`.
|
||||
`coulomb-social` in `config/dev-config.yaml` is `mfaRequired: false`; other
|
||||
clients keep the provider default. Users without an enrolled factor are sent
|
||||
to the client's `enrollmentUrl` instead of completing authorization.
|
||||
|
||||
## T03 - Support explicit step-up and fresh authentication
|
||||
|
||||
```task
|
||||
id: KEY-WP-0008-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f7acbc12-e597-5d1e-8b29-bfa02237730f"
|
||||
```
|
||||
|
||||
Implement supported ACR/max-age or equivalent IAM Profile parameters, invoke
|
||||
privacyIDEA only when policy requires it, and return verifiable assurance
|
||||
claims. Handle users without an enrolled factor through a safe enrollment
|
||||
handoff rather than an authorization bypass.
|
||||
|
||||
Done when coulomb.social can request MFA for a profile/action and verify the
|
||||
result from token claims.
|
||||
|
||||
Implemented `acr_values` preservation and AAL2/MFA forcing through the
|
||||
privacyIDEA challenge. The existing token `assurance` claim reports `aal2`
|
||||
and `mfa: true` only after successful verification.
|
||||
|
||||
## T04 - Prove policy isolation and compatibility
|
||||
|
||||
```task
|
||||
id: KEY-WP-0008-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "bc660767-387e-59a5-bfce-caccf4082c08"
|
||||
```
|
||||
|
||||
Test known and unknown users, registration link eligibility, state expiry and
|
||||
replay, password-only coulomb-social login, profile/action step-up, no-factor
|
||||
enrollment, OpenBao mandatory MFA, cross-client session reuse, and logout.
|
||||
Keep static client registration and exact redirect rules unchanged.
|
||||
|
||||
Done when existing high-assurance clients pass unchanged and the new
|
||||
coulomb-social journey passes live.
|
||||
|
||||
2026-08-16: isolation tests cover known/unknown users, registration
|
||||
eligibility, state expiry/replay, password-only coulomb-social, ACR step-up,
|
||||
no-factor enrollment handoff, OpenBao mandatory MFA, cross-client AAL1
|
||||
session reuse, logout, and exact redirect enforcement. Full Go suite passes.
|
||||
Live coulomb-social AAL1/AAL2 isolation was already proven on railiance01
|
||||
under NK-WP-0025-T05 (2026-08-14); this closeout adds the KeyCape-side
|
||||
regression suite and `/logout`.
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
id: KEY-WP-0009
|
||||
type: workplan
|
||||
title: "Provider capability declarations and bounded service identities"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-08-23"
|
||||
updated: "2026-09-27"
|
||||
depends_on:
|
||||
- NK-WP-0030
|
||||
- KEY-WP-0006
|
||||
state_hub_workstream_id: "c1a9b1cc-2ff0-566a-b544-1a2ef967fc0d"
|
||||
---
|
||||
|
||||
# KEY-WP-0009 — Provider capabilities and bounded service identities
|
||||
|
||||
Publish KeyCape-owned security-scenario interfaces and make the existing
|
||||
service-token issuer precise enough for OpenBao machine-login consumers. This
|
||||
work accepts identity issuance ownership without taking over OpenBao roles,
|
||||
policies, secret custody, or privacyIDEA token lifecycle.
|
||||
|
||||
## Publish C1 and C2b provider declarations
|
||||
|
||||
```task
|
||||
id: KEY-WP-0009-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "eaf7af48-b3d6-5f85-b0ce-ff2b640f9cc3"
|
||||
```
|
||||
|
||||
Publish Playbook Capability Contract v0.1 declarations for the KeyCape C1
|
||||
runtime and its privacyIDEA-backed C2b integration. Name exact entry points,
|
||||
parameter authority, resource ownership, trust requirements, and readiness
|
||||
evidence. Do not claim C2a or privacyIDEA-owned token lifecycle.
|
||||
|
||||
Published `capabilities/playbooks/key-cape.lightweight-sso.yaml` and
|
||||
`capabilities/playbooks/key-cape.privacyidea-token-authority.yaml`. The latter
|
||||
claims the KeyCape integration for C2b while leaving factor enrollment, token
|
||||
state, custody keys, validation decisions, and lifecycle with privacyIDEA.
|
||||
|
||||
## Define bounded service-auth contracts
|
||||
|
||||
```task
|
||||
id: KEY-WP-0009-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "212eb75c-a36c-5cb8-bf67-7d524f00f9ef"
|
||||
```
|
||||
|
||||
Define the KeyCape service-auth claims, renewal/expiry, failure, custody, and
|
||||
owner boundaries required by secrets-engine and OpenBao JWT roles. Add
|
||||
per-client access-token lifetimes so a bounded client contract does not depend
|
||||
on an unrelated global default.
|
||||
|
||||
Added a validated 1m-1h per-client `tokenLifetime` override, applied it to JWT
|
||||
`exp` and `expires_in`, and documented claims, renewal, expiry, residual JWT
|
||||
validity, explicit OpenBao cleanup, and no-fallback failure semantics in
|
||||
`docs/openbao-service-auth-contract.md`.
|
||||
|
||||
## Accept coding-agent issuance ownership
|
||||
|
||||
```task
|
||||
id: KEY-WP-0009-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c41e0144-8169-57cf-8c34-eb52e65d6d76"
|
||||
```
|
||||
|
||||
Publish the non-secret static registration for `codex-railiance-platform` with
|
||||
the exact audience, subject, tenant, role, scope, and 15-minute lifetime already
|
||||
accepted by railiance-platform. KeyCape owns JWT issuance and client disablement;
|
||||
railiance-platform owns the exact-bound OpenBao role and policy; OpenBao owns
|
||||
resulting token enforcement; secret values remain outside this repository.
|
||||
|
||||
Accepted ownership through the exact non-secret registration in
|
||||
`config/service-clients.example.yaml`. Added the parallel reviewed
|
||||
`secrets-engine-openbao` contract. Live value generation/materialization,
|
||||
deployment merge, and OpenBao role/policy application remain with their named
|
||||
custody and platform owners and are not implied by this source registration.
|
||||
|
||||
## Validate and hand off
|
||||
|
||||
```task
|
||||
id: KEY-WP-0009-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6945c0c0-bbb6-552b-8d62-d806d3fd0018"
|
||||
```
|
||||
|
||||
Run the canonical declaration validator, Go formatting/build/vet/tests, and
|
||||
repository checks. Reply to NetKingdom, secrets-engine, ops-warden, and
|
||||
railiance-platform with revision-independent paths and exact ownership status.
|
||||
|
||||
Both declarations pass NetKingdom's canonical validator. The full Go suite,
|
||||
vet, build, `gofmt`, YAML parsing, and `git diff --check` pass using an explicit
|
||||
Go 1.23 toolchain and writable local cache; the workstation's default Go cache
|
||||
is read-only and was not used.
|
||||
|
||||
2026-09-05 correction: the earlier statement that State Hub handoffs were
|
||||
delivered was not supported by receipt IDs. The audit found no matching handoff
|
||||
receipts in the Hub response. KEY-WP-0014-T02 records the correction.
|
||||
|
||||
2026-09-08 closure. Verification re-run on `main` with `GOTOOLCHAIN=go1.23.0` and
|
||||
a writable `GOCACHE`: `make contract-test` PASS, `go build ./...`, `go vet ./...`
|
||||
and `go test ./...` all pass, `git diff --check` clean. `gofmt` had drifted on
|
||||
`src/internal/server/telemetry/events.go` and `src/tests/migration/scenario_c_test.go`
|
||||
(comment alignment only, no semantic change); both were reformatted so the
|
||||
formatting claim now holds.
|
||||
|
||||
The four handoffs were then actually sent, and each receipt is readable back via
|
||||
`GET /messages/?from_agent=key-cape`:
|
||||
|
||||
| Recipient | Receipt id |
|
||||
| --- | --- |
|
||||
| netkingdom | `692ed0d7-e21b-48e7-8600-5b32b551c627` |
|
||||
| secrets-engine | `479a1698-87dc-4ee2-a732-c2dc18bc4e84` |
|
||||
| ops-warden | `5bc58847-eed5-4164-bb0f-a0d8455e75b8` |
|
||||
| railiance-platform | `9c6a4e70-6bc0-4ad6-8293-35a56e507dfc` |
|
||||
|
||||
Each names revision-independent paths, the 2026-09-08 verification result, and
|
||||
the same ownership split: KeyCape owns issuance, static registrations, client
|
||||
disablement and the claim set; secret custody, OpenBao roles/policies and token
|
||||
enforcement, privacyIDEA factor and token lifecycle, resource authorization and
|
||||
deployment remain with their named owners. C2a is not claimed. This closes the
|
||||
notification gap; it is still not evidence of live deployment or provider cutover.
|
||||
|
||||
### 2026-09-27 operator tenant-zero correction
|
||||
|
||||
OpenBao infrastructure identities belong to `tenant:platform`, not Coulomb.
|
||||
Corrected both static infrastructure registrations, matching source tests and
|
||||
the tenant/OpenBao contracts. Consumer preflight and platform role sources are
|
||||
updated together; no live registration or token migration is claimed. The
|
||||
provider contract remains complete; RPF-WP-0035-T02 and SECRETS-WP-0008-T06
|
||||
retain coordinated custody/deployment and native rejection/revocation proof.
|
||||
Human fallback and workload tenant grants are not elevated to platform.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
id: KEY-WP-0010
|
||||
type: workplan
|
||||
title: "Admit the operator-tunneled OpenBao callback"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-08-23"
|
||||
updated: "2026-08-23"
|
||||
depends_on:
|
||||
- RAILIANCE-WP-0027
|
||||
state_hub_workstream_id: "8bdf9a24-9dc1-53f9-9448-3b29c36d9950"
|
||||
---
|
||||
|
||||
# KEY-WP-0010 — OpenBao operator loopback callback
|
||||
|
||||
## Admit and validate the exact callback
|
||||
|
||||
```task
|
||||
id: KEY-WP-0010-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "46d78907-bda4-5cc9-853d-98c0026d9fe1"
|
||||
```
|
||||
|
||||
Added the exact operator-only callback
|
||||
`http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback` to the
|
||||
source-owned `openbao-admin` static client in `config/dev-config.yaml`. The
|
||||
registration retains exact-match redirects and does not authorize a login,
|
||||
change the OpenBao role, broaden a listener, or handle a credential. A config
|
||||
test pins the exact URI and proves the complete source config validates.
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
id: KEY-WP-0011
|
||||
type: workplan
|
||||
title: "Recover from live KeyCape Secret output exposure"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-08-23"
|
||||
updated: "2026-08-23"
|
||||
state_hub_workstream_id: "158aa957-8818-52ea-b7a5-3962cf30ba5e"
|
||||
---
|
||||
|
||||
# KEY-WP-0011 — Live KeyCape Secret exposure recovery
|
||||
|
||||
Recover safely after the base64 data map of Kubernetes Secret
|
||||
`sso/keycape-config` was mistakenly rendered in agent tool output during a
|
||||
metadata check. Never reproduce, decode, or store the exposed payload.
|
||||
|
||||
## Contain and notify owners
|
||||
|
||||
```task
|
||||
id: KEY-WP-0011-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a8f5e33d-c725-5181-849e-a42bba34d3d8"
|
||||
```
|
||||
|
||||
Stopped live rollout work, treated the payload as exposed, and notified
|
||||
railiance-platform and NetKingdom without values. The affected classes are the
|
||||
KeyCape RS256 signing private key and credentials embedded in `config.yaml`
|
||||
(LLDAP bind credential, Authelia client credential, and privacyIDEA admin
|
||||
token). No value was decoded, reused, or intentionally repeated and no live
|
||||
mutation was attempted.
|
||||
|
||||
## Approve and execute coordinated rotation
|
||||
|
||||
```task
|
||||
id: KEY-WP-0011-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9d60569c-a0f1-53a9-b4dc-01687ab92bcc"
|
||||
```
|
||||
|
||||
Approved by the operator on 2026-08-23, accepting deliberate invalidation of
|
||||
active sessions and a brief SSO interruption. The RS256 key, LLDAP bind/admin
|
||||
credential, Authelia-KeyCape client credential, and privacyIDEA application
|
||||
signing secret were replaced without rendering values. The exposed
|
||||
privacyIDEA admin JWT was removed from live use. Replacement material is
|
||||
recoverable only through the NetKingdom age recipient in the encrypted
|
||||
recovery overlay under `history/`.
|
||||
|
||||
## Verify recovery without values
|
||||
|
||||
```task
|
||||
id: KEY-WP-0011-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a079573d-97fd-5c6c-b1f9-c0af65e2933a"
|
||||
```
|
||||
|
||||
Verified changed resource versions and public JWKS fingerprints, four Ready
|
||||
deployments, successful replacement LLDAP and Authelia authentication,
|
||||
wrong-secret rejection, explicit privacyIDEA fail-closed denial, issuer
|
||||
discovery, and the OpenBao callback handoff to Authelia. The temporary LLDAP
|
||||
force-reset flag was removed. No Kubernetes Secret payload or authentication
|
||||
value was rendered during recovery.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: KEY-WP-0012
|
||||
type: workplan
|
||||
title: "Repair UserInfo canonical subject resolution"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: userinfo-canonical-subject-resolution
|
||||
created: "2026-08-31"
|
||||
updated: "2026-09-01"
|
||||
state_hub_workstream_id: "2aca83d8-4e80-52c6-8929-f1e957576b47"
|
||||
---
|
||||
|
||||
## Repair subject lookup
|
||||
|
||||
```task
|
||||
id: KEY-WP-0012-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "bc1ee2c7-a321-5914-96d1-9cb871f0ee1b"
|
||||
```
|
||||
|
||||
Resolve the canonical LDAP-DN `sub` emitted by the token endpoint without
|
||||
passing it to the username-only repository lookup. Preserve stable subject
|
||||
semantics and verify any `preferred_username` lookup against the canonical ID.
|
||||
|
||||
## Regression verification
|
||||
|
||||
```task
|
||||
id: KEY-WP-0012-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7073cc30-a518-5c27-acaf-1a323fd981c6"
|
||||
```
|
||||
|
||||
Cover canonical-ID, legacy username-sub, missing subject, and suspended-user
|
||||
behavior. Run the KeyCape test suite and image build checks.
|
||||
|
||||
## Deploy and verify OpenBao OIDC
|
||||
|
||||
```task
|
||||
id: KEY-WP-0012-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "571c5237-93fa-5c61-87af-974e02fd8929"
|
||||
```
|
||||
|
||||
Publish and deploy the corrected KeyCape image, prove `/userinfo` accepts a
|
||||
fresh human access token, then resume the governed Policy Nexus bootstrap.
|
||||
|
||||
Completed 2026-09-01. Forgejo Actions run 14 built commit
|
||||
`153258b9d39273d53ca7ab5d1bdc09961ea9026e`; image digest
|
||||
`sha256:4c7193efc3e50bd72a01812a583e823cc04f41ca96ef387b31ba9ae2533b80d0`
|
||||
rolled out successfully. A fresh OpenBao OIDC flow passed `/userinfo`, and the
|
||||
governed Policy Nexus bootstrap subsequently completed with deterministic
|
||||
session revocation.
|
||||
|
|
@ -0,0 +1,573 @@
|
|||
---
|
||||
id: KEY-WP-0013
|
||||
type: workplan
|
||||
title: "Approval-engine resource audience and client registrations"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
flavor: implementation
|
||||
owner: codex
|
||||
topic_slug: approval-engine-resource-audience
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-30"
|
||||
state_hub_workstream_id: "6e815d88-b0e3-5ce0-be5d-13ab15917f7f"
|
||||
---
|
||||
|
||||
Source: approval-engine inbox request 5583e896-52f2-45bd-895f-02f227b7e836,
|
||||
reviewed against its local registration and caller-authentication contracts.
|
||||
|
||||
## Implement static resource audiences
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a92432a2-4e92-5b90-be2b-d82b784bf8f0"
|
||||
```
|
||||
|
||||
Added optional static audience configuration for access tokens in both grants;
|
||||
ID tokens retain the relying-party audience. Added human access-token scope.
|
||||
Published bounded approval service fragments and the human registration contract.
|
||||
Regression tests cover the default audience, request override resistance, JWKS
|
||||
signature validation, ID-token separation and service registration scope isolation.
|
||||
Browser requests and token exchanges now enforce the client scope allow-list,
|
||||
including grants removed after authorization.
|
||||
|
||||
## Provision and prove live registrations
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "607897c5-bad9-55e5-86df-7802f592d6e8"
|
||||
```
|
||||
|
||||
Needs deployment-owned custody for both new service secret references and the
|
||||
compatible image/configuration rollout. The upstream issuer is now verified and
|
||||
pinned by NetKingdom (2026-09-09). Deploy the implementation and service registrations together,
|
||||
then prove live JWKS verification and denied excess scopes without logging values.
|
||||
Local signature proof is not live rollout evidence. See docs/approval-engine-auth-contract.md.
|
||||
The separate human UI callback gate is retained in T05; a bearer-only resource
|
||||
server does not own a redirect and its absence does not block these service clients.
|
||||
|
||||
|
||||
2026-09-05 follow-up: read-only deployment metadata shows the current image is
|
||||
forgejo.coulomb.social/coulomb/key-cape:main-153258b and only the Qonto service
|
||||
secret environment reference is present. The two approval clients are not
|
||||
materialized through deployment environment references. Published a concrete
|
||||
non-secret admission packet at docs/approval-engine-provisioning-request.yaml.
|
||||
Custody routing has no exact admitted lane for these two clients. `warden plan`
|
||||
returned founder_required but matched an unrelated generic database lane; that
|
||||
mismatch is not authority to provision. Human callback clarification is pending.
|
||||
No secrets were read or production resources changed.
|
||||
|
||||
2026-09-08: the two blocking requests were sent and their receipts are readable
|
||||
back via `GET /messages/?from_agent=key-cape`.
|
||||
|
||||
- `278a3ebe-b529-49f6-bd1a-e3ebcf318260` -> railiance-platform. Custody admission
|
||||
for both clients, quoting `docs/approval-engine-provisioning-request.yaml`
|
||||
verbatim: exact OpenBao paths and `client_secret` field, the two Kubernetes
|
||||
delivery references, the two `KEYCAPE_*_CLIENT_SECRET` environment names,
|
||||
`tenant:platform` per decision `5ed3fb35-eca9-413a-82b9-95171ba85bf6`, and the
|
||||
15m lifetime. It asks for four things explicitly: confirm/correct the paths and
|
||||
fields, confirm the delivery references, name the authority for the attended
|
||||
first provision, and agree a rollout window in which registrations and the
|
||||
KeyCape version that reads them deploy together. It restates that the
|
||||
`warden plan` `founder_required` result matched an unrelated generic database
|
||||
lane and is not being treated as authorization.
|
||||
- `43fbd997-6e22-4859-8433-58b784c2df6c` -> approval-engine. Requests the two
|
||||
missing human-registration inputs — the exact `client_id` and the full callback
|
||||
URI — noting redirects are matched exactly and a near-miss fails closed at
|
||||
`/authorize`. Restates the human client shape (public, S256 PKCE,
|
||||
`audience: approval-engine`, `allowedScopes: [openid, approval:approve]`,
|
||||
`mfaRequired: true`, no consume grant) and asks approval-engine to confirm it
|
||||
validates the access token, not the ID token, against the deployed `/jwks`.
|
||||
|
||||
Deployment state is unchanged from the 2026-09-05 observation: neither approval
|
||||
client is materialized. Task remains `wait` — the two admissions are owned by
|
||||
railiance-platform and approval-engine respectively, and live provisioning proof
|
||||
cannot begin until both land. Still no secret read and no production resource
|
||||
changed.
|
||||
|
||||
2026-09-08, second pass. Custody and the human callback are still owed, but one
|
||||
part of this task was ours all along and was not built: the task requires proving
|
||||
"live JWKS verification and denied excess scopes without logging values", and
|
||||
there was no way to do that except by hand, against production, at the moment
|
||||
custody lands — which is the worst time to be improvising a check.
|
||||
|
||||
Shipped `keycape verify-client` (`src/internal/authclient/verify.go`, documented
|
||||
in `docs/native-authentication.md`). Per registration it verifies discovery
|
||||
origin, the `client_credentials` exchange and its RS256 signature against the
|
||||
deployed JWKS, exact `sub`/`tenant`/`roles`/`principal_type`, that every
|
||||
`-deny-scope` is refused, and that the token carries no scope that was not
|
||||
requested — the last being a gap the caller commands do not cover, since they
|
||||
prove requested scopes were granted and not that nothing extra came back.
|
||||
Failures name the claim, never the observed value, and nothing is written to
|
||||
disk, so it is safe to run against production. The exact invocation for each of
|
||||
the two clients is now recorded in the `verification:` block of
|
||||
`docs/approval-engine-provisioning-request.yaml`, so admission hands back a
|
||||
command rather than a description.
|
||||
|
||||
Tests: `src/internal/authclient/verify_test.go` covers the passing case, an
|
||||
over-broad registration, a live predecessor secret, an identical "rotation" and
|
||||
output non-disclosure. Verified with teeth — neutering `mustFail` makes the suite
|
||||
fail rather than pass silently.
|
||||
|
||||
Task stays `wait`. What is owed from elsewhere is unchanged and unreduced: the
|
||||
two secret values through an admitted custody path, and the exact human
|
||||
`client_id` and callback URI. Nothing here provisions anything.
|
||||
|
||||
### 2026-09-08 attended upstream issuer proof — T02 return
|
||||
|
||||
The user explicitly admitted the prepared temporary probe. At 21:44:44 UTC its
|
||||
actual Authelia authorization-code exchange verified the signed issuer
|
||||
**`https://auth.coulomb.social`**, including signature, keycape audience, validity
|
||||
window and browser-bound nonce. The pinned Job exited 0. No token or user claim
|
||||
was retained, and no downstream credential was issued.
|
||||
|
||||
All five temporary resources and the Pod were removed with scoped cleanup;
|
||||
KeyCape Deployment and config Secret metadata remained unchanged. Receipt:
|
||||
`docs/evidence/2026-09-08-upstream-issuer-live-proof.json`.
|
||||
|
||||
This closes the actual-token issuer observation gate. T02 remains wait for the
|
||||
configuration owner to ensure `authelia.issuer` is pinned to the verified HTTPS
|
||||
value, named CCR-2026-0017/0018 reviews, custody delivery and compatible rollout.
|
||||
The diagnostic did not prove downstream KeyCape MFA/application login and did
|
||||
not activate custody. Do not repeat discovery or request another observation
|
||||
unless the provider/configuration or proof context changes.
|
||||
|
||||
|
||||
2026-09-09 configuration-owner return: NetKingdom's exercised, revision-guarded
|
||||
issuer-only helper inserted `authelia.issuer: https://auth.coulomb.social` into
|
||||
`sso/keycape-config`; resourceVersion `51346058` -> `58713343`. Independent
|
||||
readback matches. Every other configuration byte and Secret data entry, including
|
||||
the private key, was preserved. Thirteen tests passed. Receipt:
|
||||
`docs/evidence/2026-09-09-upstream-issuer-pin.json`.
|
||||
|
||||
The stored issuer configuration prerequisite is closed. No process restart,
|
||||
image rollout, custody activation or downstream login proof occurred. T02 stays
|
||||
wait for the named CCR reviews, attended custody and compatible rollout. The
|
||||
single-instance replacement and existing-human-login checks remain mandatory.
|
||||
|
||||
2026-09-09 completion: both named CCR reviews are recorded. Verifier-side custody,
|
||||
ESO delivery and the compatible pinned image/config are now live. Both service
|
||||
clients passed the pinned verifier plus independent live signature and exact
|
||||
claim/lifetime checks; wrong-secret and excess-scope requests were rejected.
|
||||
The existing OpenBao human login passed before and after replacement; each
|
||||
attended session self-revoked. Receipt: `docs/evidence/2026-09-09-keycape-verifier-admission.json`.
|
||||
|
||||
The deployment owner exercised compatible rollback and version-preserving resume.
|
||||
The final helper checks containerd imageID against the pinned manifest and uses
|
||||
the existing 30-second future-iat bound without extending token expiry. The
|
||||
pinned native verifier runs in the contained owner process because workload
|
||||
network policy excludes public issuer HTTPS. No network grant was broadened.
|
||||
|
||||
Live residual ownership remains KEY-WP-0013-T05 for the approval UI callback/MFA;
|
||||
RPF-WP-0035-T05 for separate client-side delivery; AUDIT-WP-0009-T09 for audit
|
||||
custody; APPROVAL-WP-0002 and SECRETS-WP-0009-T03 for end-to-end claim/consume,
|
||||
expiry/revocation and native delivery. Natural JWT expiry and real predecessor
|
||||
rotation were not exercised by initial provisioning.
|
||||
|
||||
## Reconcile tenant vocabularies across approval layers
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e98da2eb-1f05-5c7f-bc84-711fa38b8c4a"
|
||||
```
|
||||
|
||||
Source: glas-harness inbox message 356f6977-d361-4e3b-83ab-b2c7f4759286
|
||||
(GLAS-WP-0015), which asks for the exact store tenant, JWT tenant, CheckRequest
|
||||
tenant, any permitted mapping and wrong-tenant denial evidence.
|
||||
|
||||
Published `docs/tenant-claim-contract.md`. The KeyCape-owned JWT tenant for all
|
||||
four reviewed service registrations is `tenant:coulomb`, bound at registration
|
||||
and required by config validation. Approval store `platform` and policy
|
||||
`tenant:platform` are owned by approval-engine and flex-auth; KeyCape performs no
|
||||
normalization or aliasing, so exact comparison does not match today. No mapping
|
||||
was invented and no live registration or policy subject was changed — the two
|
||||
admissible resolutions are recorded for the owning parties to decide.
|
||||
|
||||
Added `src/internal/server/oidc/tenant_test.go`: request-supplied `tenant` and
|
||||
`tenant_hint` cannot alter the claim; two registrations never carry each other's
|
||||
tenant (the wrong-tenant denial basis); human tokens default to `tenant:coulomb`
|
||||
rather than an empty claim. Local issuance proof only, not live-rollout evidence.
|
||||
|
||||
## Align approval registrations to tenant:platform
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d93e21cc-a422-5e78-981f-9c00f3161375"
|
||||
```
|
||||
|
||||
Source: glas-harness inbox message f487c63a-dd7e-4ad8-9188-eec9fbea59c6,
|
||||
operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 ("Use tenant:platform for
|
||||
the Glas approval dependency chain", verified resolved in the hub). This closed
|
||||
resolution 2 of the two options recorded in KEY-WP-0013-T03.
|
||||
|
||||
Changed the `tenant` field on exactly `secrets-engine-approval` and
|
||||
`approval-engine-operator` in `config/service-clients.example.yaml` and the two
|
||||
matching entries in `docs/approval-engine-provisioning-request.yaml` to
|
||||
`tenant:platform`, each annotated with the decision id. `codex-railiance-platform`,
|
||||
`secrets-engine-openbao` and the human directory default keep `tenant:coulomb`.
|
||||
No audience, scope, subject, role, lifetime or MFA change; no cross-tenant grant.
|
||||
|
||||
Evidence: `TestApprovalClientIssuesExactPlatformTenantAndRejectsAliases` (exact
|
||||
`tenant:platform` with `aud=approval-engine` even when the caller requests
|
||||
`platform`, `tenant:coulomb` or `TENANT:PLATFORM`),
|
||||
`TestUnrelatedServiceClientKeepsCoulombTenant`, and
|
||||
`TestServiceRegistrationTenantsAreExactPerDecision`, which pins every reviewed
|
||||
client's tenant against the real fixture so a reintroduced alias fails the build.
|
||||
Full `go test ./...` and `go vet ./...` pass. Choice only — live provisioning and
|
||||
the other admission gates remain with KEY-WP-0013-T02.
|
||||
|
||||
2026-09-09 observation, read-only. Custody has been admitted since the
|
||||
2026-09-08 record and the deployment now carries the pieces T02 was waiting for:
|
||||
`sso/keycape-config` registers `secrets-engine-approval` and
|
||||
`approval-engine-operator` alongside the existing clients; the secrets
|
||||
`keycape-secrets-engine-approval-client` and
|
||||
`keycape-approval-engine-operator-client` exist in the namespace; both
|
||||
`KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET` and
|
||||
`KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET` are wired into the pod; and the
|
||||
image is pinned by digest to
|
||||
`sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`, the
|
||||
candidate T06 published.
|
||||
|
||||
What that does and does not establish. The live issuer passes every conformance
|
||||
check that needs no credential — it advertises itself, offers the profile
|
||||
authorization surface, publishes usable RS256 keys and advertises no excluded
|
||||
grant (`src/tests/conformance`, run against `https://kc.coulomb.social`). It does
|
||||
**not** establish the claim contract: subject, tenant, roles, scopes, lifetime
|
||||
and excess-scope denial per client are exactly what `keycape verify-client`
|
||||
exists to prove, and that needs the client secret in the environment.
|
||||
|
||||
Deliberately not done here. Reading either secret to run the verifier is
|
||||
client-side retrieval, which the provisioning packet records as **not admitted**
|
||||
(`custody_return.scope`: verifier-side copies only). So the remaining proof is an
|
||||
attended operator action, not something to take by reading the secret. T02 stays
|
||||
`wait` on that run, not on custody.
|
||||
|
||||
**Correction, same day.** The refusal above was right; the conclusion drawn from
|
||||
it was not. The attended run had already happened, on 2026-09-09T00:10Z, and its
|
||||
receipt is committed in this repository at
|
||||
`docs/evidence/2026-09-09-keycape-verifier-admission.json`. Per client — both
|
||||
`secrets-engine-approval` and `approval-engine-operator` —
|
||||
`live_jwks_signature_verified: true`, `exact_claims_verified: true`,
|
||||
`excess_scope_denied: true`, `wrong_secret_denied: true`,
|
||||
`lifetime_seconds: 900`, `maximum_future_iat_seconds: 30`,
|
||||
`pinned_artifact_verifier_passed: true`, `verifier_location: "attended owner
|
||||
process"`. `credential_values_emitted: false` and
|
||||
`client_side_read_admitted: false` throughout, so it was run the admitted way.
|
||||
The claim contract is established and T02's `status: done` is correct; it does
|
||||
not revert to `wait`.
|
||||
|
||||
The reasoning error is worth naming because it is the third of its kind here in
|
||||
two days: "not admitted **for me**" was read as "not done **by anyone**", without
|
||||
checking `docs/evidence/`. The preceding paragraph's read-only observations are
|
||||
sound and its restraint about not reading the secret is exactly right — only the
|
||||
final sentence was wrong, and it is retained above rather than deleted so the
|
||||
correction has something to point at.
|
||||
|
||||
What the receipt itself declines to claim, which is the honest residual and
|
||||
matches this task's earlier record: `real_predecessor_rotation_tested: false`,
|
||||
`observed_wall_clock_expiry: false`, and under `limits`,
|
||||
`actual_predecessor_rotation_observed: false` and
|
||||
`wall_clock_jwt_expiry_observed: false`. Natural expiry and a real predecessor
|
||||
rotation were not exercised by initial provisioning and are not claimed by
|
||||
anyone. Predecessor rejection is implemented and unit-tested in
|
||||
`keycape verify-client`; it has not been run against a real rotation, which is
|
||||
KEY-WP-0014-T04's territory and still has no admitted execution authority.
|
||||
|
||||
**Independently confirmed by the custody owner, 2026-09-09** (railiance-platform
|
||||
`ae6ff4bc`, `570e6746`). They reached the same reading from their own side and
|
||||
add one datum this record did not carry: `human_client_consume_denied: true` for
|
||||
both clients, alongside the signature, exact-claim, lifetime, excess-scope and
|
||||
wrong-secret results. They confirm the verifier ran twice — once during
|
||||
activation and again after rollout — at deployment generation 38, single ready
|
||||
replica, signing key unchanged, no credential values emitted, and state that T02
|
||||
can be closed against that receipt rather than against a run KeyCape cannot make.
|
||||
|
||||
They also correct one thing in our framing, and they are right: the packet says
|
||||
*client-side retrieval* is unadmitted, which remains true, but the attended
|
||||
operator path is not a client-side read and never required one. The two are
|
||||
distinct and this task had been treating them as the same constraint.
|
||||
|
||||
**Do not cite this receipt as rotation evidence.** Both owners now state the same
|
||||
two gaps independently: `real_predecessor_rotation_tested: false` and
|
||||
`observed_wall_clock_expiry: false`. Neither is a T02 acceptance criterion, and
|
||||
neither is closed by anything recorded here — see KEY-WP-0014-T04.
|
||||
|
||||
**Client-side retrieval is prepared but still unadmitted.** CCR-2026-0019
|
||||
(secrets-engine reader) and CCR-2026-0020 (approval-engine-operator reader) are
|
||||
`in_flight` under RPF-WP-0035-T06. 0020 has no named presenting actor in any
|
||||
owner source, and railiance-platform left it undetermined rather than guessing —
|
||||
correctly. KeyCape's view, asked for as the issuer of the identity and recorded
|
||||
in `docs/approval-engine-provisioning-request.yaml`, is that the registration as
|
||||
issued carries both `approval:create` and `approval:approve`, so a single
|
||||
presenter can create and then approve the same entry. That is a
|
||||
separation-of-duties question about who may hold the credential, not a defect in
|
||||
the token: KeyCape issues the grants approval-engine requested. The decision is
|
||||
approval-engine's and the doctrine question is gate-house's.
|
||||
|
||||
**Answered 2026-09-09, and the answer withdrew the client.** approval-engine
|
||||
checked and the presenter is nobody (`056aef0d`). Nothing in their repository
|
||||
obtains an OAuth token — the engine only verifies them, there is no
|
||||
client-credentials grant in the package, and the one outbound credential is a
|
||||
mounted audit-token, not an OAuth client. Scope by scope the bundle never
|
||||
described a single actor: `approval:approve` now belongs to the informed-decision
|
||||
human client, `approval:emit` is redundant because the running server emits its
|
||||
own heartbeat with no token, and no requester identity was ever settled for
|
||||
`approval:create`. It was a convenience bundle written when a lifecycle operator
|
||||
was assumed to exist.
|
||||
|
||||
They asked railiance-platform to cancel the client-side reader CCR-2026-0020 as
|
||||
an owner decision. Verifier custody CCR-2026-0018 stands, so the client remains
|
||||
authenticable if a presenter appears, and they asked for it to be kept on that
|
||||
basis rather than removed. Annotated as withdrawn in
|
||||
`config/service-clients.example.yaml` and the provisioning packet; not deleted.
|
||||
The separation-of-duties observation above is retained because it constrains the
|
||||
re-request: when a presenter is named the registration comes back **narrowed**,
|
||||
and `approval:approve` must not travel with the operational scopes.
|
||||
|
||||
**One thing "do not provision" cannot undo, raised rather than filed.** This
|
||||
client is already live — provisioned in the 2026-09-09 attended rollout and
|
||||
verified in `docs/evidence/2026-09-09-keycape-verifier-admission.json`. So the
|
||||
estate now holds a confidential credential that can create *and* approve
|
||||
approvals, that nobody presents, and that no consumer is waiting on. That is
|
||||
standing capability with no counterparty, which is a worse resting state than
|
||||
either provisioning it for a named presenter or disabling it. KeyCape owns client
|
||||
disablement, so the action is ours to take and not ours to decide alone; raised
|
||||
with approval-engine and railiance-platform rather than exercised, since removing
|
||||
a live registration is a deployment change. Recorded here so it is not lost:
|
||||
**an unpresented create+approve credential should not be left standing
|
||||
indefinitely.**
|
||||
|
||||
## Assign and register the human approver browser client
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T05
|
||||
status: done
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
state_hub_task_id: "9a782909-91db-59fa-aae7-83766f4fbb0d"
|
||||
```
|
||||
|
||||
Own the residual human registration separately from service-client T02. Reuse
|
||||
an existing accepted UI if available; do not invent a callback on approval-engine.
|
||||
Require public S256 PKCE, exact redirect, MFA, approval:approve without consume,
|
||||
and a real human access-token proof. No service credential substitutes for this.
|
||||
HFACT-WP-0001-T03 consumes the acceptance where a human approval is required.
|
||||
|
||||
2026-09-09. The owner exists: `informed-decision` (hub repo cf4c7da8) claims the
|
||||
approver surface and owns the two strings. approval-engine was right that a
|
||||
bearer-only resource server never owned them. Both wrote in asking to be told
|
||||
early about problems with the registration shape rather than at handover, so
|
||||
their requested shape was checked against the source rather than agreed on
|
||||
paper. Three results.
|
||||
|
||||
**Scope gap — accepted, theirs was right.** `[openid, approval:approve]` cannot
|
||||
render a decision: `GET /v1/approvals/{id}` and `/claim` both need
|
||||
`approval:read`, so the surface could submit an entry it was never able to
|
||||
display. Published `[openid, approval:read, approval:approve]` in
|
||||
`docs/approval-engine-auth-contract.md` and the provisioning packet. Reading
|
||||
through a service identity was the alternative and is worse: it weakens the
|
||||
evidence-of-what-this-person-saw claim the component exists to make.
|
||||
`approval:consume` stays excluded.
|
||||
|
||||
**Assurance shape — published, and a defect fixed.** Both asked for a documented
|
||||
shape; KeyCape already emitted one, so it is written down rather than
|
||||
renegotiated: `level` (aal1|aal2, ~acr), `methods` (~amr), `mfa`, `source`, `at`.
|
||||
Writing it down surfaced that `at` was the token **mint** time, not the
|
||||
authentication time. Those differ by hours whenever a browser session is reused,
|
||||
and approval-engine persists this object verbatim as the only downstream record
|
||||
that MFA happened — so a stored approval could have evidenced MFA at a moment the
|
||||
person proved nothing. `PKCESession.AuthTime` now carries the original login
|
||||
instant through session reuse, with mint time as the fallback for sessions
|
||||
predating the field. Covered by `src/internal/server/oidc/assurance_time_test.go`.
|
||||
|
||||
**Blocker found before anyone built on it — a human token cannot carry
|
||||
`tenant:platform`.** `effectiveTenant` resolves the human tenant from the
|
||||
directory user, not the client registration, and no adapter populates
|
||||
`domain.User.Tenant`, so every human token defaults to `tenant:coulomb`. The
|
||||
per-client `tenant` field that carries `tenant:platform` on the two service
|
||||
clients is read only on the `client_credentials` path. approval-engine compares
|
||||
tenant by exact string equality and pins near-miss spellings as refused, so an
|
||||
approver token issued today would be rejected — and it would present as a failed
|
||||
approval, not as a registration defect.
|
||||
|
||||
Two resolutions, sent to both owners (`67132ddc`, `a6a070a1`, pointer
|
||||
`42d435e8`) rather than chosen here, because the choice decides what a human's
|
||||
tenant *means*: (1) directory-sourced, approvers carry `tenant:platform` on their
|
||||
LLDAP record — keeps tenant a property of the person, needs a directory attribute
|
||||
and an owner, and changes those users everywhere; (2) registration-bound and
|
||||
fail-closed, symmetric with the service clients and with decision
|
||||
`5ed3fb35-eca9-413a-82b9-95171ba85bf6` — defensible only because registrations
|
||||
are static and deployment-owned, since dynamic registration is excluded by
|
||||
design. KeyCape leans to (2) and deliberately implemented neither: (1) is a
|
||||
directory-ownership question and (2) writes a cross-tenant capability into the
|
||||
issuer.
|
||||
|
||||
**Tenant blocker resolved in code, 2026-09-09.** On review the choice did not
|
||||
have to be made to unblock: the two resolutions differ in where a human's tenant
|
||||
*comes from*, and an implementation exists that is correct under either. A client
|
||||
registration may now declare a `tenant`, and `humanTenant()` resolves it by four
|
||||
rules — no declaration keeps the directory answer unchanged; a declared zone
|
||||
applies when the directory has placed the user nowhere; agreement passes; and a
|
||||
declared zone that **conflicts** with a directory assignment refuses issuance
|
||||
(`403`, `error_type: tenant_binding`) rather than relabelling the user.
|
||||
|
||||
That refusal is the whole design. A registration can bind a zone for unplaced
|
||||
users and can never move a placed one, so option (2) is available now without
|
||||
writing a general cross-tenant override into the issuer, and if the owners prefer
|
||||
option (1) the same code stops supplying the zone and starts enforcing agreement
|
||||
with it — no second migration and no window where a stale registration silently
|
||||
wins. It is safe only because registrations are static and deployment-owned;
|
||||
`docs/tenant-claim-contract.md` records that this rule must be revisited if
|
||||
dynamic client registration is ever admitted.
|
||||
|
||||
Evidence: `src/internal/server/oidc/human_tenant_test.go`, four cases including
|
||||
the relabel refusal. Verified with teeth — neutering the conflict check makes
|
||||
`TestHumanTenantRefusesToRelabelAPlacedUser` fail rather than pass silently.
|
||||
`docs/tenant-claim-contract.md` carries the resolution table,
|
||||
`docs/approval-engine-auth-contract.md` states the requirement on the approver
|
||||
client, and the provisioning packet now declares `tenant: tenant:platform`.
|
||||
|
||||
**Cleared and registered 2026-09-10.** informed-decision submitted the two
|
||||
strings with the origin already live:
|
||||
|
||||
client_id informed-decision-approver
|
||||
redirect_uri https://decisions.coulomb.social/auth/callback
|
||||
|
||||
The host is `decisions.coulomb.social`, not the `decide.coulomb.social` of an
|
||||
earlier draft. They verified the origin rather than reporting it: `/` and
|
||||
`/auth/callback` both return 200 from 92.205.62.239 on a Let's Encrypt
|
||||
certificate CN=decisions.coulomb.social valid 2026-09-10 to 2026-12-09. The path
|
||||
serves a placeholder while their surface is gated on APPROVAL-WP-0002-T01, which
|
||||
does not affect the registration — the redirect is matched as an exact string at
|
||||
`/authorize` and never fetched.
|
||||
|
||||
Published in `config/service-clients.example.yaml`: public `authorization_code`
|
||||
with S256 PKCE, `audience: approval-engine`, scopes
|
||||
`[openid, approval:read, approval:approve]`, `mfaRequired: true`, declared
|
||||
`tenant: tenant:platform`. No `secretRef` — PKCE is the whole proof — and no
|
||||
`serviceSubject` or `roles`, which validation rejects on a browser client.
|
||||
|
||||
`TestApproverRegistrationShapeIsExact` pins every field, so widening a scope or
|
||||
relaxing MFA fails the build rather than reading as an edit, and asserts the
|
||||
registration passes startup validation — which proves the KEY-WP-0028 tenant
|
||||
exemption holds for the registration that depends on it, not only in a synthetic
|
||||
case.
|
||||
|
||||
Two existing guards fired on the way in, both correctly, and neither was loosened:
|
||||
|
||||
- `TestServiceRegistrationTenantsAreExactPerDecision` refused an unreviewed client
|
||||
carrying a tenant, which is exactly its purpose. The approver was added to its
|
||||
reviewed set as a deliberate act.
|
||||
- `TestServiceRegistrationAudienceAndScopeIsolation` panicked on
|
||||
`secretRef[4:]`, an assumption that was safe while the fixture held only
|
||||
confidential clients. The approver is the first public one; the loop now guards
|
||||
on the `env:` prefix.
|
||||
|
||||
The tenant is declared, and the token says so: `tenant_source` is `registration`,
|
||||
never `directory`. Per gate-house's correction that is adequate for
|
||||
approval-engine's store-isolation gate and **never** for doctrine turning on this
|
||||
person's membership of the zone.
|
||||
|
||||
Not live: this is a published registration fragment. Deployment is owner-side and
|
||||
nothing has been issued to a real approver.
|
||||
|
||||
## Make negative rollout evidence discriminate actual issuer refusal
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T06
|
||||
status: done
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
state_hub_task_id: "65132e1d-10db-5915-a664-83beaf30365a"
|
||||
```
|
||||
|
||||
Critical-path review found `mustFail` accepted any Exchange error: timeout,
|
||||
HTTP 5xx, invalid token or JWKS failure could falsely prove excess-scope denial
|
||||
or predecessor rejection after a successful positive exchange. Preserve a bounded
|
||||
typed provider refusal, require the exact token endpoint/status/error/feature,
|
||||
and prove the checker rejects unrelated failures without exposing provider bodies.
|
||||
Publish the verified image candidate and return its immutable digest to T02.
|
||||
|
||||
Platform's 2026-09-08 return is RPF-WP-0035-T05 and proposed CCR-2026-0017/0018.
|
||||
The custody field is `CLIENT_SECRET`; Kubernetes key `client-secret` and both env
|
||||
names remain as requested. These records cover verifier-side delivery only.
|
||||
Owner approval, client-side retrieval and the actual upstream ID-token issuer
|
||||
proof remain distinct gates. Public discovery currently advertises
|
||||
`https://auth.coulomb.social`; that alone is not the signed-token observation.
|
||||
|
||||
T06 completion: full Go suite and vet passed; published code `dcebd46` and pulled the image by digest `sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`. Container `keycape verify-client --help` confirms the command is present. `docs/approval-clients-rollout.md` and its proposed deployment patch record configuration/issuer, first-provision, single-instance rollout, acceptance and rollback. This closes verifier preparation only; T02 and T05 retain the explicit live dependencies.
|
||||
|
||||
|
||||
## Prepare a bounded upstream issuer observation before client rollout
|
||||
|
||||
```task
|
||||
id: KEY-WP-0013-T07
|
||||
status: done
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
state_hub_task_id: "62bc6d8e-dbb2-57b6-a30b-472ffd06b497"
|
||||
```
|
||||
|
||||
2026-09-08: T02's actual upstream signed-token issuer gate had no executable
|
||||
observation path. A downstream OpenBao/KeyCape login cannot return the upstream
|
||||
Authelia token; it remains inside the adapter. Prepare `probe-upstream-issuer`
|
||||
and an exact-state temporary route so one attended flow can be verified before
|
||||
changing the normal KeyCape Deployment or its configuration.
|
||||
|
||||
Acceptance: signed issuer/audience/time/nonce proof without token or user-claim
|
||||
output; browser/state binding and one-shot refusal; finite issuer allowlist;
|
||||
ten-minute bound; no downstream credential; current Traefik route isolation;
|
||||
immutable image; reviewed temporary Job/config projection, network policies and
|
||||
owner-reference cleanup. Normal callbacks and the production issuer stay on
|
||||
the existing Deployment. The probe mounts only `config.yaml`, not `key.pem`,
|
||||
and has no Kubernetes API token. That file contains existing credential data;
|
||||
its use by the temporary diagnostic needs deployment-owner admission.
|
||||
|
||||
Source tests, image and deployment packet close this preparation task. The
|
||||
attended live receipt and issuer pin remain in T02, alongside the named CCR
|
||||
reviews and custody/rollout acceptance. Preparing the diagnostic does not
|
||||
approve CCR-2026-0017/0018 or close any live factory gate.
|
||||
|
||||
T07 preparation complete: source `6f33abd`, immutable image
|
||||
`sha256:0c85ed377cae7ae6ca5b5c56b1a52930e706b3cb78009747e42f551e869a22e4`.
|
||||
Complete Go race suite and vet passed; five renderer/collector checks, seven
|
||||
real Traefik 3.6.7 route cases and Railiance server dry-run passed. The packaged
|
||||
command ran as UID 65534 with no network and a read-only filesystem and produced
|
||||
the expected bounded idle-timeout receipt. See `docs/upstream-issuer-proof.md`
|
||||
and `docs/evidence/upstream-issuer-probe.json`. T02 retains the live proof and
|
||||
configuration-owner admission; no production resource or custody value changed.
|
||||
|
||||
## Status 2026-09-30 (CUST-WP-0074)
|
||||
|
||||
All tasks T01-T07 are `done`; nothing in this plan is waiting, so it is
|
||||
`finished` rather than `blocked`. Residual items live in other plans
|
||||
(RPF-WP-0035-T05/T06, AUDIT-WP-0009-T09, KEY-WP-0014-T04). The one open
|
||||
question recorded above, disabling the unpresented create+approve client, is a
|
||||
KeyCape deployment decision to be tracked in a new plan if taken up.
|
||||
|
||||
**2026-10-10:** KeyCape proposed the joint disablement sequence that
|
||||
railiance-platform asked for on 2026-09-15, to railiance-platform, approval-engine
|
||||
and net-kingdom: remove the registration (attended), verify `invalid_client`,
|
||||
unbind the env, retire the ESO delivery, then retire custody. The proposal executes
|
||||
nothing. Each owner confirms their step and the founder schedules the attended window.
|
||||
|
||||
**2026-10-10, amended by the owners.** net-kingdom: step 1 (`keycape-client.py
|
||||
apply --operation remove`, case acceptance CCR-2026-0018) only patches
|
||||
`sso/keycape-config` and does not restart. The step-3 env removal triggers the one
|
||||
rollout, and the new pod starts with neither the client nor the env. Step 2's
|
||||
`invalid_client` check follows that rollout. Rollback runs in reverse: env first,
|
||||
then the client. railiance-platform (RPF-WP-0035-T10): step 4 also retires the
|
||||
ClusterSecretStore, the Kubernetes-auth role and the reader policies, through an
|
||||
attended prune sync run strictly after step 3. Step 5 is a metadata delete, not an
|
||||
archive, run only after the step-2 receipt. Steps 1-5 can share one attended
|
||||
window. Still open: approval-engine's reconfirmation that there is no presenter,
|
||||
and the founder scheduling the window. Before that rollout, run
|
||||
`keycape check-config` on the patched configuration.
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
---
|
||||
id: KEY-WP-0014
|
||||
type: workplan
|
||||
title: "Review native login and client credential lane handoffs"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: native-credential-lane-handoff
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-10"
|
||||
state_hub_workstream_id: "0d003df3-f7d3-5063-8ca0-e1e33f7df74a"
|
||||
---
|
||||
|
||||
Source: ops-warden inbox message 0dd9c7bd-0ecd-42d1-806f-7cc4ba9730ed.
|
||||
The native service exchange and public PKCE login commands are now implemented.
|
||||
Cross-owner rotation and consumer-specific route cutover remain outstanding.
|
||||
|
||||
## Design owner command and custody boundaries
|
||||
|
||||
```task
|
||||
id: KEY-WP-0014-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0c0a0b61-c19e-5631-9cda-8b2dc0f47d8f"
|
||||
```
|
||||
|
||||
Review ops-warden's existing key-cape-oidc-login proxy and
|
||||
rapp-qonto-keycape-client route contracts. Specify the native interactive login
|
||||
and bounded exchange commands, token delivery, renewal and custody-mediated
|
||||
rotation before implementation. Keep secret custody with OpenBao and avoid
|
||||
retiring the proxy until replacement commands have equivalent verification.
|
||||
|
||||
## Verify handoff delivery evidence
|
||||
|
||||
```task
|
||||
id: KEY-WP-0014-T02
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "d9a5de97-b7d5-5599-98c2-eaab32f51495"
|
||||
```
|
||||
|
||||
Ops-warden reports KEY-WP-0009-T04's claimed reply did not arrive. Verify prior
|
||||
receipts for all four named recipients before claiming successful notification.
|
||||
No outbound coordination messages were sent during the 2026-09-05 repo review.
|
||||
|
||||
|
||||
## Implement and verify native caller commands
|
||||
|
||||
```task
|
||||
id: KEY-WP-0014-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4e46f474-a15e-594f-ae07-ad37a9667d89"
|
||||
```
|
||||
|
||||
Implemented keycape service-token and login with HTTPS discovery, RS256/JWKS
|
||||
verification, exact audience bindings, PKCE/state/nonce, private out-of-Git file
|
||||
delivery and redacted errors. Service Basic authentication now form-decodes
|
||||
credentials. Integration tests exercise the real token/JWKS handlers and browser
|
||||
callback, including forged state, tampering, excess scope and unsafe output.
|
||||
See docs/native-authentication.md for invocation and ownership boundaries.
|
||||
|
||||
|
||||
2026-09-10. The authority question is answered and the blocker is narrowed to a
|
||||
schedulable act. railiance-platform named the executor (platform operator,
|
||||
attended founder session; explicitly not ops-warden, not secrets-engine
|
||||
autonomously, and no unattended agent), the transport (the governed
|
||||
`openbao-platform-admin-login` lane via `warden access ... --exec`), and the
|
||||
authority (founder_required attended OIDC, role=platform-admin). What is missing
|
||||
is a rotation CCR, which they will prepare *if KeyCape asks* — a two-custodian
|
||||
CAS rotation with a service restart being a distinct version-guarded operation
|
||||
rather than an implementation detail of an existing lane.
|
||||
|
||||
**Not asked, deliberately.** Requesting a production credential rotation with a
|
||||
restart window is the operator's decision, not this repository's, so it was put
|
||||
to him rather than answered here (ops-warden msg 10fcc8af).
|
||||
|
||||
Established before recommending anything either way: **this rotation has no
|
||||
incident driver.** KEY-WP-0011 recovered a real exposure on 2026-08-23, and the
|
||||
coordinated rotation that followed replaced the RS256 signing key, the LLDAP bind
|
||||
credential, the Authelia client credential and the privacyIDEA admin token. The
|
||||
`rapp-qonto` client secret was not in that payload — it is an `env:` secretRef
|
||||
resolved from a separate Kubernetes Secret, never inline in `config.yaml`, which
|
||||
config validation enforces. So this is lifecycle hygiene, not remediation, and
|
||||
nobody should be carrying it as leftover incident work. Hygiene can wait for a
|
||||
chosen window; remediation could not.
|
||||
|
||||
Also recorded: `warden route`/`warden plan` previously returned a generic match
|
||||
that read as authorization, and offered read transports against a write need.
|
||||
This repository declined to act on it; railiance-platform independently called it
|
||||
a defect; fixed under WARDEN-WP-0038.
|
||||
|
||||
## Admit rotation and verify consumer handoff
|
||||
|
||||
```task
|
||||
id: KEY-WP-0014-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e7632c71-9676-5837-a925-7f905e3015c5"
|
||||
```
|
||||
|
||||
Qonto rotation requires admitted custody/provider transaction and rollback
|
||||
semantics; see the exact review sequence in docs/native-authentication.md.
|
||||
The current login proxy yields an OpenBao token, so it cannot be replaced by a
|
||||
JWT-writing command without adapting its consumer contract. Verify that split
|
||||
with the platform owner and ops-warden before route changes. No route was changed.
|
||||
|
||||
2026-09-05 receipt audit: GET /messages/?from_agent=key-cape returned one
|
||||
2026-03 image-publication message and no KEY-WP-0009 handoff receipts for
|
||||
NetKingdom, secrets-engine, ops-warden or railiance-platform. Corrected the
|
||||
unsupported statement in KEY-WP-0009-T04. This proves only that no receipts
|
||||
are present in the current Hub response, not that no message ever existed.
|
||||
The 2026-09-05 audit's finding stands as recorded; the replacement outbound
|
||||
messages were sent on 2026-09-08 (below).
|
||||
|
||||
2026-09-08: `0a6c9d88-cf50-45d8-acdc-99a40747683e` -> ops-warden, readable back
|
||||
via `GET /messages/?from_agent=key-cape`. It puts both open questions to the
|
||||
route and custody owner in one place:
|
||||
|
||||
1. The `key-cape-oidc-login` proxy yields an OpenBao token and `keycape login`
|
||||
yields an issuer JWT — different credential types, different verifiers,
|
||||
different enforcement owners, so not interchangeable. Two admissible options
|
||||
are offered: keep the proxy and run `keycape login` alongside it for JWT
|
||||
consumers, or adapt the route's consumer contract to a JWT and then cut over.
|
||||
Option (a) is recommended until every consumer of the route is enumerated.
|
||||
2. Qonto rotation for `rapp-qonto-keycape-client`, quoting the five-step reviewed
|
||||
sequence from `docs/native-authentication.md` and asking who executes the
|
||||
successor generation and CAS update, in which transport, under what authority.
|
||||
It records that `warden route`/`warden plan` returned no exact admitted lane
|
||||
and that a generic match is not being treated as authorization.
|
||||
|
||||
The KEY-WP-0009 handoff ops-warden reported missing has also now been delivered
|
||||
(`5bc58847-eed5-4164-bb0f-a0d8455e75b8`), closing the KEY-WP-0014-T02 correction.
|
||||
|
||||
Task remains `wait` — no route was changed and no rotation command ships until
|
||||
ops-warden answers (1) and an execution/rollback authority is admitted for (2).
|
||||
|
||||
2026-09-08, second pass — read ops-warden's own catalog rather than waiting for a
|
||||
reply, and it changes the shape of both questions.
|
||||
|
||||
`registry/routing/catalog.yaml`, lane `key-cape-oidc-login`: `intended_owner` was
|
||||
already corrected to key-cape, and `verified: asked-and-waiting` since
|
||||
2026-08-28. **ops-warden is waiting on us, not the other way round.** The lane is
|
||||
also `warden_executes: false` with a `wiki_ref` into `CredentialRouting.md` — a
|
||||
pointer, per their ADR-0001, not a code path with programmatic consumers. So the
|
||||
consumer-contract risk behind question (1) is smaller than assumed: adding
|
||||
`keycape login` alongside retires nothing, and the acceptance we owe them is a
|
||||
statement of ownership, not a cutover.
|
||||
|
||||
Lane `rapp-qonto-keycape-client`: `rotation.owner: key-cape`,
|
||||
`automatable: true`, and `blocked_on` reads "still no key-cape-native
|
||||
exchange/rotation command", `verified: source-read` 2026-08-28. **That blocker is
|
||||
stale.** `keycape service-token` (T03) shipped 2026-09-05 and is exactly the
|
||||
native `client_secret_basic` exchange they record as absent. It does not clear
|
||||
the lane, and should be narrowed rather than closed.
|
||||
|
||||
Shipped here: `keycape verify-client`, which is rotation step 4 and the denial
|
||||
checks step 5 rests on — new exchange verified against deployed JWKS, predecessor
|
||||
refused, excess scope refused, no value printed or written. See
|
||||
`docs/native-authentication.md`, "Verifying a live registration". The identical
|
||||
predecessor case is treated as a failure, because an unchanged secret is a
|
||||
rotation that did not happen.
|
||||
|
||||
Steps 1-3 remain custody's and are deliberately not automated. Task stays `wait`
|
||||
on exactly two answers: which option ops-warden takes for the login proxy, and
|
||||
who executes the successor generation and CAS update under what authority. No
|
||||
route changed.
|
||||
|
||||
Both catalog corrections were sent, receipts readable via
|
||||
`GET /messages/?from_agent=key-cape`:
|
||||
|
||||
- `12f1bdfa-b240-414b-aa49-128a4d2acf20` — accepts ownership of lane
|
||||
`key-cape-oidc-login`, answering the WARDEN-WP-0033-T04 follow-up they have
|
||||
carried as `asked-and-waiting` since 2026-08-28. States the boundary the lane's
|
||||
`fetch_command` crosses: key-cape owns browser authentication and the identity
|
||||
token; the `netkingdom` auth mount, `role=<domain>` mapping, token store and
|
||||
enforcement of the resulting OpenBao token are not ours. Supplies the source
|
||||
paths they need to move the lane to `verified: source-read`, and records our
|
||||
revised, lower estimate of the consumer risk — a pointer lane under ADR-0001
|
||||
retires nothing when `keycape login` runs alongside it — while asking them to
|
||||
correct us if the catalog understates its consumers.
|
||||
- `08d42f47-8e19-4976-98f5-dd259e7b1225` — reports lane
|
||||
`rapp-qonto-keycape-client`'s blocker as stale against its 2026-08-28
|
||||
source-read, since `keycape service-token` is the native `client_secret_basic`
|
||||
exchange it records as absent and `keycape verify-client` is its rotation step 3
|
||||
verbatim. Asks them to **narrow** it to steps 1-2 rather than clear it, with
|
||||
proposed replacement text, and notes `rotation.automatable: true` is now
|
||||
half-true and should say so precisely rather than be flipped either way.
|
||||
|
||||
Neither message asks for a route change and neither was one.
|
||||
|
||||
2026-09-08/09 — ops-warden answered both, and the second answer matters more than
|
||||
the question did.
|
||||
|
||||
**Item 1 answered: option (a).** Keep the proxy as-is and run `keycape login`
|
||||
alongside it, "for your reason rather than out of caution -- they are different
|
||||
credential types with different verifiers, so this is not a cutover deferred, it
|
||||
is two things that were never one." They verified our pointer-layer reading
|
||||
rather than accepting it, and added a fact we did not have: `warden access`
|
||||
already excludes `is_login` lanes from raw-value streaming
|
||||
(WARDEN-WP-0032-T05). Lane `key-cape-oidc-login` moved to `owner-confirmed` with
|
||||
our boundary written in verbatim (commit c133004). It stays `interim` on purpose:
|
||||
per their ADR-0003 a lane retires when a front door exists, not when ownership is
|
||||
agreed, and by our own analysis `keycape login` is not that front door. The entry
|
||||
now names the retirement condition instead of implying we owe one.
|
||||
|
||||
**Item 2 answered: nobody owns it, admittedly.** Steps 1-2 mutate custody on
|
||||
platform workload paths, which is railiance-platform's, with `secrets-engine exec`
|
||||
as the admitted transport. ops-warden declined to take the act on the grounds
|
||||
that fronting the read lane is not authority to mutate — their ADR-0002 and
|
||||
ADR-0005 — and routed it themselves rather than describing the route and leaving
|
||||
it to us. Lane `rapp-qonto-keycape-client` narrowed exactly as asked and moved
|
||||
`source-read` -> `owner-confirmed`, crediting the correction to us rather than to
|
||||
a re-read they did not perform. Rotation step 3 now points at
|
||||
`keycape verify-client` instead of restating its assertions.
|
||||
|
||||
**They found a defect in their own front door because of our caution, and told
|
||||
us.** Running our need through `warden plan` returns `verdict: autonomous`,
|
||||
`lane_id: rapp-qonto-keycape-client`, answered with three read transports — a
|
||||
need containing *generate* and *CAS-write* inherits the verdict of the lane that
|
||||
reads the same path, because `warden plan` has no read-versus-mutate intent at
|
||||
all. `autonomous` is documented as the signal to proceed without the founder.
|
||||
Raised as WARDEN-WP-0038. Their stated conclusion, which we should not soften: "A
|
||||
counterparty declining to trust our output is not a control, and we are not going
|
||||
to record it as one."
|
||||
|
||||
Standing instruction from them until WP-0038 lands, recorded here so it is not
|
||||
lost in an inbox: **treat a `warden plan` verdict on any need containing a write,
|
||||
rotate or provision act as unreliable.** That applies to this repository whenever
|
||||
it plans a rotation.
|
||||
|
||||
**On `automatable`, they did not do what we suggested and said so.** We asked
|
||||
them not to flip the boolean and to state the half-truth precisely; the schema has
|
||||
one boolean whose only consumer is a future executable driver, and a driver told
|
||||
`true` would attempt the custody steps. So it is `false`, failing safe, with the
|
||||
precision moved into per-step notes. They offered to add a per-step field if we
|
||||
would rather. We would not: a lane-level flag that fails safe plus per-step notes
|
||||
is the right shape, and asking them to change a schema for our convenience would
|
||||
be worse than the imprecision. No reply needed on that point.
|
||||
|
||||
T04 stays `wait`: the rotation authority now has a named owner
|
||||
(railiance-platform, transport `secrets-engine exec`) but does not yet exist, and
|
||||
nothing here changed a route.
|
||||
|
||||
2026-09-09, guarding against a misreading that is now easy to make. The approval
|
||||
clients went live and `docs/evidence/2026-09-09-keycape-verifier-admission.json`
|
||||
records a passing verifier run, so there is now a committed receipt in this repo
|
||||
that looks like rotation evidence and is not. It records
|
||||
`real_predecessor_rotation_tested: false` and `observed_wall_clock_expiry: false`,
|
||||
and under `limits`, `actual_predecessor_rotation_observed: false` and
|
||||
`wall_clock_jwt_expiry_observed: false`. railiance-platform stated the same two
|
||||
gaps unprompted from their side (`ae6ff4bc`): "do not cite this receipt as
|
||||
rotation evidence."
|
||||
|
||||
So `keycape verify-client`'s predecessor rejection has been exercised only
|
||||
against unit tests and a first provisioning where predecessor and successor were
|
||||
never distinct. Rotation step 4 is implemented and unproven, which is a different
|
||||
state from either "missing" or "done", and T04 is where that distinction lives.
|
||||
|
||||
**2026-09-10: the authority is answered. What remains is an artifact and a
|
||||
decision.** railiance-platform answered the routed question (`7c7228ac`, relayed
|
||||
by ops-warden `b14655b3`):
|
||||
|
||||
- **Who:** the platform operator, attended, as a founder-attended session. Not
|
||||
ops-warden, not secrets-engine autonomously, and no unattended agent —
|
||||
railiance-platform named themselves in that exclusion too.
|
||||
- **Transport:** the governed `openbao-platform-admin-login` lane, invoked only
|
||||
through `warden access openbao-platform-admin-login --exec -- <command>` with a
|
||||
unique metadata receipt path. The owner command is never run directly.
|
||||
- **Authority:** `founder_required` attended OIDC via netkingdom
|
||||
`role=platform-admin`. Nothing else in that repo carries write authority
|
||||
against `platform/workloads/`.
|
||||
|
||||
**Missing: a rotation CCR.** They will not raise one speculatively, and their
|
||||
reason is sound — a two-custodian CAS rotation with a service restart is a
|
||||
distinct version-guarded operation, not an implementation detail of an existing
|
||||
lane. It must name the CAS precondition and expected version on *both*
|
||||
custodians, sibling-field preservation on the KV write, the restart window,
|
||||
bounded predecessor retention for rollback and denial checks, and the
|
||||
reconcile-to-same-version step on failure. That last is step 5 of the sequence in
|
||||
`docs/native-authentication.md`, so the reviewed sequence is what the CCR encodes.
|
||||
|
||||
**Blocked on a decision that is not KeyCape's to take unilaterally.** Their offer:
|
||||
"if key-cape wants the rotation scheduled, say so and I will prepare it under
|
||||
RPF-WP-0035 with both custodians in one window." ops-warden deliberately did not
|
||||
ask on our behalf — correctly, per their ADR-0003, since implying an owner request
|
||||
from a routing errand is quiet ownership. So T04 now waits on the repository owner
|
||||
deciding whether the Qonto rotation should happen, and when. Nothing indicates
|
||||
compromise; this is hygiene plus the first real exercise of rotation step 4, which
|
||||
is why it is a judgement call rather than an obvious yes.
|
||||
|
||||
**Precedent offered, and it is the right one.** The identical two-custodian shape
|
||||
was exercised 2026-09-09 for the approval clients: attended founder session,
|
||||
OpenBao KV custody plus a Kubernetes-delivered consumer copy, compatible pinned
|
||||
rollout, protected recovery retained, and rollback that restored configuration and
|
||||
image and detached delivery before a clean resume. Their
|
||||
`scripts/keycape_approval_custody.py` (activate / resume-activate / verify) is the
|
||||
shape a rapp-qonto rotation should follow rather than a bare `bao kv patch` — for
|
||||
exactly the provider/consumer consistency reason this task gave when it declined
|
||||
to ship a wrapper around a raw KV write. They endorsed that refusal unprompted.
|
||||
|
||||
**And the `warden plan` defect is fixed** (WARDEN-WP-0038, 2026-09-10). The Qonto
|
||||
need now returns `founder_required` naming the write owner, and read transports
|
||||
are no longer offered against a write need. ops-warden's closing line is worth
|
||||
keeping: "Your caution was load-bearing and it should not have had to be."
|
||||
|
||||
If the rotation is scheduled, it is also the first opportunity to close the
|
||||
`real_predecessor_rotation_tested: false` gap recorded above — `verify-client`
|
||||
with `-previous-secret-env` is exactly step 4, and the predecessor would finally
|
||||
be genuinely distinct.
|
||||
|
||||
**Decision, 2026-09-10: not yet.** The repository owner declined to schedule the
|
||||
rotation now. Recorded with the reasoning so it is revisited on evidence rather
|
||||
than re-litigated from scratch: nothing indicates compromise, the offer stands
|
||||
open, and the estate already has one founder-attended window pending for the two
|
||||
fail-closed startup changes. Rotating for hygiene alone, in a second attended
|
||||
session, against a credential with no incident attached, is work the owner chose
|
||||
not to spend now.
|
||||
|
||||
This is a deferral, not a refusal, and the thing that should reopen it is a
|
||||
change in evidence rather than the passage of time: an actual exposure or
|
||||
suspected one (see KEY-WP-0011), the Qonto secret's age becoming a stated concern,
|
||||
a consumer requiring proof of rotation, or a decision to prove rotation step 4
|
||||
before relying on it elsewhere. Any of those makes the answer different; a
|
||||
calendar date does not.
|
||||
|
||||
**Closed 2026-09-10.** The paragraph above left T04 `wait` with nothing
|
||||
outstanding, which is a contradiction: `wait` means waiting or blocked, and this
|
||||
was neither. Re-reading the task against its own title settles it — *admit
|
||||
rotation and verify consumer handoff* — and both halves are delivered:
|
||||
|
||||
- **Rotation admitted.** The semantics were reviewed and published
|
||||
(`docs/native-authentication.md`), the executor and authority are named
|
||||
(platform operator, attended founder session, governed
|
||||
`openbao-platform-admin-login` lane, `founder_required` attended OIDC via
|
||||
`role=platform-admin`), the transport is admitted, and the CCR will be prepared
|
||||
on request. Step 4 shipped here as `keycape verify-client`, treating an
|
||||
identical predecessor as a failure because an unchanged secret is a rotation
|
||||
that did not happen. Steps 1–3 are custody's and are deliberately not
|
||||
automated by this repository.
|
||||
- **Consumer handoff verified.** ops-warden took option (a) for the reason we
|
||||
gave rather than out of caution — the proxy yields an OpenBao token and
|
||||
`keycape login` an issuer JWT, so they were never one thing to cut over
|
||||
between. Lane `key-cape-oidc-login` is `owner-confirmed` with our boundary
|
||||
written in verbatim, and `rapp-qonto-keycape-client` was narrowed to steps 1–2
|
||||
rather than cleared. No route was changed.
|
||||
|
||||
*Executing* a rotation was never this task's deliverable, and treating the
|
||||
owner's deferral as an open item would have kept a task open against work it was
|
||||
never scoped to do. The deferral stands on its own above, revisited on evidence.
|
||||
|
||||
Two residuals are recorded rather than carried here, because they belong to
|
||||
whoever next opens a window: `real_predecessor_rotation_tested: false` and
|
||||
`observed_wall_clock_expiry: false`. `verify-client -previous-secret-env` is
|
||||
ready to close the first the moment a genuinely distinct predecessor exists.
|
||||
|
||||
Also standing, from ops-warden and not to be lost: until WARDEN-WP-0038 lands,
|
||||
**treat a `warden plan` verdict on any need containing a write, rotate or
|
||||
provision act as unreliable** — it inherits the verdict of the lane that reads
|
||||
the same path, and `autonomous` is documented as the signal to proceed without
|
||||
the founder. That applies to this repository whenever it plans a rotation.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
id: KEY-WP-0015
|
||||
type: workplan
|
||||
title: "Reconcile repository scope with implementation and intent"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: scope-intent-assessment
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-05"
|
||||
state_hub_workstream_id: "14a7312b-ec92-5640-8b26-cbaa4753c18e"
|
||||
---
|
||||
|
||||
## Inventory implementation and document scope gaps
|
||||
|
||||
```task
|
||||
id: KEY-WP-0015-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "99be1489-d9e5-53b1-9c82-75dc961131f7"
|
||||
```
|
||||
|
||||
Reviewed source revision b989de4, INTENT, SCOPE, runtime composition, authentication
|
||||
commands, adapters, migration/validator code, tests, packaging and open workplans.
|
||||
Updated SCOPE.md to describe implemented surfaces and their actual limits.
|
||||
Recorded evidence, ten prioritized gaps and closure criteria in
|
||||
history/2026-09-05-011726-scope-intent-assessment.md. INTENT and implementation are
|
||||
unchanged. Findings are assessment results, not implementation completion claims.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
id: KEY-WP-0016
|
||||
type: workplan
|
||||
title: "Authorization-code protocol hardening and token verification bindings"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: authorization-code-protocol-hardening
|
||||
created: "2026-09-06"
|
||||
updated: "2026-09-06"
|
||||
state_hub_workstream_id: "22698171-f283-59a9-9443-183faaeb6287"
|
||||
---
|
||||
|
||||
Closes gap G01 of `history/2026-09-05-011726-scope-intent-assessment.md`, the
|
||||
assessment's first recommended item. The browser grant validates PKCE, client id
|
||||
and scopes but does not bind the redirect URI, authenticate confidential clients,
|
||||
enforce grant-type eligibility, or consume the authorization code atomically.
|
||||
UserInfo verification is weaker than the caller CLI's contract.
|
||||
|
||||
Scope is the local protocol surface only. Upstream provider-token verification
|
||||
(the Authelia adapter's deliberate no-signature-check trust assumption) is a
|
||||
separate trust-contract decision and is not addressed here.
|
||||
|
||||
## Consume authorization codes atomically
|
||||
|
||||
```task
|
||||
id: KEY-WP-0016-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a5171df0-4905-518d-a52f-7152b980af79"
|
||||
```
|
||||
|
||||
`SessionStore.Get` reads a code and `Delete` removes it in separate lock
|
||||
acquisitions, with signing in between, so two simultaneous exchanges can both
|
||||
observe the same session before either deletes it. Replace the token path's
|
||||
lookup with a single-operation consume that removes the session under the same
|
||||
lock that reads it, so exactly one concurrent exchange can succeed. A failed
|
||||
exchange must not leave a replayable code. Cover with a concurrent-reuse test
|
||||
that asserts exactly one success across parallel requests.
|
||||
|
||||
Added `SessionStore.Consume`, which reads and deletes under one lock, and moved
|
||||
the token path onto it. Verified the defect was real before claiming the fix:
|
||||
with `Get`/`Delete` restored, 9 of 16 concurrent exchanges succeeded and a
|
||||
PKCE-failed exchange left the code replayable. Both now fail closed —
|
||||
`TestConcurrentCodeExchangeSucceedsExactlyOnce`, `TestFailedExchangeConsumesTheCode`.
|
||||
|
||||
## Bind redirect URI, grant type and confidential client authentication
|
||||
|
||||
```task
|
||||
id: KEY-WP-0016-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1d0fb108-273f-5c9a-a7c7-7640f19bad9d"
|
||||
```
|
||||
|
||||
On the authorization-code path: require `redirect_uri` and compare it for exact
|
||||
equality with the value stored at authorization time; reject a client whose
|
||||
registration does not permit `authorization_code` (an empty `grantTypes` stays an
|
||||
implicit authorization-code client, matching config validation, but a
|
||||
`client_credentials`-only client must be refused); and authenticate confidential
|
||||
clients with a constant-time secret comparison, accepting the same
|
||||
`client_secret_basic` and form-encoded credentials as the service grant. Public
|
||||
clients continue to present no secret and must not be able to supply one to
|
||||
impersonate a confidential registration. Negative tests per condition.
|
||||
|
||||
Implemented all three in `token.go`, with `basicClientCredentials` and
|
||||
`secretsEqual` shared with the service grant (digest-first comparison, so a
|
||||
wrong-length secret is indistinguishable from a wrong value). Tests:
|
||||
`TestCodeExchangeRejectsRedirectURIMismatchAndOmission` (omitted, different,
|
||||
traversal-shaped and trailing-slash variants),
|
||||
`TestCodeExchangeRejectsServiceOnlyClient`,
|
||||
`TestCodeExchangeAllowsImplicitAuthorizationCodeClient`,
|
||||
`TestConfidentialCodeClientRequiresItsSecret` and
|
||||
`TestPublicClientSecretIsIgnoredAndEmptyConfidentialSecretIsRejected`. The
|
||||
`keycape login` CLI already sent `redirect_uri`, so no caller change was needed;
|
||||
the authclient test stub was recording no redirect and is now faithful to real
|
||||
`/authorize`.
|
||||
|
||||
## Harden UserInfo token verification
|
||||
|
||||
```task
|
||||
id: KEY-WP-0016-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "336db63a-e4fd-522d-86b6-0bf54a31af1c"
|
||||
```
|
||||
|
||||
UserInfo verifies an RSA signature and expiry but ignores its own `Issuer` field
|
||||
and does not check the JOSE header algorithm, the issuer claim, or the token
|
||||
purpose, so an ID token or a foreign-issuer token of the right shape is accepted
|
||||
where an access token is required. Enforce `alg=RS256` from the header, reject
|
||||
`none` and any other algorithm, require the configured issuer, and reject tokens
|
||||
that are not access tokens. Negative tests per condition.
|
||||
|
||||
`validateJWT` became `validateAccessToken`, checking the JOSE header before
|
||||
trusting the signature, requiring the configured issuer (an empty configured
|
||||
issuer is a misconfiguration, not a skipped check), and requiring the `scope`
|
||||
claim the token endpoint sets only on access tokens. Purpose is decided
|
||||
verification-side deliberately: adding a `token_use` claim would change the
|
||||
issued-token contract that consumers pin exactly. Tests:
|
||||
`TestUserinfoRejectsUnverifiedTokenShapes` (foreign issuer, missing issuer, ID
|
||||
token presented as an access token) and `TestUserinfoRejectsNonRS256Algorithms`,
|
||||
which restates the header algorithm over a genuine RS256 signature so it
|
||||
isolates the header check.
|
||||
|
||||
## Reconcile scope and assessment records
|
||||
|
||||
```task
|
||||
id: KEY-WP-0016-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "90efa6ef-9495-5716-bee2-49d01c9ce05d"
|
||||
```
|
||||
|
||||
Update `SCOPE.md`'s protocol-hardening limit and G01's closure status in the
|
||||
assessment to state exactly which bindings are now enforced and which remain
|
||||
open, naming the upstream-trust item as still outstanding. Do not claim complete
|
||||
profile conformance: G01 closure covers these bindings, not the whole profile.
|
||||
|
||||
Updated `SCOPE.md`'s protocol-hardening limit and recorded a partial-closure
|
||||
status under G01 in the assessment, naming the Authelia upstream-trust item as
|
||||
still open. G01 is explicitly not fully closed.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
id: KEY-WP-0017
|
||||
type: workplan
|
||||
title: "Reconcile the canonical model and discovery metadata with the runtime"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: canonical-model-and-discovery-conformance
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "0904caad-ad8b-573f-9cdb-b6da09262a18"
|
||||
---
|
||||
|
||||
Closes gap G02 of `history/2026-09-05-011726-scope-intent-assessment.md`.
|
||||
`spec/canonical-model.yaml` and `src/internal/domain/model.go` both claim to be
|
||||
the source of truth for the client registration shape, and they disagree: the
|
||||
spec restricts grants to `authorization_code`, requires redirect URIs for every
|
||||
client, and omits the service-subject, tenant, audience, lifetime, role and
|
||||
MFA/handoff fields the runtime actually reads. Discovery advertises a fixed basic
|
||||
scope and claim list that omits core profile claims and every configured resource
|
||||
scope.
|
||||
|
||||
The point of the work is the executable link, not the edit: reconciling the two
|
||||
files once leaves them free to drift again the same afternoon.
|
||||
|
||||
## Reconcile the client entity with the runtime model
|
||||
|
||||
```task
|
||||
id: KEY-WP-0017-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "996f6895-f36b-585e-844d-3e55d02eefda"
|
||||
```
|
||||
|
||||
Bring the `Client` entity in `spec/canonical-model.yaml` in line with
|
||||
`domain.Client`: add `audience`, `serviceSubject`, `tenant`, `roles`,
|
||||
`mfaRequired`, `registrationUrl` and `enrollmentUrl`; extend the `grantTypes`
|
||||
enum to include `client_credentials`; and make `redirectUris` required only for
|
||||
clients that use the authorization-code grant, matching what config validation
|
||||
already enforces. Record which fields are deliberately absent from the canonical
|
||||
model rather than silently omitted — `clientSecret` and `tokenLifetime` are
|
||||
runtime policy, not identity data, and both are `yaml:"-"` in the Go model.
|
||||
|
||||
Added all seven fields, extended the grant enum, and made `redirectUris`
|
||||
optional at the schema level with the authorization-code condition stated in its
|
||||
description. `tokenProfile` and `environments` were declared in the spec but read
|
||||
by nothing; they are kept and marked `runtime: false` rather than deleted, since
|
||||
removing them would discard intent the check can now hold honest.
|
||||
|
||||
## Add an executable conformance check
|
||||
|
||||
```task
|
||||
id: KEY-WP-0017-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "77df5043-2b23-56cd-947e-317f7117ac18"
|
||||
```
|
||||
|
||||
Add a test that compares the canonical model's `Client` field set against
|
||||
`domain.Client`'s YAML tags by reflection and fails on divergence in either
|
||||
direction: a Go field with no spec entry, or a spec field the runtime does not
|
||||
read. Fields intentionally excluded must be named in one list the test reads, so
|
||||
excluding a field is a deliberate, reviewable act rather than an omission. This
|
||||
is the generation/conformance link the assessment asks for.
|
||||
|
||||
`src/internal/domain/conformance_test.go` checks `Client`, `User`, `Group` and
|
||||
`Membership` by reflection over YAML tags. It earned its place on the first run
|
||||
by finding drift outside the assessment's list: `User.tenant` was read by the
|
||||
runtime and undeclared in the spec. `yaml:"-"` fields are excluded by
|
||||
construction, which is what keeps `clientSecret` and `tokenLifetime` out without
|
||||
a hand-maintained exception list.
|
||||
|
||||
## Reconcile discovery metadata with the profile surface
|
||||
|
||||
```task
|
||||
id: KEY-WP-0017-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5eb36f9b-a8e0-541e-8f81-310db49b77a0"
|
||||
```
|
||||
|
||||
`claims_supported` omits `tenant`, `principal_type`, `assurance`, `scope` and
|
||||
`nonce`, which the token endpoint emits on every token, and `scopes_supported` is
|
||||
a fixed list that omits every configured resource scope. Advertise the core
|
||||
profile claims and derive the scope list from the registered clients. Keep the
|
||||
distinction the assessment asks for: an omitted core claim is a contract defect,
|
||||
while optional discovery metadata is not, so document which is which rather than
|
||||
treating every omission alike.
|
||||
|
||||
`claims_supported` now lists the core profile claims (`tenant`, `principal_type`,
|
||||
`assurance`, `scope`, `nonce`) alongside the scope-gated human claims and the
|
||||
optional cached `tenant_roles`. `scopes_supported` is derived from the registered
|
||||
clients — baseline OIDC scopes first, configured resource scopes sorted after, so
|
||||
the document is stable regardless of map iteration order — with the baseline
|
||||
still advertised when no clients are configured.
|
||||
`TestDiscoveryAdvertisesEveryCoreProfileClaim` states the distinction the
|
||||
assessment asked for: a missing core claim fails, optional metadata does not.
|
||||
|
||||
## State a single source of truth
|
||||
|
||||
```task
|
||||
id: KEY-WP-0017-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "63290e2a-bae1-51c3-9cb9-d49994d804b6"
|
||||
```
|
||||
|
||||
Two files currently claim to be authoritative. State in both that the Go model is
|
||||
the runtime authority and the canonical model is the reviewed contract the
|
||||
conformance check holds it to, so the next reader knows which one to edit first.
|
||||
Update `SCOPE.md` and G02's status in the assessment to say what is now enforced
|
||||
and what remains — this closes the client-registration and discovery drift, not
|
||||
schema enforcement in general (G06 is a separate gap).
|
||||
|
||||
Both files now state the split: the Go model is the runtime authority, the YAML
|
||||
the reviewed contract, and the conformance test the link. SCOPE.md and G02's
|
||||
status say what is enforced and that G06 remains open.
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
id: KEY-WP-0018
|
||||
type: workplan
|
||||
title: "Make the LLDAP export report its own completeness"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: export-completeness-evidence
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "82ff99a1-65cc-5e1f-93fc-5bc8911ce77a"
|
||||
---
|
||||
|
||||
Closes gap G05 of `history/2026-09-05-011726-scope-intent-assessment.md`.
|
||||
|
||||
`src/internal/migration/lldapexport/exporter.go` discovers groups by walking each
|
||||
user's memberships, so a group nobody belongs to is absent from the snapshot and
|
||||
nothing says so. A `LookupGroups` failure is skipped by a `continue` under a
|
||||
comment claiming it is recorded in the incompatibility report — it is not. The
|
||||
run then emits `result: "success"` and the CLI reports a clean export. Group
|
||||
order comes from map iteration, so two exports of an unchanged directory differ.
|
||||
|
||||
A second defect falls out of reading the adapter: `LDAPAdapter.LookupGroups`
|
||||
never populates `Group.Members`, and the exporter builds every membership from
|
||||
that field. Against a real LLDAP directory the `memberships` block is therefore
|
||||
always empty, while the fixture-based tests pass because the mock fills it in.
|
||||
|
||||
The unit of work is the evidence, not the enumeration: an export that cannot
|
||||
prove it saw the whole directory must say which one it is.
|
||||
|
||||
## Enumerate groups independently of membership
|
||||
|
||||
```task
|
||||
id: KEY-WP-0018-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "abb11f82-06e9-58ee-8be2-6d20491e9524"
|
||||
```
|
||||
|
||||
Add an optional `domain.GroupLister` capability (`ListGroups`) and implement it
|
||||
on `LDAPAdapter` with a direct group-subtree search that reads `member` and
|
||||
`uniqueMember`, so `Group.Members` is populated from the directory rather than
|
||||
left empty. Keep it optional rather than widening `UserRepository`: the OIDC
|
||||
layer has no use for it, and every existing implementation and test double would
|
||||
otherwise have to grow a method it never calls.
|
||||
|
||||
Added `domain.GroupLister` and `LDAPAdapter.ListGroups`, searching the group
|
||||
subtree for `groupOfNames`/`groupOfUniqueNames` and reading `member` with
|
||||
`uniqueMember` as the fallback attribute. Compile-time assertions on the adapter
|
||||
now state both contracts it satisfies. The adapter tests assert the filter is
|
||||
*not* a membership filter and that a member-less group survives the mapping,
|
||||
which is the property the whole task exists for.
|
||||
|
||||
## Report or fail on incomplete reads
|
||||
|
||||
```task
|
||||
id: KEY-WP-0018-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d08f62dd-42e8-5c22-ba50-15b53f7473fd"
|
||||
```
|
||||
|
||||
Record on the result which enumeration actually ran — a complete directory
|
||||
enumeration or the membership-derived fallback — and treat a failed group
|
||||
enumeration as fatal rather than as a silent gap. In the fallback path, record
|
||||
each skipped user in the incompatibility report instead of discarding the error,
|
||||
and derive memberships from the discovered user/group pairs. Emit `partial`
|
||||
telemetry, and have the CLI say which enumeration produced the snapshot, so a
|
||||
degraded export is legible without reading the YAML.
|
||||
|
||||
`ExportResult` gained `groupEnumeration` — written unconditionally, so nobody has
|
||||
to infer completeness from an absent field — and a `Complete()` predicate that
|
||||
requires both a directory enumeration and an empty report. The fallback path
|
||||
records its own incompleteness as a report entry before it starts, so the
|
||||
snapshot says so even when every user lookup succeeds.
|
||||
|
||||
The fallback also had to stop deriving memberships from `Group.Members`: the
|
||||
LLDAP adapter never populated that field, so against a real directory the
|
||||
`memberships` block was always empty while the fixture-backed tests passed. It
|
||||
now derives each membership from the user/group pair actually observed.
|
||||
|
||||
## Define ordering and test the failure modes
|
||||
|
||||
```task
|
||||
id: KEY-WP-0018-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "3cd47458-9469-5cd9-a0b1-f2894402e1f8"
|
||||
```
|
||||
|
||||
Sort users, groups and memberships on stable keys so an unchanged directory
|
||||
exports byte-identically. Cover the cases the assessment names and the current
|
||||
suite does not: an empty group, a group enumeration failure, a per-user lookup
|
||||
failure in the fallback path, and repeat-run determinism.
|
||||
|
||||
Sorted users and groups by ID and memberships by group then user. Four exporter
|
||||
tests and two adapter tests cover the named cases; the determinism test runs the
|
||||
export three times over deliberately unsorted input and compares the full
|
||||
ordering, rather than asserting on a single sorted field.
|
||||
|
||||
Recorded in `SCOPE.md` and in G05's status what this does and does not establish:
|
||||
completeness evidence for the user/group/membership surface, not for credential
|
||||
migration, which stays under G03.
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
id: KEY-WP-0019
|
||||
type: workplan
|
||||
title: "Verify upstream Authelia ID tokens"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: upstream-provider-token-verification
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "87ea18ea-dc60-52ce-baf4-476b46aa06f9"
|
||||
---
|
||||
|
||||
Closes the remaining half of gap G01 in
|
||||
`history/2026-09-05-011726-scope-intent-assessment.md`. The Authelia adapter
|
||||
decodes upstream ID-token claims without verifying the signature, issuer,
|
||||
audience or expiry, justified in a comment by a server-to-server TLS boundary.
|
||||
|
||||
**Operator decision (2026-09-07, Bernd):** verify the token itself. The
|
||||
KeyCape→Authelia hop is to be HTTPS as defence in depth, but KeyCape will not
|
||||
monitor, check or gate on that — transport enforcement is friction without
|
||||
protection value here, and a check that must be configured correctly to help is
|
||||
itself a failure mode. Verification is chosen precisely because it does not
|
||||
depend on the transport being what we believe it is. No HTTPS validation, opt-in
|
||||
flag or transport telemetry is to be added.
|
||||
|
||||
## Extract a shared RS256/JWKS verifier
|
||||
|
||||
```task
|
||||
id: KEY-WP-0019-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "68c1b099-96b4-5065-a4be-8592438a0ca9"
|
||||
```
|
||||
|
||||
`internal/authclient` already contains a strict RS256 verifier — kid required,
|
||||
no `crit`, RSA-only keys of at least 2048 bits with an odd exponent. The upstream
|
||||
verification needs the same rules. Duplicating security-critical verification is
|
||||
how two copies drift and one silently misses a fix, so extract the reusable half
|
||||
into `internal/jose`: strict JWK-set parsing and signature verification returning
|
||||
claims. Claim policy stays with each caller, whose audience, nonce and issuer
|
||||
rules genuinely differ.
|
||||
|
||||
Added `internal/jose`: strict JWK-set parsing (RSA signing keys only, at least
|
||||
2048 bits, odd exponent 3 or greater, no duplicate key ids) and RS256 signature
|
||||
verification returning claims. All failures collapse to one `ErrVerification`
|
||||
so a caller cannot leak which check failed back to whoever supplied the token.
|
||||
`authclient` is not yet migrated onto it — see T05.
|
||||
|
||||
## Verify the Authelia ID token
|
||||
|
||||
```task
|
||||
id: KEY-WP-0019-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8105030b-c91f-5a40-a221-4a38c8531e1c"
|
||||
```
|
||||
|
||||
Before trusting any claim from the upstream ID token: verify the RS256 signature
|
||||
against Authelia's published keys, require the issuer Authelia advertises, require
|
||||
KeyCape's own client ID in the audience, and reject expired or not-yet-valid
|
||||
tokens. Fail closed — an unavailable or unparseable key set denies the login
|
||||
rather than falling back to unverified claims. Resolve Authelia's metadata and
|
||||
key set from the server-side token base URL so a split-horizon deployment, where
|
||||
the advertised public URL is not reachable from KeyCape, still verifies; allow
|
||||
explicit issuer and JWKS overrides for deployments where that inference is wrong.
|
||||
Refresh the key set once on an unknown key id so provider key rotation does not
|
||||
require a KeyCape restart. Replace the comment claiming the TLS boundary as
|
||||
justification.
|
||||
|
||||
`idTokenVerifier` in the adapter resolves the issuer and JWKS path from the
|
||||
provider's discovery document, rebasing the advertised `jwks_uri` path onto the
|
||||
server-side token base URL so split-horizon deployments resolve, with
|
||||
`issuer`/`jwksUrl` config overrides where that inference is wrong. Key sets are
|
||||
cached and refetched once on an unknown key id. `parseIDTokenClaims` remains for
|
||||
diagnostics but is documented as outside the authentication path.
|
||||
|
||||
## Prove the rejections
|
||||
|
||||
```task
|
||||
id: KEY-WP-0019-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "163f64dd-aa87-5ce3-b3be-d134fa993eb6"
|
||||
```
|
||||
|
||||
Negative tests per condition: forged signature, `alg: none` and other algorithms,
|
||||
unknown key id, wrong issuer, missing and wrong audience, expired token, future
|
||||
`iat`/`nbf`, malformed key set, and an unreachable JWKS endpoint. A positive case
|
||||
must confirm a genuine token still authenticates, and a rotation case that a new
|
||||
key id is picked up after one refresh.
|
||||
|
||||
Thirteen rejection cases in `verify_test.go` plus the algorithm and rotation
|
||||
cases. Confirmed they catch the original defect rather than merely passing: with
|
||||
the unverified parse restored, all fifteen fail. The existing adapter tests now
|
||||
build genuinely signed tokens against a fixture provider, so they exercise the
|
||||
real verification path instead of being routed around it.
|
||||
|
||||
## Reconcile the records
|
||||
|
||||
```task
|
||||
id: KEY-WP-0019-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "8a208092-7e85-5ad9-a8c4-0dddc66aaaee"
|
||||
```
|
||||
|
||||
Update `SCOPE.md` and G01's status in the assessment to state that upstream
|
||||
provider tokens are now verified independently of transport, and record the
|
||||
operator decision and its reasoning so the absence of transport enforcement reads
|
||||
as a choice rather than an oversight. G01 closes with this; complete profile
|
||||
conformance is still not claimed.
|
||||
|
||||
SCOPE.md and G01's status record the verification and the operator decision,
|
||||
including why no transport enforcement exists, so its absence reads as a choice.
|
||||
|
||||
## Consolidate the caller verifier
|
||||
|
||||
```task
|
||||
id: KEY-WP-0019-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0dc909fe-1772-5816-9e11-dbe7664a4e1f"
|
||||
```
|
||||
|
||||
Move `internal/authclient`'s inline verification onto `internal/jose` so one
|
||||
implementation serves both paths. Kept separate from T01/T02 deliberately: the
|
||||
caller verifier is a tested security path, and destabilising it in the same
|
||||
change that introduces upstream verification would confuse the evidence for both.
|
||||
|
||||
`Client.Verify` now fetches the raw key set and delegates parsing and signature
|
||||
checking to `internal/jose`, keeping its own claim policy — audience and nonce
|
||||
bindings are the caller's and cannot live in a shared verifier. Its existing
|
||||
tests pass unchanged, which is the point: the migration preserved behaviour.
|
||||
|
||||
One deliberate strictness increase: a key set containing any malformed RSA
|
||||
signing key is now refused outright, where the previous code would have used a
|
||||
good key alongside a bad one. KeyCape's own `/jwks` publishes a single key, so
|
||||
this affects no current deployment.
|
||||
|
||||
Also added direct tests for `internal/jose` (17 cases). It is now the single
|
||||
verifier behind both paths, so testing it only through its callers would leave
|
||||
its edges — duplicate key ids, `crit`, even exponents, undersized moduli — to be
|
||||
covered by accident.
|
||||
|
||||
Follow-on evidence for the same task: "existing tests pass unchanged" shows the
|
||||
move preserved behaviour, not that the behaviour is checked, so the check was
|
||||
made explicit. Disabling the RSA comparison in `jose.Verify` fails both callers'
|
||||
suites, which establishes the shared verifier is load-bearing on each path.
|
||||
Added two caller-side cases the existing tamper case cannot reach, since it
|
||||
alters signature bytes for a key that is legitimately published: a structurally
|
||||
valid token signed by an unpublished key under a published kid, which fails only
|
||||
if key selection is bound to the key set, and an undersized modulus in the
|
||||
published set, which must deny rather than fall through to the claims.
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
id: KEY-WP-0020
|
||||
type: workplan
|
||||
title: "Make the Keycloak transform preserve or name every policy field"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: migration-contract-preservation
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "803845de-98ad-5a5b-89ec-11bd9e0fafec"
|
||||
---
|
||||
|
||||
Closes the first half of gap G03 of
|
||||
`history/2026-09-05-011726-scope-intent-assessment.md` — semantic preservation.
|
||||
The second half, proof against a running Keycloak, is G04 and stays open.
|
||||
|
||||
`keycape-to-keycloak` calls `Transform`, which passes no clients, so the realm it
|
||||
writes has an empty `clients` array and nothing says the service-identity
|
||||
contract was not migrated. `TransformWithClients` exists but no caller reaches
|
||||
it. Where clients are supplied, `mapClient` always enables the standard flow
|
||||
regardless of grant types, and drops `Audience`, `ServiceSubject`, `Tenant`,
|
||||
`Roles`, `TokenLifetime`, `MFARequired`, `SecretRef` and the handoff URLs.
|
||||
`mapUser` drops the canonical ID, tenant and roles. Realm roles and client scopes
|
||||
are written as empty containers with a comment saying they "can be extended".
|
||||
|
||||
The failure mode is not the missing mapping — it is that a dropped field and an
|
||||
inapplicable one look identical in the output. An operator diffing the realm JSON
|
||||
against the KeyCape config has no way to tell what this tool decided not to carry
|
||||
across. Preserve what Keycloak can express, and name the rest.
|
||||
|
||||
## Give the CLI the client registrations
|
||||
|
||||
```task
|
||||
id: KEY-WP-0020-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6f6bcfd7-16c5-5311-9568-ad9aa9c3b60a"
|
||||
```
|
||||
|
||||
Add a `-clients` flag reading the KeyCape config's client registrations and pass
|
||||
them through `TransformWithClients`. When no client file is given, say so on
|
||||
stderr and in the report rather than emitting an empty `clients` array that reads
|
||||
like a realm with no clients.
|
||||
|
||||
Added `-clients`, reading registrations through a new `config.Registrations()`
|
||||
that converts without resolving secrets — migration tooling needs the contract,
|
||||
never the material, so `ClientSecret` is left empty by construction rather than
|
||||
by remembering not to use it. An unparseable `tokenLifetime` is an error there,
|
||||
since silently dropping a per-client lifetime is the defect class this closes.
|
||||
Run against `config/dev-config.yaml`, all five registrations now reach the realm.
|
||||
|
||||
## Preserve what Keycloak can express
|
||||
|
||||
```task
|
||||
id: KEY-WP-0020-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "88c0126c-fd73-5178-af1f-a018708ff8fb"
|
||||
```
|
||||
|
||||
Derive `standardFlowEnabled` and `serviceAccountsEnabled` from the declared grant
|
||||
types instead of hardcoding the standard flow on. Carry the audience, tenant,
|
||||
service subject, roles and principal type as protocol mappers, per-client
|
||||
lifetime as the Keycloak client attribute that expresses it, and the handoff URLs
|
||||
and secret reference as attributes — the reference, never a secret value. Derive
|
||||
realm roles and client scopes from the users and clients actually present rather
|
||||
than emitting empty containers. Carry the user's canonical ID, tenant and roles.
|
||||
|
||||
Flows now follow the grants: the old mapping set `standardFlowEnabled` on every
|
||||
client, so migrating a `client_credentials` service registration silently gave it
|
||||
the browser flow. Audience, tenant, service subject and roles ride as protocol
|
||||
mappers, since Keycloak has no native concept for the profile's claims and would
|
||||
otherwise issue tokens the profile rejects. Lifetime, handoff URLs and the secret
|
||||
*reference* are attributes; a test marshals the realm and fails if a resolved
|
||||
secret value appears, because a realm import file is not a custody boundary.
|
||||
Realm roles and client scopes are derived and sorted.
|
||||
|
||||
## Name everything not preserved
|
||||
|
||||
```task
|
||||
id: KEY-WP-0020-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5b8127b2-c01c-5d3c-afbf-adbb368941e6"
|
||||
```
|
||||
|
||||
Report each field the transform cannot carry, at the point it is dropped, so the
|
||||
output distinguishes a deliberate omission from a gap: passwords and MFA
|
||||
credentials, MFA policy enforcement, and subject continuity — LLDAP's canonical
|
||||
ID is a DN, and Keycloak will mint its own `sub`, so tokens after a migration
|
||||
will not carry the same subject unless something downstream maps it. Fail the
|
||||
realm-level validation when a supplied client's contract is not fully
|
||||
represented. Test that each unpreserved field appears in the report.
|
||||
|
||||
`UnpreservedReport()` is separate from `ValidationReport()`. Folding them
|
||||
together was the first attempt and it broke an existing test asserting a clean
|
||||
export reports nothing — correctly so: consistency with the snapshot and
|
||||
completeness of the migration are different questions, and one list cannot answer
|
||||
both. Keeping them apart preserves the existing contract and makes the
|
||||
distinction the point rather than a side effect.
|
||||
|
||||
Six tests cover the flows, the mappers and lifetime, the secret-value exclusion,
|
||||
the derived roles and scopes, every named limit, and the separation of the two
|
||||
reports. Mutation-checked: restoring the hardcoded `standardFlowEnabled` fails
|
||||
the flow test. An incomplete transform now emits `partial` telemetry.
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
id: KEY-WP-0021
|
||||
type: workplan
|
||||
title: "Make snapshot attribute validation enforce a real rule"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: snapshot-attribute-validation
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "733ad1a5-c4d8-5c2f-8608-c51bf1d5b17e"
|
||||
---
|
||||
|
||||
Closes gap G06 of `history/2026-09-05-011726-scope-intent-assessment.md`:
|
||||
`checkNoUnknownAttributes` is named for a rule it does not implement. It rejects
|
||||
blank keys, which is not an allow-list, so a snapshot carrying arbitrary or
|
||||
malformed LDAP attribute names passes a check whose name says otherwise. A rule
|
||||
that passes for the wrong reason is worse than an absent one, because a report
|
||||
listing `no_unknown_attributes: passed` is read as evidence.
|
||||
|
||||
One part of the assessment's description needs correcting rather than
|
||||
implementing: it says group membership validation checks nonempty member IDs
|
||||
rather than complete referenced identity existence. `checkValidGroupMemberships`
|
||||
does only check emptiness, but the semantic rule `checkReferencedUsersExist`
|
||||
already resolves every member against the user set and fails on an unknown one.
|
||||
The reference constraint exists; it lives in a different rule than the one the
|
||||
assessment looked at.
|
||||
|
||||
## Enforce real attribute constraints
|
||||
|
||||
```task
|
||||
id: KEY-WP-0021-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "e2921983-853c-501b-8dfc-158751dea093"
|
||||
```
|
||||
|
||||
`ldapAttributes` is defined by the canonical model as raw attributes *not covered
|
||||
by* the model, so an allow-list of permitted names cannot be derived from the
|
||||
schema — the field exists precisely to carry what the schema does not name. Two
|
||||
constraints are derivable and worth enforcing:
|
||||
|
||||
- the attribute name must be a syntactically valid LDAP attribute descriptor
|
||||
(RFC 4512: a letter followed by letters, digits or hyphens, or a numeric OID),
|
||||
since a name that cannot be written to a directory cannot round-trip; and
|
||||
- it must not shadow a canonical field's documented LDAP mapping (`uid`, `cn`,
|
||||
`mail`), because two sources for one value is how an export and a directory
|
||||
come to disagree, and the model already owns those.
|
||||
|
||||
Attribute descriptors are case-insensitive in LDAP, so keys differing only by
|
||||
case are a collision, not two attributes. Keep the blank-key check. Rename the
|
||||
rule to describe what it actually enforces rather than leaving a name that
|
||||
overpromises.
|
||||
|
||||
Implemented as `raw_attributes_well_formed`, replacing `no_unknown_attributes`
|
||||
in both `validator.go` and `spec/ldap-schema.yaml`, with the schema entry stating
|
||||
plainly that this is not an allow-list and why one cannot be derived.
|
||||
|
||||
## Prove each rule with an invalid snapshot
|
||||
|
||||
```task
|
||||
id: KEY-WP-0021-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "de4caf0a-39c6-5148-a986-db18a1308505"
|
||||
```
|
||||
|
||||
The assessment asks for invalid-snapshot cases proving each stated rule. Add one
|
||||
per constraint — malformed descriptor, shadowed canonical mapping, case-only
|
||||
duplicate, blank key — plus passing cases for legitimate raw attributes and a
|
||||
valid numeric OID, so the rule is shown to be selective rather than merely
|
||||
strict.
|
||||
|
||||
Eleven malformed-descriptor cases, six canonical-mapping cases across casings, a
|
||||
case-only duplicate, a message-content case, and five accepted-attribute cases.
|
||||
Confirmed they test the rule rather than merely passing: making the check return
|
||||
early fails four of the five tests.
|
||||
|
||||
## Reconcile the records
|
||||
|
||||
```task
|
||||
id: KEY-WP-0021-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "8a48c6cc-1e7e-5fd4-abfa-2b893c3d400d"
|
||||
```
|
||||
|
||||
Update `SCOPE.md` and G06's status. State what the rule now enforces and, just as
|
||||
importantly, what it still does not: this is not schema-derived validation of
|
||||
every attribute against a directory schema, and the canonical model cannot
|
||||
provide one for a field defined as everything the model does not cover. Correct
|
||||
the assessment's reference-constraint premise rather than restating it.
|
||||
|
||||
`history/…-scope-intent-assessment.md` was being edited by a concurrent session
|
||||
when this workplan was written; make this edit only against a clean tree.
|
||||
|
||||
Done against a clean tree after the other session committed. G06's status records
|
||||
both corrections: no allow-list is derivable for a field defined as what the
|
||||
model does not cover, and the reference constraint was already implemented in
|
||||
`checkReferencedUsersExist` rather than missing.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
id: KEY-WP-0022
|
||||
type: workplan
|
||||
title: "Make the replacement harness runnable and target conformance externally"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: replacement-harness-and-external-conformance
|
||||
created: "2026-09-07"
|
||||
updated: "2026-09-07"
|
||||
state_hub_workstream_id: "94d74b17-1b2f-57dd-b6c3-50f10a7dccd9"
|
||||
---
|
||||
|
||||
Closes the runnable half of gap G04 of
|
||||
`history/2026-09-05-011726-scope-intent-assessment.md`. The Scenario B and C
|
||||
scripts cannot execute: they reference compose files that do not exist, call
|
||||
binaries at `src/bin/` where the Makefile builds to root `bin/`, pass `--base-dn`
|
||||
to `lldap-to-ldap` whose flag is `--basedn`, and invoke a hardcoded workstation Go
|
||||
path from outside the Go module.
|
||||
|
||||
The deeper problem is that repairing the shell would not prove anything. Both
|
||||
scripts set `KEYCAPE_TEST_ISSUER`, but the profile suite builds its own `httptest`
|
||||
server and never reads it, so the tests pass identically whether or not the
|
||||
external provider is running. A harness that cannot fail for the reason it exists
|
||||
is worse than a missing one.
|
||||
|
||||
## Repair the harness prerequisites
|
||||
|
||||
```task
|
||||
id: KEY-WP-0022-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1d552328-e75b-57fd-83bc-e469ae46b397"
|
||||
```
|
||||
|
||||
Fix the binary paths, the `--basedn` flag, and the Go invocation so the scripts
|
||||
run from a clean checkout with `make build`: use the module directory rather than
|
||||
a workstation-specific Go path, and fail with a clear message when a prerequisite
|
||||
binary or a required environment value is absent instead of part-way through.
|
||||
|
||||
Scenario B rewritten: root `bin/`, `go -C src` so the module root is right,
|
||||
prerequisite checks before anything starts, and a cleanup trap so a failure does
|
||||
not leave a stack running. Scenario C repaired in place — `bin/` paths, `--basedn`
|
||||
for `lldap-to-ldap` (`lldap-export` correctly takes `--base-dn`; the two flags
|
||||
genuinely differ), and the same module-relative test invocation.
|
||||
|
||||
## Add the missing stacks
|
||||
|
||||
```task
|
||||
id: KEY-WP-0022-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "06f34e8b-f3d9-53ed-95c5-02e5063774a0"
|
||||
```
|
||||
|
||||
Provide `docker-compose.scenario-b.yml` (Keycloak over the migrated realm) and
|
||||
`docker-compose.scenario-c.yml` (OpenLDAP seeded from generated LDIF, plus
|
||||
Keycloak). Validate them with `docker compose config` rather than assuming they
|
||||
parse. Whether a full scenario passes end to end is a separate claim from whether
|
||||
the stack definition is valid, and only the second is established here.
|
||||
|
||||
Both files added and validated with `docker compose config`. Scenario B was also
|
||||
started for real; Scenario C's stack has not been run.
|
||||
|
||||
## Target conformance at an external issuer
|
||||
|
||||
```task
|
||||
id: KEY-WP-0022-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d837730d-e9d0-5adb-9dda-ffc47661fca3"
|
||||
```
|
||||
|
||||
Add a suite that reads `KEYCAPE_TEST_ISSUER` and exercises that issuer over HTTP:
|
||||
discovery metadata, the JWKS parsing under the same strict rules the runtime
|
||||
applies, and — when client credentials are supplied — a real token exchange whose
|
||||
claims are verified against the published keys. It must skip cleanly when the
|
||||
variable is unset so ordinary `make test` is unaffected, and it must fail when
|
||||
pointed at an issuer that does not meet the profile, which is the property the
|
||||
current harness lacks.
|
||||
|
||||
`src/tests/conformance` checks that the issuer advertises itself, the profile
|
||||
authorization surface (code, authorization_code, S256, RS256), that published
|
||||
keys parse under `internal/jose` — the same rules the runtime applies, so a
|
||||
provider publishing keys KeyCape would refuse fails here rather than at first
|
||||
login — that excluded grants are absent, and, with credentials, that an issued
|
||||
token verifies against the published keys and carries a matching issuer.
|
||||
|
||||
## Prove the suite against a real provider
|
||||
|
||||
```task
|
||||
id: KEY-WP-0022-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "cb4d8af7-76a4-50b8-bee5-eb1c52c2f894"
|
||||
```
|
||||
|
||||
Run the new suite against an actual Keycloak instance, not only against KeyCape,
|
||||
since the point of the scenario is a replacement provider. Record what genuinely
|
||||
passed and what remains unproven. Do not describe a partially exercised scenario
|
||||
as a demonstrated live swap.
|
||||
|
||||
Run against Keycloak 26.0. Discovery, authorization surface and published-key
|
||||
checks passed. A `client_credentials` exchange against a service client created
|
||||
through the admin API returned a token that verified against Keycloak's JWKS via
|
||||
`internal/jose`, with a matching issuer claim — the end-to-end property a relying
|
||||
party depends on, proved against a replacement provider rather than against
|
||||
KeyCape's own handlers.
|
||||
|
||||
The suite also failed, correctly. Stock Keycloak advertises `implicit` and
|
||||
`password` in its discovery document. In Keycloak these are server capabilities,
|
||||
not per-client toggles, so no realm the transformer emits can remove them: a
|
||||
migrated Keycloak presents a wider grant surface than KeyCape. Scenario B
|
||||
therefore reports a real failure today, which is the harness working. KeyCape
|
||||
itself was not targeted in this run — the dev stack needs key material absent
|
||||
from the checkout — so the suite is proved against the replacement provider, not
|
||||
yet against both sides of the swap.
|
||||
|
||||
## Reconcile the records
|
||||
|
||||
```task
|
||||
id: KEY-WP-0022-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "2cb63801-8182-5c71-ab8d-9a1c377f418c"
|
||||
```
|
||||
|
||||
Update `SCOPE.md` and G04's status with what the harness now does and what
|
||||
replacement claims remain unproven — in particular that directory migration, MFA
|
||||
and unchanged relying-party behaviour against a live Keycloak are not established
|
||||
by this work. G04 does not fully close here.
|
||||
|
||||
SCOPE.md and G04's status record both the working harness and the grant-surface
|
||||
finding, which substantiates with evidence what SCOPE previously asserted without
|
||||
it. Scenario C has still never been run end to end.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
id: KEY-WP-0023
|
||||
type: workplan
|
||||
title: "Prove the migration against live directories and fix what that surfaced"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: live-migration-proof
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
state_hub_workstream_id: "9f19bfa0-78d1-5b5c-9b64-e251fa4838d6"
|
||||
---
|
||||
|
||||
Finishes the unproven half of gap G04. KEY-WP-0022 made the harness runnable and
|
||||
targeted conformance at a live issuer; what remained was directory migration
|
||||
against a real directory, both sides of the swap measured, and the explicit
|
||||
limits recorded.
|
||||
|
||||
Running it found four defects that no amount of reading would have. That is the
|
||||
result worth keeping: every one of them passed the full unit suite.
|
||||
|
||||
## Run the export against a live LLDAP
|
||||
|
||||
```task
|
||||
id: KEY-WP-0023-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b7b2ea88-8457-5fae-a500-a2d0f75754de"
|
||||
```
|
||||
|
||||
**Defect: every LLDAP user search pointed at a branch that does not exist.**
|
||||
`Config.userOU()` defaulted to `ou=users`; LLDAP stores users under `ou=people`,
|
||||
and nothing in this repository set `UserOU` to correct it. `LookupUser`,
|
||||
`ListUsers` and `ValidatePassword` all derive their search base from it, so all
|
||||
three silently found nothing against a stock LLDAP — human login included, not
|
||||
just the export.
|
||||
|
||||
Symptom that exposed it: exporting from a live LLDAP returned **zero users while
|
||||
still emitting a membership referencing `uid=admin`**, a snapshot the repository's
|
||||
own validator rejects with `referenced_users_exist`. Default corrected to
|
||||
`ou=people` and pinned by a test; the re-run exports one user and validates
|
||||
clean.
|
||||
|
||||
## Correct the rule I broke in KEY-WP-0021
|
||||
|
||||
```task
|
||||
id: KEY-WP-0023-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2f988626-ed08-52ce-919a-cd45d48e712d"
|
||||
```
|
||||
|
||||
**Defect of my own making.** The LLDAP adapter records `_validation_warning`
|
||||
inside `LDAPAttributes`. The `raw_attributes_well_formed` rule added in
|
||||
KEY-WP-0021 rejects underscores as invalid attribute descriptors, so the
|
||||
exporter's own annotation made its own output fail validation — a false positive
|
||||
shipped one workplan earlier and caught only by validating a real export.
|
||||
|
||||
Keys prefixed with `_` are now recognised as tooling annotations and skipped,
|
||||
documented as such. A test proves the exemption does not weaken the rule: a
|
||||
malformed real attribute alongside an annotation still fails.
|
||||
|
||||
## Prove the LDIF loads into a real OpenLDAP
|
||||
|
||||
```task
|
||||
id: KEY-WP-0023-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "0f503342-0ca8-55ad-b7e4-e2316d4d7bad"
|
||||
```
|
||||
|
||||
**Defect: migrated group memberships were dangling.** `resolveMemberDN` returned
|
||||
a member that already looked like a DN unchanged, so members kept their *source*
|
||||
DNs (`uid=admin,ou=people,…`) while `writeUser` wrote entries to the *target*
|
||||
branch (`ou=users`). Groups named entries the migrated directory does not
|
||||
contain. Only the naming value is carried across now; branch and naming
|
||||
attribute come from the target.
|
||||
|
||||
**Defect: empty groups could not be loaded at all.** `groupOfNames` makes
|
||||
`member` a MUST, so OpenLDAP rejected every memberless group with an object class
|
||||
violation and aborted the load. Since KEY-WP-0018 the export deliberately
|
||||
preserves empty groups, so this was reachable by any real migration. Empty groups
|
||||
now reference a placeholder entry the same LDIF creates — an `organizationalRole`,
|
||||
not a person, so it cannot be mistaken for a migrated user — emitted only when
|
||||
some group needs it.
|
||||
|
||||
**Defect: the compose file could not start.** `bitnami/openldap:2.6` does not
|
||||
exist in the registry. It passed `docker compose config` in KEY-WP-0022, which is
|
||||
exactly the gap between a file that parses and a stack that runs. Pinned to
|
||||
`osixia/openldap:1.5.0`, the image the scenario was actually proved against.
|
||||
|
||||
Proof: LLDAP → export → validate → LDIF → `ldapadd` into OpenLDAP 1.5.0, all
|
||||
seven entries added with no errors, and every `member` value in the loaded
|
||||
directory resolves to an entry that exists.
|
||||
|
||||
## Measure both sides of the swap
|
||||
|
||||
```task
|
||||
id: KEY-WP-0023-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8a7fe95e-d463-5e74-81fa-5fa60a879774"
|
||||
```
|
||||
|
||||
KeyCape was run locally against a generated key and targeted with the
|
||||
conformance suite: all five checks pass, including the excluded-grant check
|
||||
Keycloak fails. The Keycloak side was exercised twice — the stock realm in
|
||||
KEY-WP-0022, and here the *migrated* realm produced from the live export, which
|
||||
imported cleanly and served discovery. The migrated user arrived carrying
|
||||
`keycape.canonicalId`.
|
||||
|
||||
The divergence is now measured rather than asserted: KeyCape passes 5/5, a
|
||||
migrated Keycloak passes 4/5 and fails on advertising the excluded `implicit` and
|
||||
`password` grants.
|
||||
|
||||
## Record what is proved and what is not
|
||||
|
||||
```task
|
||||
id: KEY-WP-0023-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "acef6ceb-6dea-56e1-94bc-e5d589ef3b6f"
|
||||
```
|
||||
|
||||
G04 is substantially closed: stacks are reproducible and were run, conformance is
|
||||
externally targeted and was run against three issuers, and directory migration is
|
||||
proved end to end into a real directory.
|
||||
|
||||
Not closed, and stated as such: no browser login was performed against the
|
||||
migrated realm, so unchanged relying-party behaviour is not established, and MFA
|
||||
was not exercised. Credential and MFA migration is a **scope exclusion** rather
|
||||
than a test gap — SCOPE says it is not supplied, so no harness can demonstrate
|
||||
it. `keycape-to-keycloak` already reports that subject continuity is not
|
||||
preserved: the canonical ID survives as an attribute while Keycloak mints its own
|
||||
`sub`, so relying parties keyed on `sub` will not recognise migrated users.
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
id: KEY-WP-0024
|
||||
type: workplan
|
||||
title: "Wire tenant_roles as explicit opt-in configuration"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: tenant-roles-opt-in-wiring
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
state_hub_workstream_id: "d75f52f2-df0d-5a56-8394-892feeec4b4d"
|
||||
---
|
||||
|
||||
Closes gap G07. The tenant-engine client and `TokenHandler.TenantEngine` were
|
||||
implemented and tested, but `main.go` supplied no client and exposed no
|
||||
configuration, so the stock executable left the field nil and never emitted
|
||||
`tenant_roles`. The capability existed at library level only.
|
||||
|
||||
Wired rather than documented away as library-only: the claim is cheap, the
|
||||
adapter already fails open, and leaving a tested capability unreachable from the
|
||||
binary invites the same gap being rediscovered later.
|
||||
|
||||
## Add opt-in configuration and wire the client
|
||||
|
||||
```task
|
||||
id: KEY-WP-0024-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0e53485b-7e94-5dd8-8e02-1d18f5413a78"
|
||||
```
|
||||
|
||||
Added a `tenantEngine` config block with `baseURL` and an optional `timeout`.
|
||||
An empty `baseURL` disables the claim, which keeps the stock server's behaviour
|
||||
exactly as it was — enabling it is a deliberate act.
|
||||
|
||||
Validation treats a configured source as one that must work: the URL must be
|
||||
http/https with a host, and the timeout must parse and fall in (0, 10s], since it
|
||||
sits on the synchronous token-issuance path. A `timeout` set without a `baseURL`
|
||||
is rejected rather than ignored — it means someone expected the claim to be on
|
||||
and it silently would not have been.
|
||||
|
||||
## Verify it in the built executable
|
||||
|
||||
```task
|
||||
id: KEY-WP-0024-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "c4279ce0-f6f8-5612-a48a-c1d2fd3da3b6"
|
||||
```
|
||||
|
||||
G07's closure criterion is the built executable, not the wiring, so all three
|
||||
behaviours were checked by running `bin/keycape` and inspecting real tokens
|
||||
rather than by reading the code:
|
||||
|
||||
- configured against a stub tenant-engine, a `client_credentials` token carries
|
||||
`tenant_roles: [capability:approve, capability:observe]`;
|
||||
- with no `tenantEngine` block, the claim is absent;
|
||||
- configured but with the source down, issuance still succeeds and the claim is
|
||||
absent — the fail-open behaviour the adapter documents, confirmed end to end.
|
||||
|
||||
Nine validation cases cover the accepted and rejected configurations.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
id: KEY-WP-0025
|
||||
type: workplan
|
||||
title: "Give the runtime real readiness, graceful shutdown and stated operational limits"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: runtime-lifecycle-and-readiness
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
state_hub_workstream_id: "70f74bbd-6748-5bbe-b261-a310b6f24e0c"
|
||||
---
|
||||
|
||||
Closes gap G08. `/healthz` returned a constant without probing anything, the
|
||||
server called `ListenAndServe` with no signal handling, and the operational
|
||||
limits of in-memory state, startup-loaded keys and local-only logout were spread
|
||||
across code comments rather than stated anywhere an operator would look.
|
||||
|
||||
G08 explicitly does not require shared storage or refresh tokens. The work is
|
||||
therefore to make readiness real, make restarts survivable for in-flight
|
||||
requests, and write the limits down honestly.
|
||||
|
||||
## Separate readiness from liveness
|
||||
|
||||
```task
|
||||
id: KEY-WP-0025-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0e0d3349-efaf-546c-a58d-2a9b736278a9"
|
||||
```
|
||||
|
||||
Added `internal/server/readiness` and a `/readyz` endpoint probing LLDAP,
|
||||
Authelia and privacyIDEA. `/healthz` stays liveness and deliberately probes
|
||||
nothing: wiring liveness to dependency health means an orchestrator restarts
|
||||
KeyCape when a dependency blinks, and since a restart also discards every
|
||||
in-flight login, the reaction is worse than the condition.
|
||||
|
||||
Design decisions worth keeping:
|
||||
|
||||
- **LLDAP is probed with a bind, not a dial.** A rotated or revoked service
|
||||
password leaves the port open and every lookup failing — precisely the state
|
||||
readiness exists to catch, and precisely what a TCP dial would miss.
|
||||
- **The body names the failing check but never the reason.** The endpoint is
|
||||
unauthenticated, and upstream error text carries hostnames and sometimes
|
||||
credentials-in-URLs. Detail goes to the log. A test asserts a probe error
|
||||
containing a credentialed URL does not reach the response.
|
||||
- **Results are cached for 2s.** Not an optimisation: without it, anyone able to
|
||||
reach an unauthenticated endpoint could drive one upstream request per
|
||||
dependency per call.
|
||||
- **Probes run concurrently, each bounded at 3s**, so readiness costs the slowest
|
||||
probe rather than their sum, and a hung dependency makes the endpoint answer
|
||||
rather than hang alongside it.
|
||||
|
||||
## Shut down without dropping requests
|
||||
|
||||
```task
|
||||
id: KEY-WP-0025-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0bea0780-523a-587e-a157-d429df15307b"
|
||||
```
|
||||
|
||||
`SIGTERM`/`SIGINT` now drain in-flight requests for up to 15 seconds. The grace
|
||||
period sits under the 30s read/write timeouts so a stuck request cannot outlive
|
||||
the window an orchestrator allows before `SIGKILL`.
|
||||
|
||||
In-memory login state is deliberately not preserved: a browser mid-login must
|
||||
start again after a restart. That is a property of the single-replica topology,
|
||||
documented rather than papered over.
|
||||
|
||||
## State the operational limits
|
||||
|
||||
```task
|
||||
id: KEY-WP-0025-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "97eba4bb-f8a0-5941-bdc8-6d59b0fa2942"
|
||||
```
|
||||
|
||||
`docs/operations.md` states the supported topology — exactly one replica, because
|
||||
authorization codes, login sessions and handoffs are process-local and no sticky
|
||||
configuration makes a second replica safe — and the consequences: restarts drop
|
||||
in-flight logins while issued tokens keep working, and rolling deploys have a
|
||||
window where new logins fail.
|
||||
|
||||
It also records three limits that are easy to get wrong in operation:
|
||||
|
||||
- the key ID is the constant `key-1`, so rotating the key without changing it can
|
||||
leave consumers caching JWKS by `kid` rejecting freshly issued tokens;
|
||||
- removing a client registration does not revoke its issued tokens, since there
|
||||
is no revocation or introspection endpoint — they stay valid until expiry;
|
||||
- `/logout` clears the local session only, not the Authelia session or any token.
|
||||
|
||||
No throughput or resource figures are given: nothing here benchmarks KeyCape, so
|
||||
any number would be invented.
|
||||
|
||||
## Verify in the running server
|
||||
|
||||
```task
|
||||
id: KEY-WP-0025-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "bc64fdc2-0a31-5dab-afd1-5e7b6d11d2b3"
|
||||
```
|
||||
|
||||
Checked against the built executable rather than the handlers, following the
|
||||
lesson from KEY-WP-0023:
|
||||
|
||||
- with every dependency down, `/readyz` returns 503 naming all three as failed
|
||||
while `/healthz` still returns 200 — the split doing its job;
|
||||
- starting LLDAP flips its check to `ok` while the others stay failed, so the
|
||||
probes are measuring what they claim;
|
||||
- 40 requests issued across a `SIGTERM` all returned 200, and the log shows the
|
||||
signal handled and shutdown completed.
|
||||
|
||||
Seven unit tests cover the reporter, including the disclosure, caching, timeout
|
||||
and concurrency properties above.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
id: KEY-WP-0026
|
||||
type: workplan
|
||||
title: "Reconcile packaging, bootstrap and migration credential handling"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: packaging-bootstrap-and-credential-handling
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
state_hub_workstream_id: "d2b04d59-41b1-531b-8cab-a980328611b4"
|
||||
---
|
||||
|
||||
Closes gap G09. Five separate defects, only loosely related: the dev stack could
|
||||
not start from a clean checkout, the image shipped one binary of five, the
|
||||
publish workflow named a different registry than the one the cluster pulls from,
|
||||
the exporter took a password on argv, and every migration artifact was written
|
||||
world-readable.
|
||||
|
||||
## Take the bind password off argv
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "510a4603-337f-5ceb-b6b7-b9126d8605f0"
|
||||
```
|
||||
|
||||
`lldap-export` took `--bind-pw`, so the service account password was visible to
|
||||
any local user through `ps`, and captured by shell history and process
|
||||
accounting. It now prefers `KEYCAPE_LLDAP_BIND_PW`, accepts `--bind-pw-file`, and
|
||||
keeps `--bind-pw` working with a warning — deprecated rather than removed,
|
||||
because existing runbooks use it and silently breaking them would be worse than
|
||||
the exposure for one more cycle.
|
||||
|
||||
Conflicting sources are rejected rather than silently ranked: with two supplied,
|
||||
an operator cannot tell which bind was attempted. A password file has its
|
||||
trailing newline trimmed, since editors and heredocs add one and binding with it
|
||||
fails confusingly. Both migration scripts now pass the password by environment.
|
||||
|
||||
## Stop writing migration artifacts world-readable
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "679e21a1-e37b-574b-bc25-b9fd46bdfd16"
|
||||
```
|
||||
|
||||
The canonical export, the generated LDIF and the Keycloak realm were all written
|
||||
`0644`. None carries credential material, but the snapshot is a directory dump —
|
||||
every username, display name, email and group membership in the estate — and it
|
||||
tended to land in `/tmp`. All three are `0600` now.
|
||||
|
||||
## Ship every binary in the image
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5252a13c-cd5d-5686-a7f5-04587d7d9b1d"
|
||||
```
|
||||
|
||||
The image packaged `keycape` alone, so the validator and the three migration
|
||||
binaries needed a Go toolchain on the host — which defeats shipping an image for
|
||||
the cutover work they exist to support. All five are built and copied; the issuer
|
||||
remains the entrypoint and the rest are reachable by overriding it. Verified by
|
||||
building the image and running each binary inside it.
|
||||
|
||||
## Reconcile the publish target
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "45b8c386-41bd-514c-9247-5eba9663d8fb"
|
||||
```
|
||||
|
||||
The workflow published to `92.205.130.254:32166/coulomb/key-cape` while the
|
||||
cluster runs `forgejo.coulomb.social/coulomb/key-cape` (KEY-WP-0013). Whether
|
||||
those are one registry behind two names or two registries is not determinable
|
||||
from this repository, which is exactly the problem: a push could not be assumed
|
||||
to have deployed the current source. Now defaults to the recorded Forgejo name
|
||||
and stays overridable through a repository variable.
|
||||
|
||||
**Unverified, and needs an operator's eye:** this repository cannot test that the
|
||||
runner resolves that hostname or that `REGISTRY_USER`/`REGISTRY_TOKEN` are valid
|
||||
for it. If the next publish fails, set the `REGISTRY` repository variable back to
|
||||
the address.
|
||||
|
||||
## Make the dev stack bootstrappable
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "a750ac2f-cad2-5e7f-a505-2ca6b94f1a5a"
|
||||
```
|
||||
|
||||
`docker-compose.dev.yml` mounts `config/dev-key.pem` and `config/authelia/`,
|
||||
neither in the checkout — correctly, since one is a private key and the other
|
||||
carries a password hash. `scripts/bootstrap-dev.sh` generates both locally: an
|
||||
RSA key created under `umask 077` rather than chmodded afterwards, so it is never
|
||||
briefly world-readable, plus an Authelia configuration, its OIDC signing key and
|
||||
a user fixture. Everything it writes is git-ignored, and re-running it leaves
|
||||
existing material alone.
|
||||
|
||||
The fixture password is published in the script deliberately: a reader can see
|
||||
exactly what it unlocks, which is nothing deployed.
|
||||
|
||||
## Reconcile the records
|
||||
|
||||
```task
|
||||
id: KEY-WP-0026-T06
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "cc7c86ae-9c78-5422-8f3c-19441ceeb513"
|
||||
```
|
||||
|
||||
`SCOPE.md` and G09's status record what is fixed and what is not: the registry
|
||||
change is unverified from here, and a documented external bootstrap remains the
|
||||
route for anything beyond development.
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
id: KEY-WP-0027
|
||||
type: workplan
|
||||
title: "Establish the live state and the rollout gap for G10"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: rollout-readiness-and-live-state
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
state_hub_workstream_id: "2ab60558-ee14-542c-9f19-bb487de8e668"
|
||||
---
|
||||
|
||||
G10 cannot be closed from this repository: it waits on named custody and platform
|
||||
owners. What was doable was to stop guessing about the live state, verify the
|
||||
handoffs actually went out, and find out whether main is safe to roll out. The
|
||||
last question turned up a defect in this repository's own recent work.
|
||||
|
||||
## Verify the handoff receipts independently
|
||||
|
||||
```task
|
||||
id: KEY-WP-0027-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "00de2bad-04da-5b58-8cde-aaa144272541"
|
||||
```
|
||||
|
||||
KEY-WP-0009 was reopened once because delivery was claimed without receipts, so
|
||||
the claim that it has now been sent deserved the same scrutiny rather than being
|
||||
taken on trust. `GET /messages/?from_agent=key-cape` shows all seven present and
|
||||
dated 2026-09-08T08:21Z: the four KEY-WP-0009 handoffs (netkingdom,
|
||||
secrets-engine, ops-warden, railiance-platform) and the three admission requests
|
||||
(railiance-platform custody, approval-engine human callback, ops-warden token
|
||||
split). No replies had arrived at the time of writing, which is expected hours
|
||||
after sending and is not itself a blocker to record.
|
||||
|
||||
## Record the live state as observed, not as remembered
|
||||
|
||||
```task
|
||||
id: KEY-WP-0027-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "2d513229-1dbd-500e-9100-c0173be7b4de"
|
||||
```
|
||||
|
||||
Read-only cluster inspection, `sso` namespace, 2026-09-08:
|
||||
|
||||
- image `forgejo.coulomb.social/coulomb/key-cape:main-153258b`, predating every
|
||||
change from KEY-WP-0016 onward;
|
||||
- environment carries `KEYCAPE_RAPP_QONTO_CLIENT_SECRET` only, so the two
|
||||
approval clients are still not materialized — KEY-WP-0013-T02 remains correctly
|
||||
blocked;
|
||||
- four registered clients: `demo-app`, `netkingdom-bootstrap-console` and
|
||||
`openbao-admin` (public, authorization_code) and `rapp-qonto-client`
|
||||
(confidential, client_credentials);
|
||||
- no `tenantEngine` block, so KEY-WP-0024 is inert on rollout.
|
||||
|
||||
**Correction to an earlier claim of mine.** I reported the `ou=people` fix
|
||||
(KEY-WP-0023) as a production-relevant correction to human login. The deployed
|
||||
config sets `userOU: "ou=people"` explicitly, so production was never affected.
|
||||
The fix matters for anyone relying on the default — the dev stack, the scenario
|
||||
scripts, a fresh deployment — not for this one.
|
||||
|
||||
## Find out whether main is safe to roll out
|
||||
|
||||
```task
|
||||
id: KEY-WP-0027-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "23c8d04b-c2b7-5347-80a0-6388c1c41c53"
|
||||
```
|
||||
|
||||
**Defect found in KEY-WP-0019, before it shipped.** Upstream verification
|
||||
discovers the expected issuer server-side from `authelia.tokenBaseURL`. The
|
||||
deployed Authelia derives its advertised issuer from the request Host, confirmed
|
||||
directly:
|
||||
|
||||
| Host presented | issuer advertised |
|
||||
| --- | --- |
|
||||
| `authelia.sso.svc.cluster.local:9091` | `http://authelia.sso.svc.cluster.local:9091` |
|
||||
| `auth.coulomb.social` | `http://auth.coulomb.social` |
|
||||
|
||||
KeyCape fetches over the in-cluster address, so it would expect the first value
|
||||
while tokens minted for the browser-facing host carry the second. Verification
|
||||
fails closed, so if they differ **every human login fails after rollout**, and it
|
||||
presents as a broken login rather than a configuration error.
|
||||
|
||||
Which value the ID token actually carries could not be settled from here without
|
||||
performing a real login against production Authelia, which was not done. The risk
|
||||
is therefore recorded as a rollout precondition, not as a confirmed breakage.
|
||||
|
||||
Two mitigations, both in this repository:
|
||||
|
||||
- `docs/operations.md` documents pinning `authelia.issuer` and `authelia.jwksUrl`
|
||||
explicitly, with the one-line `curl` that reveals the value the provider will
|
||||
advertise for a given Host. The overrides already existed; nothing said when
|
||||
they were needed.
|
||||
- The failure is now diagnosable. The authentication failure event carries a
|
||||
specific reason — `id_token_issuer_mismatch`, distinct from
|
||||
`id_token_signature`, `provider_keys_unavailable` and the rest — so an operator
|
||||
can tell a misconfiguration from an attack. Five cases cover the mapping. The
|
||||
browser still learns nothing.
|
||||
|
||||
## Record what each owner owes
|
||||
|
||||
```task
|
||||
id: KEY-WP-0027-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "7ba84b61-0458-5820-8ded-c6f59d820303"
|
||||
```
|
||||
|
||||
G10's status now names the live state, the outstanding requests with their
|
||||
receipt ids, and the issuer precondition that must be checked before main is
|
||||
deployed. The precondition was also sent to railiance-platform as the deployment
|
||||
owner (message c8b1ad10), with the exact `curl` to settle it, the config keys to
|
||||
pin, and the `error_type` to look for if it bites — plus a note that the two
|
||||
approval clients remain unprovisioned, confirmed by inspection rather than
|
||||
asserted. G10 stays open: nothing here admits custody, registers the human
|
||||
callback, or reconciles the OpenBao-token boundary, and no repo-local change can.
|
||||
|
||||
|
||||
2026-09-08 live return: the actual signed-token uncertainty in T03 is now
|
||||
resolved by KEY-WP-0013-T02's admitted probe. The issuer is exactly
|
||||
`https://auth.coulomb.social`; signature/audience/time/nonce checks passed and
|
||||
the Job exited 0. The temporary route, policies, service and Job/Pod were removed.
|
||||
`docs/evidence/2026-09-08-upstream-issuer-live-proof.json` is the source receipt.
|
||||
The configuration owner still ensures the exact pin before main rollout; no
|
||||
normal config or Deployment changed during this proof.
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
id: KEY-WP-0028
|
||||
type: workplan
|
||||
title: "Reject service-identity fields that a browser client silently ignores"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: browser-client-field-validation
|
||||
created: "2026-09-09"
|
||||
updated: "2026-09-09"
|
||||
state_hub_workstream_id: "6d0cb4fd-17da-5466-8011-5ade1ac46171"
|
||||
---
|
||||
|
||||
`serviceSubject` and `roles` are read only on the `client_credentials` path. On a
|
||||
browser client they are accepted at startup and then ignored: the subject and
|
||||
roles come from the directory user. A registration that looks effective and is
|
||||
not fails later, somewhere else, as a downstream rejection rather than as a
|
||||
registration defect.
|
||||
|
||||
`tokenLifetime` was already rejected this way for non-service clients, so the
|
||||
rule exists — it was just incomplete.
|
||||
|
||||
## Reject the ignored fields
|
||||
|
||||
```task
|
||||
id: KEY-WP-0028-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "e4bce8c7-0e60-55e8-ad32-a3a0c4e46044"
|
||||
```
|
||||
|
||||
Config validation now rejects `serviceSubject` and `roles` on any client that
|
||||
does not use `client_credentials`, naming where the value actually comes from.
|
||||
Nothing in `config/dev-config.yaml`, the example fixture or the live deployment
|
||||
sets either field on a browser client, so this breaks no existing configuration —
|
||||
checked against all three rather than assumed.
|
||||
|
||||
Re-verified against `sso/keycape-config` on 2026-09-09, after a peer raised that
|
||||
this validation fails closed at startup and a rollout is imminent, so a stray
|
||||
field would surface as KeyCape failing to boot. The deployed config now holds six
|
||||
clients: `demo-app`, `netkingdom-bootstrap-console` and `openbao-admin` are
|
||||
browser clients carrying neither field, while `rapp-qonto-client`,
|
||||
`secrets-engine-approval` and `approval-engine-operator` carry both legitimately
|
||||
on the `client_credentials` path. The rule rejects none of them. Kept as errors
|
||||
rather than downgraded to warnings on that evidence: a warning for a field that
|
||||
does nothing is the state this change exists to end.
|
||||
|
||||
**`tenant` is deliberately excluded from the rule**, and a test pins that. A
|
||||
browser client may declare a tenant; `humanTenant` resolves it against the
|
||||
directory and refuses issuance when the two disagree (KEY-WP-0013-T05). An
|
||||
earlier version of this change rejected `tenant` too, which would have made that
|
||||
feature unusable.
|
||||
|
||||
## Correction
|
||||
|
||||
```task
|
||||
id: KEY-WP-0028-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5ab951f8-2f9c-522c-b6ea-7f6d572cd618"
|
||||
```
|
||||
|
||||
This work started from KEY-WP-0013-T05's blocker paragraph, which described a
|
||||
human token as unable to carry `tenant:platform`. That was true when written and
|
||||
had already been fixed in a concurrent session by the time this task began; the
|
||||
paragraph was read as current state rather than as a dated record.
|
||||
|
||||
Two consequences, both corrected within the session:
|
||||
|
||||
- the `tenant` rejection was removed before it was committed, and a test now
|
||||
asserts a browser client carrying `tenant: tenant:platform` validates cleanly;
|
||||
- a hub decision (`0145ab57`) recording the tenant question as open and
|
||||
unimplemented was withdrawn with a rationale pointing at the implementation.
|
||||
Nothing was built on it.
|
||||
|
||||
A third instance followed the same day, outside this task: a message to
|
||||
railiance-platform (d361bf28) listed the upstream issuer pin as outstanding when
|
||||
`docs/evidence/2026-09-09-upstream-issuer-pin.json` showed it done ten hours
|
||||
earlier — `check_before.issuer_matches: false`, then pinned to
|
||||
`https://auth.coulomb.social`. Corrected in dd827b9f. The concern itself was
|
||||
sound; only the claim that it was still owed was wrong, and the two are worth
|
||||
separating rather than letting the correct half excuse the other.
|
||||
|
||||
The lesson worth keeping: in a repository where several sessions work at once,
|
||||
prose ages faster than the code and the receipts it describes. `docs/evidence/`
|
||||
is the authoritative artifact for anything claiming a live proof here. Read it
|
||||
before writing "outstanding", "unproven" or "not done" — that check is cheaper
|
||||
than any of the corrections it would have prevented.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
id: KEY-WP-0029
|
||||
type: workplan
|
||||
title: "Reassess SCOPE against the current revision"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: scope-reassessment
|
||||
created: "2026-09-09"
|
||||
updated: "2026-09-09"
|
||||
state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78"
|
||||
state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78"
|
||||
---
|
||||
|
||||
SCOPE.md declared itself reviewed against `b989de4`, 63 commits behind. Its
|
||||
limits section had been kept current by appending to bullets, which works until
|
||||
the header asserts a review date that no longer describes what was reviewed.
|
||||
|
||||
## Recheck every claim against source
|
||||
|
||||
```task
|
||||
id: KEY-WP-0029-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "b12dfe7d-f4b6-5c44-89c6-9d3c02435d4c"
|
||||
```
|
||||
|
||||
Routes read from the mux registrations, subcommands from the dispatch in
|
||||
`main.go` and `internal/authclient/cli.go`, flags from the flag sets, claim
|
||||
behaviour from `token.go` and `userinfo.go`. Where the existing prose and the
|
||||
code disagreed, the code won.
|
||||
|
||||
Three claims were false and are corrected: caller commands omitted
|
||||
`verify-client`; the container was described as packaging only `keycape`, untrue
|
||||
since KEY-WP-0026; and the issuer API row omitted `/readyz`. Capabilities that
|
||||
had only reached SCOPE piecemeal — the authorization-code bindings, upstream
|
||||
token verification, readiness and drain, the human tenant rules,
|
||||
authentication-time assurance, the shared verifier — are now stated in the
|
||||
capability table rather than only in the limits prose.
|
||||
|
||||
## Separate source behaviour from deployed behaviour
|
||||
|
||||
```task
|
||||
id: KEY-WP-0029-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "28ae58c0-9bb9-573f-9cf3-51230d053e3c"
|
||||
```
|
||||
|
||||
The document now says plainly that it describes source, and that the two are
|
||||
different claims with different evidence: the test suite and harness runs for
|
||||
one, committed receipts in `docs/evidence/` for the other.
|
||||
|
||||
What is proven live is narrower and is stated exactly — the issuer pin, whose
|
||||
pre-pin state did *not* match, so that risk was real rather than hypothetical;
|
||||
and the two approval clients verified in an attended owner process. Both receipts
|
||||
decline real predecessor rotation and observed wall-clock expiry, and SCOPE
|
||||
declines them too. The deployed image predates this revision, so several
|
||||
behaviours SCOPE describes are running nowhere yet, two of them fail-closed at
|
||||
startup.
|
||||
|
||||
This is the correction that matters. Four times in two days a statement about
|
||||
current state was written from a workplan paragraph, a partial `git status`, or
|
||||
an inference about what was admitted, without reading the authoritative artifact.
|
||||
A SCOPE that does not distinguish "the code does this" from "this is running"
|
||||
invites exactly that error from the next reader.
|
||||
|
||||
## Record the reassessment
|
||||
|
||||
```task
|
||||
id: KEY-WP-0029-T03
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "7ef92d4f-eb56-5208-8b85-ed7bb2656c46"
|
||||
```
|
||||
|
||||
`history/2026-09-09-scope-reassessment.md` records the method, what changed since
|
||||
2026-09-05, the corrections, and what remains. It supersedes rather than replaces
|
||||
the 2026-09-05 assessment, which stays as the record of the ten-gap backlog and
|
||||
its closures; both are linked from SCOPE.
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
id: KEY-WP-0030
|
||||
type: workplan
|
||||
title: "Make the static-registration precondition a checked condition"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: tenant-precondition-guard
|
||||
created: "2026-09-09"
|
||||
updated: "2026-09-09"
|
||||
state_hub_workstream_id: "9ed8f851-080a-5d15-8642-f799c2e3295c"
|
||||
---
|
||||
|
||||
`informed-decision` replied on KEY-WP-0013-T05 and asked for one thing that is
|
||||
ours to do: the caveat under which a registration-bound human tenant is safe
|
||||
should be "a condition of the capability, so a future change to registration
|
||||
policy has to confront it", not reasoning left in a message.
|
||||
|
||||
They are right that it was only prose. `docs/tenant-claim-contract.md` states it,
|
||||
and a separate test asserted `registration_endpoint` is absent — but nothing
|
||||
connected the two, so a future session adding dynamic registration would see a
|
||||
test about discovery metadata, not a warning about relabelling users.
|
||||
|
||||
## Tie the exclusion to the capability it protects
|
||||
|
||||
```task
|
||||
id: KEY-WP-0030-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4729d591-39d4-5f33-bef8-db8057090913"
|
||||
```
|
||||
|
||||
`TestRegistrationBoundTenantRequiresStaticRegistration` asserts both halves
|
||||
together: that a client-declared tenant is issued, and that dynamic client
|
||||
registration is not advertised. Whichever is removed first, the failure points at
|
||||
the other.
|
||||
|
||||
The failure message carries the reasoning rather than the observation. Adding
|
||||
dynamic registration while a registration may declare its users' tenant means
|
||||
anyone able to register a client can relabel the users who log in through it into
|
||||
a zone of their choosing; the message says that and names the two ways out, drop
|
||||
the capability or gate it to statically configured registrations.
|
||||
|
||||
Verified in both directions rather than assumed: advertising a
|
||||
`registration_endpoint` fails it with the escalation message, and neutering
|
||||
`humanTenant` fails it with the message saying to remove the guard along with the
|
||||
capability it protects.
|
||||
|
||||
This is deliberately not a vote on the tenant question. It makes the precondition
|
||||
of one option checkable; it does not choose between them, and if option 1 lands
|
||||
the capability and this guard are removed together.
|
||||
|
||||
## What stays with the owners
|
||||
|
||||
```task
|
||||
id: KEY-WP-0030-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "32b10327-f55d-5a0e-a4bc-be92929469dc"
|
||||
```
|
||||
|
||||
`informed-decision` prefers option 2 and explicitly declines to treat that as the
|
||||
answer, flagging it to gate-house on GH-DEC-2026-012 and naming approval-engine's
|
||||
stake. KeyCape leans the same way, which is a reason for more care rather than
|
||||
less: the code already implements option 2 (`329e48f`), so the doctrine question
|
||||
is being decided against a working implementation. That is worth saying out loud
|
||||
to everyone waiting on it, and was.
|
||||
|
||||
Their scope set `[openid, approval:read, approval:approve]` is confirmed and
|
||||
already published. `client_id` and callback follow at their T07, deliberately not
|
||||
before the tenant question resolves — registering a client that fails closed at
|
||||
first use is the failure this exchange existed to avoid.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
id: KEY-WP-0031
|
||||
type: workplan
|
||||
title: "Implement the gate-house tenant ruling"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: gate-house-tenant-ruling
|
||||
created: "2026-09-10"
|
||||
updated: "2026-09-10"
|
||||
state_hub_workstream_id: "2bc4d085-9b10-5160-bc8a-4fccc5d41ba9"
|
||||
---
|
||||
|
||||
gate-house ruled GH-DEC-2026-013 (`gate-house@16c1d46`) on the human-tenant
|
||||
question KeyCape declined to settle for itself. Directory-sourced is the terminal
|
||||
state; the registration-bound shape is granted as a declared bounded gap under
|
||||
three conditions, on the general property that *a transitional shape is
|
||||
admissible where it fails closed on exactly the case distinguishing it from the
|
||||
correct resolution*. Ours refuses issuance where registration and directory
|
||||
disagree, which is that case.
|
||||
|
||||
Conditions (a) and (c) needed nothing from this repository — (a) is already the
|
||||
implemented behaviour, and its strengthening, that a future change preferring the
|
||||
**directory** is equally void because picking any winner is a silent
|
||||
cross-tenant assertion, is recorded in the contract. This workplan covers the two
|
||||
items that were implementation.
|
||||
|
||||
## Carry the tenant claim's provenance
|
||||
|
||||
```task
|
||||
id: KEY-WP-0031-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "46d42120-eca5-5b7f-8445-2211b3b8eab0"
|
||||
```
|
||||
|
||||
gate-house ruled on GH-DEC-2026-013 while this was in flight. Section 5 is a
|
||||
finding nobody asked for and is ours to implement: `tenant` is a bare string, so
|
||||
a consumer cannot tell a zone the directory asserted about the person from one a
|
||||
registration supplied about the client they came through. approval-engine
|
||||
exact-matches that string while its contract reads as though it relies on the
|
||||
first — the check is sound and the property a reader infers from it is absent.
|
||||
|
||||
Every token now carries `tenant_source` beside `tenant`: `directory`,
|
||||
`registration`, or `default`. It is advertised in `claims_supported` and asserted
|
||||
on both grants at the token level, not only in the resolution function, because
|
||||
the claim a consumer reads is the thing under obligation.
|
||||
|
||||
Three values where the ruling names two, and that is the substantive judgement
|
||||
here. Labelling an unasserted profile default as `directory` would reproduce the
|
||||
same defect one level down — a consumer reading an assertion the identity layer
|
||||
never made. The ruling cites GH-DEC-2026-011 §3 on unknown versus absent for the
|
||||
case it examined; the same rule applies to our own fallback, so the fallback is
|
||||
named rather than folded into the strongest neighbouring value.
|
||||
|
||||
The agreement case resolves to `directory` deliberately: if a registration
|
||||
declares the zone the directory also assigned, the directory did assert it, and
|
||||
reporting the weaker source would understate what is known.
|
||||
|
||||
## Condition (b) corrected a message I had just shipped
|
||||
|
||||
```task
|
||||
id: KEY-WP-0031-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b21aeac8-8370-5572-ab00-6e9812fa48e7"
|
||||
```
|
||||
|
||||
The guard committed in `5f516a0` told a future reader that adding dynamic
|
||||
registration could be resolved by dropping the capability **or gating it to
|
||||
statically configured registrations**. Condition (b) voids the second: admitting
|
||||
dynamic registration voids the registration-bound shape that day, and the
|
||||
directory becomes the only source whatever state the adapter is in. gate-house
|
||||
strengthened our own "must be revisited" on the grounds that *revisited* implies
|
||||
the answer might survive review, and it would not.
|
||||
|
||||
So the message offered a way out the ruling forbids, in the exact place someone
|
||||
would read it while making that change. Corrected the same day. Worth recording
|
||||
rather than quietly editing: the guard was written to force a confrontation, and
|
||||
a guard that suggests an inadmissible resolution is worse than none.
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
id: KEY-WP-0032
|
||||
type: workplan
|
||||
title: "Pin the single route by which a principal becomes human"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: principal-type-provenance-guard
|
||||
created: "2026-09-10"
|
||||
updated: "2026-09-10"
|
||||
state_hub_workstream_id: "150b892c-f112-5fb9-b0f0-8072a52b85d5"
|
||||
---
|
||||
|
||||
GH-DEC-2026-016 §5 applies A-16 to what makes a principal *human*: if `human` is
|
||||
reachable by two routes — asserted by the identity layer about the person, or
|
||||
supplied by a registration about the client they came through — the record must
|
||||
say which, and a human-in-the-loop control must not be discharged on the
|
||||
registration-supplied one. Refusing a service principal while accepting an
|
||||
unverified assertion of humanity moves the defect rather than closing it.
|
||||
|
||||
gate-house said explicitly this was not a request. It is built because the
|
||||
property it protects is true *today* and is the kind a plausible change erases
|
||||
quietly.
|
||||
|
||||
## Pin the property, not the behaviour
|
||||
|
||||
```task
|
||||
id: KEY-WP-0032-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "2f42a72e-6292-5c02-9223-835db17b0a63"
|
||||
```
|
||||
|
||||
`principal_type` has exactly one route to `human`: a string literal on the
|
||||
authorization-code path, reached only after an upstream login resolved to a
|
||||
directory user. No configuration field can assert it — verified against source
|
||||
rather than assumed, and it is why the tenant provenance work has no counterpart
|
||||
here and why A-16 does not yet bite.
|
||||
|
||||
Asserting the *behaviour* would not have protected that. A test checking "a human
|
||||
token says human" passes just as happily when the value starts coming from a
|
||||
registration. So the guard parses the package and requires every assignment to
|
||||
`principal_type` to be a string literal, with the literal set exactly
|
||||
`{human, service}` — one route each. `claims["principal_type"] =
|
||||
client.PrincipalType` would look like a feature in review and fails the build
|
||||
instead, with a message saying that if it is deliberate the claim must carry its
|
||||
provenance the way `tenant_source` does.
|
||||
|
||||
Both assignment shapes are covered. The browser path assigns into a map; the
|
||||
service path uses a key-value pair inside a map literal. Checking only
|
||||
assignments found one of two routes and passed — a guard proving less than it
|
||||
claimed, caught because the expected literal set was pinned rather than merely
|
||||
counted.
|
||||
|
||||
Verified by mutation: replacing the literal with a function of the client
|
||||
registration fails it at the exact line, and restoring it passes.
|
||||
|
||||
## What this deliberately does not do
|
||||
|
||||
```task
|
||||
id: KEY-WP-0032-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "13199970-75d1-5c38-aa8c-27d6e38dcf65"
|
||||
```
|
||||
|
||||
No `principal_type_source` claim was added. There is one route, so a provenance
|
||||
marker would encode a distinction that does not exist and invite consumers to
|
||||
branch on it — the opposite of the tenant case, where two routes existed and the
|
||||
claim could not say which had been taken.
|
||||
|
||||
The guard is the honest response to "not a request today": it makes the absence
|
||||
of a second route a checked fact rather than a remembered one, and hands the
|
||||
decision to whoever creates the second route, at the moment they create it.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
id: KEY-WP-0033
|
||||
type: workplan
|
||||
title: "Preserve fresh-user authentication for the Vergabe company handoff"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
flavor: implementation
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-09-12"
|
||||
updated: "2026-09-25"
|
||||
related: [VERGABE-WP-0019, NK-WP-0037, USER-WP-0025]
|
||||
state_hub_workstream_id: "144dd430-1a09-51e1-9aad-e799ee86c338"
|
||||
---
|
||||
|
||||
## Forward login freshness to the actual authentication provider
|
||||
|
||||
```task
|
||||
id: KEY-WP-0033-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "fe05486b-f079-503b-ade1-fca65bf64b34"
|
||||
```
|
||||
|
||||
KeyCape parsed prompt=login and max_age but dropped them before Authelia.
|
||||
Forward these through the provider-neutral AuthRequest and the Authelia adapter.
|
||||
Absent values retain ordinary SSO behavior. Handler and adapter regressions
|
||||
cover forced login and zero/nonzero maximum age. Full Go suite passes after
|
||||
correcting the existing example-count regression: the example file already
|
||||
contains four service clients and the admitted human approver client.
|
||||
|
||||
## Publish, preflight and prove the fresh recipient boundary
|
||||
|
||||
```task
|
||||
id: KEY-WP-0033-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7b58f08c-6063-554a-8700-a1edbc805ca4"
|
||||
```
|
||||
|
||||
Publish the exact source and validate current live config before replacing the
|
||||
single issuer instance. The live image is dcebd46/digest 7ff54c54; current main
|
||||
also contains startup validation and tenant provenance changes documented in
|
||||
docs/operations.md. Preserve all unrelated client, credential and MFA policy
|
||||
configuration. NK-WP-0037 adds the exact public Vergabe callback with no tenant
|
||||
assertion or MFA downgrade. Confirm prompt=login reaches Authelia through the
|
||||
live redirect, wrong callbacks and missing PKCE fail, and the actual recipient
|
||||
uses their own identity. Provider restart invalidates pending in-memory logins;
|
||||
USER-WP-0025-T03 still owns full provider sign-out coordination.
|
||||
|
||||
2026-09-12 release prepared: source 8d4336e, Forgejo image run 45 succeeded,
|
||||
digest sha256:5f10f36a5da23ce1aaf3df9b84a8ff98d7926f34ceaa19e63bd3356adb68e01a.
|
||||
The exact client-registration and deployment server dry runs pass. The runtime
|
||||
is unchanged. Await the attended window required by docs/operations.md; the
|
||||
complete packet is railiance-apps/docs/vergabe-demo-company-sso-rollout.md.
|
||||
|
||||
2026-09-12 attended rollout executed after explicit operator approval. KeyCape
|
||||
and password setup are Ready on the prepared digests; exact public client
|
||||
registration was CAS-applied (config resourceVersion 60123977) with unrelated
|
||||
config bytes/Secret data preserved. Existing portal and product client both
|
||||
pass fresh-login forwarding, wrong-callback and missing-PKCE checks (6 checks).
|
||||
Vergabe Helm revision 2 is Ready; identity migration completed, both PVCs remain,
|
||||
and requests remain 60m CPU/256Mi memory. Eleven live product checks pass:
|
||||
company welcome, anonymous gate, no-store, secure scoped CSRF, POST/CSRF-only
|
||||
login start, native issuer redirect, private company/media protection and
|
||||
invalid callback/confirmation rejection. Initial readback showed zero accounts,
|
||||
identity mappings and staff accounts. Native invited-user sign-in/MFA and
|
||||
confirmation are now requested from the operator; no user credential was used
|
||||
by the agent. Recovery and two-user acceptance remain their existing tasks.
|
||||
Evidence: railiance-apps/docs/evidence/2026-09-12-demo-company-sso-live.md.
|
||||
|
||||
2026-09-23 regression from the attended recipient sign-in (net-kingdom message
|
||||
ee4808e2, NK-WP-0037-T02): Authelia 4.38 registers the authorization request
|
||||
only after the login, so forwarding prompt=login fails every real login ("auth_time
|
||||
happened before the authorization request was registered"). The 2026-09-12
|
||||
forwarding checks were redirect-only and never completed a login. Fix (owner
|
||||
option a): KeyCape no longer sends prompt to Authelia. prompt=login and
|
||||
max_age=0 travel as max_age=10, a shorter max_age is raised to 10, a longer one
|
||||
passes through. KeyCape then enforces the exact requirement itself against the
|
||||
verified upstream auth_time: prompt=login needs an authentication no earlier
|
||||
than the KeyCape request, max_age needs one within that age, and a missing
|
||||
auth_time fails closed (event `stale_upstream_authentication`). Known edge: a
|
||||
user whose Authelia login is under 10s old at request time is not re-prompted
|
||||
and is refused rather than admitted. Also fixed the stale example-count test left
|
||||
by 651625c/1620ce2 (6 service + 1 human). Full Go suite passes.
|
||||
|
||||
The image was already the live issuer
|
||||
(`sha256:8dc35801`, Forgejo run 69, source `11ce29a`). No second rollout.
|
||||
2026-09-24 23:46 UTC the founder completed the Vergabe journey: one
|
||||
`token_exchange_error`, then `auth_success` and `token_issued` for
|
||||
`vergabe-demo-company`. No `stale_upstream_authentication`. Receipt:
|
||||
`docs/evidence/2026-09-24-fresh-login-and-account-switch.md`.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
id: KEY-WP-0034
|
||||
type: workplan
|
||||
title: "Browser authentication recovery and confirmed shared sign-out"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: finished
|
||||
flavor: implementation
|
||||
owner: codex
|
||||
topic_slug: key-cape
|
||||
created: "2026-09-12"
|
||||
updated: "2026-09-25"
|
||||
state_hub_workstream_id: "49b0cd1d-fb9d-5351-99eb-9be9143a092d"
|
||||
---
|
||||
|
||||
The operator reports a dead-end authentication error after using an account
|
||||
outside the product tenant. Recent issuer telemetry indicates token exchange
|
||||
failure; tenant rejection and provider failure must not be conflated.
|
||||
|
||||
## Implement and validate recovery
|
||||
|
||||
```task
|
||||
id: KEY-WP-0034-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ba285335-4ead-5478-8fb7-89a2ec1ba695"
|
||||
```
|
||||
|
||||
Route failed browser login to the public account recovery surface without codes,
|
||||
state or unverified identity. Show verified portal identity, tenant memberships,
|
||||
and recorded workload memberships; preserve operator/customer separation.
|
||||
Provide CSRF-protected portal logout and confirmed shared provider sign-out with
|
||||
fixed owner-configured return locations. No automatic reauthentication loops,
|
||||
MFA downgrade, global JWT revocation claim or inferred workload entitlements.
|
||||
|
||||
## Publish and verify the recovery flow
|
||||
|
||||
```task
|
||||
id: KEY-WP-0034-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4c836ddf-2eb8-528a-b63f-8499f3a6cbf2"
|
||||
```
|
||||
|
||||
Publish immutable images, update canonical runtime pins, verify anonymous
|
||||
recovery and sign-out confirmation live, and record actual account switching
|
||||
only after browser evidence. Existing application sessions may outlive provider
|
||||
logout. Related: USER-WP-0025-T03 and VERGABE-WP-0019-T06.
|
||||
|
||||
Source verification: Full Go suite passed; final OIDC/config tests passed after recovery expansion. Immutable publication and live checks are in progress.
|
||||
|
||||
### Live recovery rollout — 2026-09-12
|
||||
|
||||
Recovery is deployed in KeyCape 4d8b8fe, User Engine e54b6ee and Vergabe c067993
|
||||
(Helm revision 3). All three are Ready. Six provider checks, eleven product
|
||||
checks and six fresh anonymous Chromium checks pass, including actual provider
|
||||
logout POST and return to the portal without test overrides. Detailed receipt: railiance-apps/docs/evidence/2026-09-12-account-recovery-live.md.
|
||||
|
||||
2026-09-24 23:42–23:45 UTC the founder completed the account switch. The issuer
|
||||
recorded one `mfa_failed` for `user-engine-portal`, then three separate
|
||||
`auth_start` → `auth_success` → `token_issued` sequences. A silent reuse of
|
||||
the previous portal session would not have produced those new successes.
|
||||
Provider logout still does not revoke already issued application JWTs.
|
||||
Receipt: `docs/evidence/2026-09-24-fresh-login-and-account-switch.md`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue