net-kingdom/security-case-types/oidc-browser-relying-party/type.yaml
tegwick fa2cb57384
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0048-T04: SecurityCaseType oidc-browser-relying-party 1.0 (proposed)
Composes key-cape.oidc-client-registration; confidential/public conditions,
one justified attended step, threat model, fit table of hand-built clients,
Forgejo and public-portal examples. Validator gains when/required_when and
provider discovery. 43 tests pass. Adds T08 (identity-provisioner groups ensure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5457@bnt-lap001
Assistant-Session: 792bf84a-3783-4f5d-a811-cded61dd2012
2026-10-08 22:45:31 +02:00

205 lines
8.7 KiB
YAML

apiVersion: netkingdom.io/security-case-type/v0.1
kind: SecurityCaseType
spec:
id: oidc-browser-relying-party
version: "1.0"
status: proposed
title: "Browser OIDC relying party for an internal service"
purpose: >-
Let an internal web service sign its human users in through NetKingdom
(KeyCape): an exact-redirect OIDC client, optional MFA pin, the LLDAP groups
the service maps to its own roles, and, for confidential clients, a client
secret minted straight into OpenBao custody. Replaces the per-client rollout
scripts in sso-mfa/k8s/keycape. First case: Forgejo (KEY-WP-0038).
parameters:
- name: client_id
type: string
case_settable: true
constraints: {pattern: "^[a-z0-9][a-z0-9-]{1,62}$"}
sensitivity: operational
description: OAuth2 client_id, unique in KeyCape.
- name: display_name
type: string
case_settable: true
constraints: {pattern: "^[^\\n\\r]{1,80}$"}
sensitivity: public
description: Name shown on the sign-in page.
- name: redirect_uris
type: string
case_settable: true
constraints: {pattern: "^https://[^*\\s,]+(,https://[^*\\s,]+)*$"}
sensitivity: security_sensitive
description: Comma-separated exact https redirect URIs. No wildcards, no plain http.
- name: allowed_scopes
type: string
case_settable: true
constraints: {enum: ["openid", "openid,profile", "openid,profile,email", "openid,profile,email,groups", "openid,email", "openid,email,groups", "openid,profile,groups", "openid,groups"]}
sensitivity: security_sensitive
description: Scopes the client may request, openid first.
- name: client_type
type: string
case_settable: true
constraints: {enum: ["confidential", "public"]}
sensitivity: security_sensitive
description: confidential for server-side services that can hold a secret; public for SPAs and native apps (PKCE only).
- name: mfa_required
type: string
case_settable: true
constraints: {enum: ["true", "false"]}
sensitivity: security_sensitive
description: Pin MFA on this client. Required "true" when the service grants administrative or owner rights.
- name: access_groups
type: string
case_settable: true
constraints: {pattern: "^[a-z][a-z0-9-]{1,62}(,[a-z][a-z0-9-]{1,62})*$"}
sensitivity: operational
description: Comma-separated LLDAP groups that gate sign-in and that the service maps to its roles. Created if absent.
- name: initial_members
type: string
required: false
case_settable: true
constraints: {pattern: "^[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\\+[a-z][a-z0-9._-]*)*(;[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\\+[a-z][a-z0-9._-]*)*)*$"}
sensitivity: operational
description: "Initial memberships as group=user+user;group=user. Usernames are directory usernames, never secrets."
- name: secret_ref
type: string
required: false
case_settable: true
constraints:
pattern: "^bao:platform/workloads/[a-z0-9/_-]+$"
required_when: "client_type == confidential"
sensitivity: secret_reference
description: OpenBao custody path for a confidential client's secret. Omitted for public clients.
- name: consuming_service
type: string
case_settable: true
constraints: {pattern: "^[a-z0-9][a-z0-9-]*$"}
sensitivity: operational
description: The service that signs users in with this client.
- name: consumer_owner_repo
type: string
case_settable: true
constraints: {pattern: "^[a-z0-9][a-z0-9-]*$"}
sensitivity: operational
description: The repository that owns the consuming service's configuration.
- name: issuer
type: string
case_settable: false
default: https://kc.coulomb.social
sensitivity: security_sensitive
description: The NetKingdom issuer; fixed by the type.
composition:
authority: netkingdom
initial_trust: [runtime_secret_trust, runtime_identity_trust]
capabilities: [c1.lightweight-sso]
providers:
c1.lightweight-sso: key-cape.oidc-client-registration
parameter_bindings:
key-cape.oidc-client-registration.client_id: client_id
key-cape.oidc-client-registration.display_name: display_name
key-cape.oidc-client-registration.redirect_uris: redirect_uris
key-cape.oidc-client-registration.allowed_scopes: allowed_scopes
key-cape.oidc-client-registration.client_type: client_type
key-cape.oidc-client-registration.mfa_required: mfa_required
materialization:
- id: client-secret-path
kind: kv_path
when: "client_type == confidential"
template: "{secret_ref}"
- id: keycape-reader-policy
kind: openbao_policy
when: "client_type == confidential"
template: "keycape-client-{client_id}-read"
- id: consumer-reader-policy
kind: openbao_policy
when: "client_type == confidential"
template: "{consuming_service}-oidc-client-{client_id}-read"
- id: catalog-entry
kind: catalog_entry
template: "oidc-client-{client_id}"
follow_through:
- id: ensure-groups
owner: net-kingdom
entry_point: "identity-provisioner groups ensure --groups {access_groups} --members '{initial_members}'"
auth: "NetKingdom identity-provisioner service identity with LLDAP group administration, via the ops-warden proxy"
warden_executes: true
attended: false
- id: mint-secret
owner: railiance-platform
when: "client_type == confidential"
entry_point: "railiance-platform credential broker: generate a client secret into {secret_ref}"
auth: "credential broker issuer, scoped to the case's KV path"
warden_executes: true
attended: false
- id: register-client
owner: key-cape
entry_point: "keycape-client register (key-cape.oidc-client-registration) for {client_id}"
auth: "operator write access to the live KeyCape client registry in namespace sso"
warden_executes: false
attended: true
attended_justification: >-
The live KeyCape client registry is a Kubernetes Secret, and agents do
not read or write Secrets. Until KeyCape loads clients from a
non-secret registry with secrets referenced from custody, one attended
operator session applies the rendered registration. ops-warden groups
all attended steps of a run into that one session.
- id: hand-over
owner: "{consumer_owner_repo}"
entry_point: "configure {consuming_service} with issuer {issuer}, client {client_id} and custody {secret_ref}"
auth: "the consumer owner's own deployment path"
warden_executes: false
attended: false
verification:
- id: authorize-accepts-redirects
check: "GET {issuer}/authorize with client {client_id} and each of {redirect_uris} returns the login page"
expect: present
reads_secret_value: false
- id: authorize-refuses-other-redirect
check: "GET {issuer}/authorize with client {client_id} and an unregistered redirect returns an OAuth error"
expect: deny
reads_secret_value: false
- id: custody-denied-to-default
when: "client_type == confidential"
check: "a default-only OpenBao identity has no read capability on {secret_ref}"
expect: deny
reads_secret_value: false
- id: custody-allowed-to-consumer
when: "client_type == confidential"
check: "policy {consuming_service}-oidc-client-{client_id}-read grants read on {secret_ref} (capabilities check only)"
expect: allow
reads_secret_value: false
- id: groups-present
check: "LLDAP groups {access_groups} exist"
expect: present
reads_secret_value: false
rollback:
- reverses: hand-over
action: "remove the OIDC source for {client_id} from {consuming_service} (owner {consumer_owner_repo})"
- reverses: register-client
action: "keycape-client register --remove {client_id}"
- reverses: mint-secret
when: "client_type == confidential"
action: "destroy the secret at {secret_ref}"
- reverses: ensure-groups
action: "remove the case's initial memberships; delete groups {access_groups} only if this case created them and no other case uses them"
- reverses: client-secret-path
when: "client_type == confidential"
action: "remove the KV path {secret_ref}"
- reverses: keycape-reader-policy
when: "client_type == confidential"
action: "delete policy keycape-client-{client_id}-read"
- reverses: consumer-reader-policy
when: "client_type == confidential"
action: "delete policy {consuming_service}-oidc-client-{client_id}-read"
- reverses: catalog-entry
action: "remove catalog entry oidc-client-{client_id}"
acceptance:
accepted_by: [owner, founder]
risk_class: medium