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
205 lines
8.7 KiB
YAML
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
|