NK-WP-0054-T03b: declarations and validator migrated to canon grants (draft-3)
Schema, validator, fixtures, declarations and tests (40 pass) now use the imported principal_type and authority grants MODE @ SCOPE with activation and environments, estate conventions bind and service-admin, unknown placeholders, and an elimination path on every exception. SI-4 and SI-7 became decidable. Before/after over the same declarations: 1/11/2/5/6 to 1/10/2/5/7 (non-conformant/unknown/excepted/planned/ conformant); the only move is KeyCape's secrets-engine-openbao client, whose scope is established. lldap/admin is now also excepted under SI-4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 328925@bnt-lap001 Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed
This commit is contained in:
parent
956a7e11a6
commit
ac66753bd4
18 changed files with 528 additions and 230 deletions
|
|
@ -311,7 +311,7 @@
|
|||
| task | NK-WP-0054-T01 | done | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T02 | wait | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T03 | done | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T03b | todo | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T03b | done | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T04 | wait | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T05 | wait | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
| task | NK-WP-0054-T06 | wait | — | workplans/NK-WP-0054-service-identity-policy.md |
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://policy.coulomb.social/schemas/service-identity/v0.1",
|
||||
"title": "NetKingdom service identity declaration v0.1",
|
||||
"description": "One service declares every identity it holds (NetKingdom Service Identity and Privilege v0.1, proposed). Holds no credential and no secret-shaped value. Facts a declarer cannot establish are written as 'unknown', never guessed.",
|
||||
"description": "One service declares every account it holds (NetKingdom Service Identity and Privilege v0.1, draft-3: an estate profile of SecurityCanon). Accounts carry an imported principal_type and authority grants written MODE @ SCOPE with activation and environment. Holds no credential and no secret-shaped value. Facts a declarer cannot establish are 'unknown', never guessed.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
|
|
@ -114,9 +114,9 @@
|
|||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"classes",
|
||||
"principal_type",
|
||||
"purpose",
|
||||
"privilege_level",
|
||||
"grants",
|
||||
"interactive_login",
|
||||
"second_factor",
|
||||
"custody",
|
||||
|
|
@ -129,38 +129,45 @@
|
|||
"name": {
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"classes": {
|
||||
"principal_type": {
|
||||
"enum": [
|
||||
"HUMAN",
|
||||
"WORKLOAD",
|
||||
"SERVICE",
|
||||
"AGENT",
|
||||
"CONTROLLER",
|
||||
"DEVICE",
|
||||
"unknown"
|
||||
],
|
||||
"description": "Imported from InfoTechCanon via SecurityCanon. 'unknown' is a declarer's placeholder."
|
||||
},
|
||||
"conventions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"enum": [
|
||||
"human",
|
||||
"break-glass",
|
||||
"bind",
|
||||
"service-admin",
|
||||
"workload",
|
||||
"agent",
|
||||
"infrastructure"
|
||||
"service-admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "Estate naming conventions for recurring grant shapes (standard section 2). More than one is itself a finding."
|
||||
},
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 300
|
||||
},
|
||||
"privilege_level": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read",
|
||||
"write_scoped",
|
||||
"admin",
|
||||
"unknown"
|
||||
]
|
||||
"grants": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 20,
|
||||
"items": {
|
||||
"$ref": "#/$defs/grant"
|
||||
}
|
||||
},
|
||||
"grants_admin_policy": {
|
||||
"type": "boolean"
|
||||
"federated_role": {
|
||||
"type": "boolean",
|
||||
"description": "A role mapping federated principals to authority; holds no credential."
|
||||
},
|
||||
"assurance_claim_bound": {
|
||||
"type": "boolean"
|
||||
|
|
@ -346,14 +353,12 @@
|
|||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"classes": {
|
||||
"contains": {
|
||||
"const": "agent"
|
||||
}
|
||||
"principal_type": {
|
||||
"const": "AGENT"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"classes"
|
||||
"principal_type"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
|
|
@ -374,7 +379,8 @@
|
|||
"reason",
|
||||
"owner",
|
||||
"expires",
|
||||
"compensating_control"
|
||||
"compensating_control",
|
||||
"elimination_path"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
@ -387,7 +393,7 @@
|
|||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^(SI-(1[0-3]|[1-9])|NAMING)$"
|
||||
"pattern": "^(SI-(1[0-5]|[1-9])|NAMING)$"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
|
|
@ -411,6 +417,71 @@
|
|||
"type": "string",
|
||||
"minLength": 10,
|
||||
"maxLength": 400
|
||||
},
|
||||
"elimination_path": {
|
||||
"type": "string",
|
||||
"minLength": 10,
|
||||
"maxLength": 400,
|
||||
"description": "What change ends the exception (DSM-EXC-004)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"grant": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"mode",
|
||||
"scope"
|
||||
],
|
||||
"description": "MODE @ SCOPE with SecurityCanon dimensions. 'unknown' is a declarer's placeholder, not a canon mode.",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"enum": [
|
||||
"USE",
|
||||
"OBSERVE",
|
||||
"OPERATE",
|
||||
"ADMINISTER",
|
||||
"GOVERN",
|
||||
"CONSTRUCT",
|
||||
"SOVEREIGN",
|
||||
"unknown"
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 120
|
||||
},
|
||||
"activation": {
|
||||
"enum": [
|
||||
"STANDING",
|
||||
"JUST_IN_TIME",
|
||||
"SCHEDULED",
|
||||
"APPROVED",
|
||||
"INCIDENT",
|
||||
"BREAK_GLASS",
|
||||
"RECOVERY"
|
||||
]
|
||||
},
|
||||
"environments": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"enum": [
|
||||
"DEVELOPMENT",
|
||||
"TEST",
|
||||
"STAGING",
|
||||
"PRODUCTION",
|
||||
"RECOVERY"
|
||||
]
|
||||
}
|
||||
},
|
||||
"read_only": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"maxLength": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ related:
|
|||
rules it profiles and states only what NetKingdom adds. New: the estate identities in canon terms (§3) and
|
||||
SI-15, accountable automation. All founder decisions and every rule id from draft-2 are kept.
|
||||
|
||||
The offline validator (`tools/service-identity`) still implements the draft-2 field model. Its migration to
|
||||
grants is NK-WP-0054-T03b. The live health check (T04) does not exist yet.
|
||||
The offline validator (`tools/service-identity`) implements draft-3 (principal type and grants, NK-WP-0054-T03b).
|
||||
The live health check (T04) does not exist yet.
|
||||
|
||||
## 1. Purpose, scope and authority
|
||||
|
||||
|
|
@ -216,9 +216,9 @@ Per account the result is `conformant`, `non-conformant`, `excepted`, `unknown`
|
|||
finding, not a pass. These match the companion's conformance states: `excepted` is a declared gap,
|
||||
`non-conformant` is an undeclared violation.
|
||||
|
||||
The offline validator decides from a declaration alone: SI-1, SI-2, SI-3, SI-6, SI-8, SI-9, SI-11, SI-12, SI-14
|
||||
and naming. SI-4 is an advisory. SI-5, SI-7, SI-10, SI-13 and SI-15 are process rules, checked by evidence rather
|
||||
than by declaration. Dates are explicit (`--as-of`).
|
||||
The offline validator decides from a declaration alone: SI-1, SI-2, SI-3, SI-4, SI-6, SI-7, SI-8, SI-9, SI-11,
|
||||
SI-12, SI-14 and naming; more than one service-admin account per service is an advisory. SI-5, SI-10, SI-13 and
|
||||
SI-15 are process rules, checked by evidence rather than by declaration. Dates are explicit (`--as-of`).
|
||||
|
||||
The health check (T04) is SEC-DSM's automated discovery (DSM-INV-004) applied to accounts. It compares declared
|
||||
against observed state from owner-provided read-only metadata and repository manifests. It MUST NOT read a Secret.
|
||||
|
|
@ -232,7 +232,7 @@ control), expiring or revalidated, and carrying an **elimination path**: what ch
|
|||
|
||||
| Id | Rules | Account | Reason | Elimination path |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| SI-X1 | SI-1, SI-2, SI-3, SI-9, naming | LLDAP `admin` | The human, the super-admin and the shared bind of four services in one account; password in a person's safe and a Kubernetes Secret | NK-WP-0054-T05: `svc-*` binds, provisioner account, `ld-admin`, `admin` human-only |
|
||||
| SI-X1 | SI-1, SI-2, SI-3, SI-4, SI-9, naming | LLDAP `admin` | The human, the super-admin and the shared bind of four services in one account; password in a person's safe and a Kubernetes Secret | NK-WP-0054-T05: `svc-*` binds, provisioner account, `ld-admin`, `admin` human-only |
|
||||
| SI-X2 | SI-10 | LDAP bind accounts | Static long-lived credential; LDAP has no lease | Governed residual secret (DSM-TRANS-002) until a federated directory lookup exists |
|
||||
| SI-X3 | naming, SI-11 | `trigger-admin` (privacyIDEA) | Legacy account, consumers uninventoried | NK-WP-0049-T22 disposition |
|
||||
| SI-X4 | SI-8 | OpenBao `netkingdom-platform-admin` | Role binds the group claim only | railiance-platform binds `aal2` (decided 2026-10-24) |
|
||||
|
|
@ -287,7 +287,7 @@ interactively (SI-3).
|
|||
|
||||
## 11. Adoption
|
||||
|
||||
Structure first, tooling second. Ratify the vocabulary and rules; migrate the declarations and validator to
|
||||
grants (T03b); confirm the declarations with their owners; run the health check read-only (T04) and publish the
|
||||
Structure first, tooling second. Ratify the vocabulary and rules (the declarations and validator already follow
|
||||
draft-3); confirm the declarations with their owners; run the health check read-only (T04) and publish the
|
||||
first report; then eliminate the exceptions one by one, starting with SI-X1 (T05). No exception is closed by
|
||||
writing a declaration. Each is closed by the live change and an evidence run.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ live state is T04's job.
|
|||
|
||||
## Classes used
|
||||
|
||||
These are draft-1 classes. Draft-3 of the standard replaces them with the canon's `principal_type` and grants
|
||||
`MODE @ SCOPE` (standard §2 has the mapping); the declarations in `tools/service-identity/declarations/` use draft-3.
|
||||
|
||||
|
||||
| Class | Meaning |
|
||||
| --- | --- |
|
||||
| human | A person's login |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ NK-WP-0054-T03. Offline and read-only. Checks declarations against
|
|||
`canon/schemas/service-identity_v0.1.schema.json` and evaluates the rules of the
|
||||
proposed [Service Identity and Privilege standard](../../canon/standards/service-identity-privilege_v0.1.md)
|
||||
that a declaration alone can decide. It was built against draft-1, before the founder's review,
|
||||
so a rule change in the standard changes this tool. It now follows draft-2.
|
||||
so a rule change in the standard changes this tool. It follows draft-3.
|
||||
|
||||
```bash
|
||||
make service-identity-test # unit tests
|
||||
|
|
@ -30,14 +30,20 @@ Alongside the results, **advisories** report declarations not asserted complete
|
|||
declarations not confirmed by the owning service, stale exceptions, more than one service-admin
|
||||
and more than one break-glass identity.
|
||||
|
||||
## Rules decided here
|
||||
## Model and rules decided here
|
||||
|
||||
SI-1, SI-2, SI-3 (including a credential shared by more than one service), SI-6, SI-8 (including an
|
||||
admin policy without a bound assurance claim), SI-9, SI-11 (overdue legacy disposition), SI-12, SI-14
|
||||
(an agent must present as an agent, with a token lifetime at or below the 3600-second, 60-minute ceiling) and
|
||||
the naming rules (`NAMING`). SI-4 (one admin per service) is an advisory. SI-5, SI-7, SI-10 and SI-13 are
|
||||
not decidable from a declaration. Observed state (what is really configured) is the health check,
|
||||
NK-WP-0054-T04.
|
||||
Draft-3 of the standard is an estate profile of SecurityCanon. Each account declares an imported
|
||||
`principal_type` (`HUMAN`, `SERVICE`, `WORKLOAD`, `AGENT`, `CONTROLLER`, `DEVICE`), its authority as grants
|
||||
`MODE @ SCOPE` with `activation` and `environments`, and optional estate `conventions` (`bind`,
|
||||
`service-admin`). Break-glass is `activation: BREAK_GLASS`, not a kind of account. `unknown` is allowed where a
|
||||
fact is not established and is reported as a finding. Exceptions carry an `elimination_path` (DSM-EXC-004).
|
||||
|
||||
Decided from a declaration: SI-1, SI-2, SI-3 (including a credential shared by more than one service), SI-4
|
||||
(a service-admin holds `ADMINISTER` over one service only), SI-6 (`SOVEREIGN` is offline and break-glass in
|
||||
production), SI-7 (`CONSTRUCT` never in production), SI-8 (second factor; admin roles bind the assurance
|
||||
claim), SI-9, SI-11 (overdue legacy disposition), SI-12, SI-14 (an agent presents as an agent, tokens at most
|
||||
3600 seconds) and naming. More than one service-admin account per service is an advisory. SI-5, SI-10, SI-13 and
|
||||
SI-15 are process rules. Observed state is the health check, NK-WP-0054-T04.
|
||||
|
||||
## Declarations
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ attestation:
|
|||
complete: true
|
||||
identities:
|
||||
- name: keycape
|
||||
classes: [workload]
|
||||
principal_type: SERVICE
|
||||
purpose: "The only registered OIDC client: KeyCape's upstream sign-in"
|
||||
privilege_level: none
|
||||
grants: [{mode: USE, scope: authelia, activation: STANDING, environments: [PRODUCTION], note: "authorization_code, one_factor"}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: identity-provisioner-token
|
||||
classes: [workload]
|
||||
principal_type: SERVICE
|
||||
purpose: "Service token that user-engine presents; the NetworkPolicy admits only user-engine pods"
|
||||
privilege_level: write_scoped
|
||||
grants: [{mode: USE, scope: identity-provisioner, activation: STANDING, environments: [PRODUCTION]}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: secrets-engine-openbao
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes openbao:login only (deny openbao:admin)"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; obtains a login token whose only scope is openbao:login"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: USE, scope: openbao, activation: STANDING, environments: [PRODUCTION], note: "scope openbao:login only; openbao:admin denied"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -20,9 +22,11 @@ identities:
|
|||
rotation: {path: "Mint in custody, then deliver (credential broker); KeyCape reads it at start", coupling: [keycape, secrets-engine]}
|
||||
status: active
|
||||
- name: secrets-engine-approval
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes not established in the repository"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; its token scopes are not established in the repository"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: unknown, scope: "token scopes", note: "not established in the repository"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -31,9 +35,11 @@ identities:
|
|||
rotation: {path: "Mint in custody, then deliver (credential broker); KeyCape reads it at start", coupling: [keycape, secrets-engine]}
|
||||
status: active
|
||||
- name: secrets-engine-requester
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes not established in the repository"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; its token scopes are not established in the repository"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: unknown, scope: "token scopes", note: "not established in the repository"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -42,9 +48,11 @@ identities:
|
|||
rotation: {path: "Mint in custody, then deliver (credential broker); KeyCape reads it at start", coupling: [keycape, secrets-engine]}
|
||||
status: active
|
||||
- name: approval-engine-operator
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes not established in the repository"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; its token scopes are not established in the repository"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: unknown, scope: "token scopes", note: "not established in the repository"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -53,9 +61,11 @@ identities:
|
|||
rotation: {path: "Mint in custody, then deliver (credential broker); KeyCape reads it at start", coupling: [keycape, approval-engine]}
|
||||
status: active
|
||||
- name: informed-decision-sitting-requester
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes not established in the repository"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; its token scopes are not established in the repository"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: unknown, scope: "token scopes", note: "not established in the repository"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -64,9 +74,11 @@ identities:
|
|||
rotation: {path: "Mint in custody, then deliver (credential broker); KeyCape reads it at start", coupling: [keycape, informed-decision-sitting]}
|
||||
status: active
|
||||
- name: rapp-qonto
|
||||
classes: [workload]
|
||||
purpose: "Confidential OIDC client; scopes not established in the repository"
|
||||
privilege_level: unknown
|
||||
principal_type: SERVICE
|
||||
purpose: "Confidential OIDC client; its token scopes are not established in the repository"
|
||||
grants:
|
||||
- {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"}
|
||||
- {mode: unknown, scope: "token scopes", note: "not established in the repository"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: admin
|
||||
classes: [human, bind, service-admin]
|
||||
purpose: "Human platform administrator and, with the same password, the directory super-admin and the bind of four services"
|
||||
privilege_level: admin
|
||||
principal_type: HUMAN
|
||||
conventions: [bind, service-admin]
|
||||
purpose: "The human platform administrator; with the same password also the directory super-admin and the bind of four services"
|
||||
grants:
|
||||
- {mode: ADMINISTER, scope: directory, activation: STANDING, environments: [PRODUCTION]}
|
||||
- {mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true, note: "service binds"}
|
||||
interactive_login: true
|
||||
second_factor: required
|
||||
custody: [delivered-secret, personal-safe]
|
||||
|
|
@ -28,9 +31,11 @@ identities:
|
|||
status: active
|
||||
recovery: {needed: true, works_with_sso_down: false, in_crisis_list: true}
|
||||
- name: ld-admin
|
||||
classes: [service-admin]
|
||||
purpose: "LLDAP only: the directory's configured super-admin, separate from every bind and every person; introduced by configuration after a rehearsal"
|
||||
privilege_level: admin
|
||||
principal_type: HUMAN
|
||||
conventions: [service-admin]
|
||||
purpose: "LLDAP only: the directory's configured super-admin, a sealed fallback; introduced by configuration after a rehearsal"
|
||||
grants:
|
||||
- {mode: ADMINISTER, scope: lldap, activation: BREAK_GLASS, environments: [PRODUCTION]}
|
||||
interactive_login: true
|
||||
second_factor: unknown
|
||||
custody: [unknown]
|
||||
|
|
@ -39,9 +44,10 @@ identities:
|
|||
rotation: {path: "To be decided in NK-WP-0054-T06", coupling: []}
|
||||
status: planned
|
||||
- name: svc-authelia
|
||||
classes: [bind]
|
||||
principal_type: SERVICE
|
||||
conventions: [bind]
|
||||
purpose: "Target: Authelia's read-only directory lookup"
|
||||
privilege_level: read
|
||||
grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -50,9 +56,10 @@ identities:
|
|||
rotation: {path: "Secret rotation with Authelia only", coupling: [authelia]}
|
||||
status: planned
|
||||
- name: svc-keycape
|
||||
classes: [bind]
|
||||
principal_type: SERVICE
|
||||
conventions: [bind]
|
||||
purpose: "Target: KeyCape's read-only directory lookup"
|
||||
privilege_level: read
|
||||
grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -61,9 +68,10 @@ identities:
|
|||
rotation: {path: "Secret rotation with KeyCape only", coupling: [keycape]}
|
||||
status: planned
|
||||
- name: svc-privacyidea
|
||||
classes: [bind]
|
||||
principal_type: SERVICE
|
||||
conventions: [bind]
|
||||
purpose: "Target: the privacyIDEA resolver's read-only directory lookup"
|
||||
privilege_level: read
|
||||
grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -72,9 +80,9 @@ identities:
|
|||
rotation: {path: "Resolver bind password, stored encrypted in the privacyIDEA database", coupling: [privacyidea]}
|
||||
status: planned
|
||||
- name: svc-provisioner
|
||||
classes: [workload]
|
||||
principal_type: SERVICE
|
||||
purpose: "Target: identity-provisioner, narrowest working write scope for users and groups, not the super-admin (scope to be tested)"
|
||||
privilege_level: write_scoped
|
||||
grants: [{mode: ADMINISTER, scope: "directory/people,groups", activation: STANDING, environments: [PRODUCTION]}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -84,9 +92,10 @@ identities:
|
|||
status: planned
|
||||
exceptions:
|
||||
- id: SI-X1
|
||||
rules: [SI-1, SI-2, SI-3, SI-9, NAMING]
|
||||
rules: [SI-1, SI-2, SI-3, SI-4, SI-9, NAMING]
|
||||
identity: admin
|
||||
reason: "One account is the human, the super-admin and the shared bind of four services, and its password is in a person's safe and in a Kubernetes Secret"
|
||||
owner: "Founder"
|
||||
expires: 2027-01-31
|
||||
compensating_control: "Web UI restricted by IP allowlist; OTP on the human login; NK-WP-0054-T05 removes the sharing"
|
||||
compensating_control: "Web UI restricted by IP allowlist; OTP on the human login"
|
||||
elimination_path: "NK-WP-0054-T05: svc-* binds, a provisioner account, ld-admin as the configured super-admin, admin human-only"
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: netkingdom-platform-admin
|
||||
classes: [human]
|
||||
principal_type: HUMAN
|
||||
federated_role: true
|
||||
purpose: "OIDC role: group net-kingdom-admins receives policies platform-admin and operator-custody, one-hour token"
|
||||
privilege_level: admin
|
||||
grants_admin_policy: true
|
||||
grants: [{mode: ADMINISTER, scope: openbao, activation: STANDING, environments: [PRODUCTION]}]
|
||||
assurance_claim_bound: false
|
||||
interactive_login: true
|
||||
second_factor: required
|
||||
|
|
@ -23,10 +23,10 @@ identities:
|
|||
status: active
|
||||
recovery: {needed: true, works_with_sso_down: false, in_crisis_list: true}
|
||||
- name: credential-broker-keycape-client-secret-generate
|
||||
classes: [human]
|
||||
principal_type: HUMAN
|
||||
federated_role: true
|
||||
purpose: "OIDC role: net-kingdom-admins with aal2 may generate a KeyCape client secret in custody"
|
||||
privilege_level: write_scoped
|
||||
grants_admin_policy: false
|
||||
grants: [{mode: ADMINISTER, scope: "openbao/platform/workloads/keycape", activation: STANDING, environments: [PRODUCTION]}]
|
||||
assurance_claim_bound: true
|
||||
interactive_login: true
|
||||
second_factor: required
|
||||
|
|
@ -36,10 +36,10 @@ identities:
|
|||
rotation: {path: "Federated; policy change by reviewed change", coupling: []}
|
||||
status: active
|
||||
- name: fluid-telegram-operators
|
||||
classes: [human]
|
||||
principal_type: HUMAN
|
||||
federated_role: true
|
||||
purpose: "OIDC role: fluid-telegram operators with aal2"
|
||||
privilege_level: unknown
|
||||
grants_admin_policy: false
|
||||
grants: [{mode: unknown, scope: openbao, note: "policy not established in the repository"}]
|
||||
assurance_claim_bound: true
|
||||
interactive_login: true
|
||||
second_factor: required
|
||||
|
|
@ -49,9 +49,10 @@ identities:
|
|||
rotation: {path: "Federated; policy change by reviewed change", coupling: []}
|
||||
status: active
|
||||
- name: secrets-engine-openbao
|
||||
classes: [workload]
|
||||
principal_type: SERVICE
|
||||
federated_role: true
|
||||
purpose: "JWT role: subject service:secrets-engine, scope openbao:login, policy secrets-engine-login-self, five-minute token"
|
||||
privilege_level: read
|
||||
grants: [{mode: USE, scope: openbao, activation: STANDING, environments: [PRODUCTION], note: "login-self only"}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [none]
|
||||
|
|
@ -60,10 +61,11 @@ identities:
|
|||
rotation: {path: "Federated; the KeyCape client secret rotates in the keycape declaration", coupling: [keycape]}
|
||||
status: active
|
||||
- name: coding-agent
|
||||
classes: [agent]
|
||||
principal_type: AGENT
|
||||
federated_role: true
|
||||
agent: {mode: unknown, presents_principal_type: service, max_token_ttl_seconds: 900, boundary_policy: agent-high-risk-boundary}
|
||||
purpose: "JWT role and AppRole for the coding agent; policies agent-high-risk-boundary and workload-kv-read-issue-core-runtime, fifteen minutes"
|
||||
privilege_level: unknown
|
||||
grants: [{mode: unknown, scope: openbao, note: "boundary policy content not established"}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [none]
|
||||
|
|
@ -78,4 +80,5 @@ exceptions:
|
|||
reason: "The role file binds on the group claim only; the aal2 claim is not bound while two sibling roles bind it"
|
||||
owner: "railiance-platform and founder"
|
||||
expires: 2027-01-31
|
||||
compensating_control: "KeyCape requires a second factor for every login (requireForAll); decision on standard section 10 question 1"
|
||||
compensating_control: "KeyCape requires a second factor for every login (requireForAll)"
|
||||
elimination_path: "railiance-platform binds aal2 in the role (decided 2026-10-24)"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
# People and the break-glass identity. Provisional location (standard section 10, question 6).
|
||||
# People and the break-glass authority, in SecurityCanon terms (standard section 3).
|
||||
schema_version: "0.1"
|
||||
service: platform
|
||||
owner: "Founder (Bernd Worsch)"
|
||||
declared_at: 2026-10-24
|
||||
attestation:
|
||||
authored_by: claude-code
|
||||
basis: [docs/service-identity-inventory.md, docs/platform-root-custody.md, canon/standards/credential-management_v0.2.md]
|
||||
basis: [docs/service-identity-inventory.md, docs/platform-root-custody.md, canon/standards/service-identity-privilege_v0.1.md]
|
||||
owner_confirmed: false
|
||||
complete: false
|
||||
identities:
|
||||
- name: platform-root
|
||||
classes: [break-glass]
|
||||
purpose: "Final authority (king credential); not for routine or production use"
|
||||
privilege_level: admin
|
||||
principal_type: HUMAN
|
||||
purpose: "King credential: final authority, break-glass in production; build-mode authority outside production"
|
||||
grants:
|
||||
- {mode: SOVEREIGN, scope: platform, activation: BREAK_GLASS, environments: [PRODUCTION, RECOVERY]}
|
||||
- {mode: CONSTRUCT, scope: platform, activation: STANDING, environments: [DEVELOPMENT, TEST]}
|
||||
interactive_login: true
|
||||
second_factor: required
|
||||
custody: [offline-key]
|
||||
|
|
@ -22,9 +24,10 @@ identities:
|
|||
status: active
|
||||
recovery: {needed: true, works_with_sso_down: false, in_crisis_list: true}
|
||||
- name: tegwick
|
||||
classes: [human]
|
||||
purpose: "Developer identity: builds and tests the platform, Git and tooling"
|
||||
privilege_level: unknown
|
||||
principal_type: HUMAN
|
||||
purpose: "Developer identity (CONSTRUCT @ product); its actual production reach is not established"
|
||||
grants:
|
||||
- {mode: CONSTRUCT, scope: product, activation: STANDING, environments: [DEVELOPMENT, TEST]}
|
||||
interactive_login: true
|
||||
second_factor: unknown
|
||||
custody: [unknown]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: privacyidea
|
||||
classes: [infrastructure]
|
||||
purpose: "Owner of privacyidea_db in the net-kingdom-pg cluster"
|
||||
privilege_level: write_scoped
|
||||
principal_type: SERVICE
|
||||
purpose: "Database role owning privacyidea_db in the net-kingdom-pg cluster"
|
||||
grants:
|
||||
- {mode: USE, scope: privacyidea_db, activation: STANDING, environments: [PRODUCTION]}
|
||||
- {mode: ADMINISTER, scope: privacyidea_db, activation: STANDING, environments: [PRODUCTION], note: "database owner"}
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ attestation:
|
|||
complete: false
|
||||
identities:
|
||||
- name: pi-admin
|
||||
classes: [service-admin]
|
||||
principal_type: HUMAN
|
||||
conventions: [service-admin]
|
||||
purpose: "privacyIDEA's local administrator; policy pi-admin-all-rights restricted to adminuser=pi-admin"
|
||||
privilege_level: admin
|
||||
grants: [{mode: ADMINISTER, scope: privacyidea, activation: STANDING, environments: [PRODUCTION]}]
|
||||
interactive_login: true
|
||||
second_factor: unknown
|
||||
custody: [personal-safe]
|
||||
|
|
@ -21,9 +22,10 @@ identities:
|
|||
status: active
|
||||
recovery: {needed: true, works_with_sso_down: true, in_crisis_list: true}
|
||||
- name: trigger-admin
|
||||
classes: [service-admin]
|
||||
purpose: "Legacy Keycloak-era administrator account, policy and Secret; consumers not inventoried"
|
||||
privilege_level: admin
|
||||
principal_type: unknown
|
||||
conventions: [service-admin]
|
||||
purpose: "Legacy Keycloak-era administrator account, policy and Secret; who presents it is not established"
|
||||
grants: [{mode: ADMINISTER, scope: privacyidea, activation: STANDING, environments: [PRODUCTION]}]
|
||||
interactive_login: false
|
||||
second_factor: unknown
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -33,9 +35,9 @@ identities:
|
|||
status: legacy
|
||||
disposition: {action: defer, owner: "Founder", date: 2026-12-31}
|
||||
- name: keycape-factor-reader
|
||||
classes: [workload]
|
||||
principal_type: SERVICE
|
||||
purpose: "KeyCape's access to read factor state; delivered by railiance-platform (RPF-WP-0040)"
|
||||
privilege_level: unknown
|
||||
grants: [{mode: unknown, scope: privacyidea, note: "exact rights not established"}]
|
||||
interactive_login: false
|
||||
second_factor: not_applicable
|
||||
custody: [delivered-secret]
|
||||
|
|
@ -50,4 +52,5 @@ exceptions:
|
|||
reason: "Legacy account whose name does not follow the admin naming form and whose consumers are uninventoried"
|
||||
owner: "Founder"
|
||||
expires: 2026-12-31
|
||||
compensating_control: "Not provisioned by the current bootstrap; revocation tracked under NK-WP-0049-T22"
|
||||
compensating_control: "Not provisioned by the current bootstrap"
|
||||
elimination_path: "Revocation or acceptance under NK-WP-0049-T22"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,50 @@
|
|||
"service": "example",
|
||||
"owner": "Example owners",
|
||||
"declared_at": "2026-10-24",
|
||||
"attestation": {"authored_by": "fixture", "basis": ["fixture"], "owner_confirmed": true},
|
||||
"attestation": {
|
||||
"authored_by": "fixture",
|
||||
"basis": [
|
||||
"fixture"
|
||||
],
|
||||
"owner_confirmed": true
|
||||
},
|
||||
"complete": true,
|
||||
"identities": [
|
||||
{"name": "svc-example", "classes": ["bind"], "purpose": "Lookup with hvs.ABCDEFGH12345678abcdef smuggled in", "privilege_level": "read",
|
||||
"interactive_login": false, "second_factor": "not_applicable", "custody": ["delivered-secret"],
|
||||
"consumers": [{"name": "example", "kind": "service"}], "owner": "Example owners",
|
||||
"rotation": {"path": "Rotate with the consumer", "coupling": []}, "status": "active"}
|
||||
{
|
||||
"name": "svc-example",
|
||||
"purpose": "Lookup with hvs.ABCDEFGH12345678abcdef smuggled in",
|
||||
"interactive_login": false,
|
||||
"second_factor": "not_applicable",
|
||||
"custody": [
|
||||
"delivered-secret"
|
||||
],
|
||||
"consumers": [
|
||||
{
|
||||
"name": "example",
|
||||
"kind": "service"
|
||||
}
|
||||
],
|
||||
"owner": "Example owners",
|
||||
"rotation": {
|
||||
"path": "Rotate with the consumer",
|
||||
"coupling": []
|
||||
},
|
||||
"status": "active",
|
||||
"principal_type": "SERVICE",
|
||||
"conventions": [
|
||||
"bind"
|
||||
],
|
||||
"grants": [
|
||||
{
|
||||
"mode": "USE",
|
||||
"scope": "directory",
|
||||
"activation": "STANDING",
|
||||
"environments": [
|
||||
"PRODUCTION"
|
||||
],
|
||||
"read_only": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,87 @@
|
|||
"service": "example",
|
||||
"owner": "Example owners",
|
||||
"declared_at": "2026-10-24",
|
||||
"attestation": {"authored_by": "fixture", "basis": ["fixture"], "owner_confirmed": true},
|
||||
"attestation": {
|
||||
"authored_by": "fixture",
|
||||
"basis": [
|
||||
"fixture"
|
||||
],
|
||||
"owner_confirmed": true
|
||||
},
|
||||
"complete": true,
|
||||
"identities": [
|
||||
{"name": "svc-example", "classes": ["bind"], "purpose": "Read-only lookup", "privilege_level": "read",
|
||||
"interactive_login": false, "second_factor": "not_applicable", "custody": ["delivered-secret"],
|
||||
"consumers": [{"name": "example", "kind": "service"}], "owner": "Example owners",
|
||||
"rotation": {"path": "Rotate with the consumer", "coupling": ["example"]}, "status": "active"},
|
||||
{"name": "ex-admin", "classes": ["service-admin"], "purpose": "Administer the example service", "privilege_level": "admin",
|
||||
"interactive_login": true, "second_factor": "required", "custody": ["personal-safe"],
|
||||
"consumers": [{"name": "operator", "kind": "person"}], "owner": "Example owners",
|
||||
"rotation": {"path": "Owner-managed", "coupling": []}, "status": "active"}
|
||||
{
|
||||
"name": "svc-example",
|
||||
"purpose": "Read-only lookup",
|
||||
"interactive_login": false,
|
||||
"second_factor": "not_applicable",
|
||||
"custody": [
|
||||
"delivered-secret"
|
||||
],
|
||||
"consumers": [
|
||||
{
|
||||
"name": "example",
|
||||
"kind": "service"
|
||||
}
|
||||
],
|
||||
"owner": "Example owners",
|
||||
"rotation": {
|
||||
"path": "Rotate with the consumer",
|
||||
"coupling": [
|
||||
"example"
|
||||
]
|
||||
},
|
||||
"status": "active",
|
||||
"principal_type": "SERVICE",
|
||||
"conventions": [
|
||||
"bind"
|
||||
],
|
||||
"grants": [
|
||||
{
|
||||
"mode": "USE",
|
||||
"scope": "directory",
|
||||
"activation": "STANDING",
|
||||
"environments": [
|
||||
"PRODUCTION"
|
||||
],
|
||||
"read_only": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ex-admin",
|
||||
"purpose": "Administer the example service",
|
||||
"interactive_login": true,
|
||||
"second_factor": "required",
|
||||
"custody": [
|
||||
"personal-safe"
|
||||
],
|
||||
"consumers": [
|
||||
{
|
||||
"name": "operator",
|
||||
"kind": "person"
|
||||
}
|
||||
],
|
||||
"owner": "Example owners",
|
||||
"rotation": {
|
||||
"path": "Owner-managed",
|
||||
"coupling": []
|
||||
},
|
||||
"status": "active",
|
||||
"principal_type": "HUMAN",
|
||||
"conventions": [
|
||||
"service-admin"
|
||||
],
|
||||
"grants": [
|
||||
{
|
||||
"mode": "ADMINISTER",
|
||||
"scope": "example",
|
||||
"activation": "STANDING",
|
||||
"environments": [
|
||||
"PRODUCTION"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,16 @@ SCHEMA = V.load_schema()
|
|||
NOW = dt.date(2026, 10, 24)
|
||||
|
||||
|
||||
READ = {"mode": "USE", "scope": "directory", "activation": "STANDING", "environments": ["PRODUCTION"], "read_only": True}
|
||||
|
||||
|
||||
def grant(mode, scope="example", **over):
|
||||
return {"mode": mode, "scope": scope, "activation": "STANDING", "environments": ["PRODUCTION"], **over}
|
||||
|
||||
|
||||
def identity(**over):
|
||||
base = {"name": "svc-x", "classes": ["bind"], "purpose": "Read-only lookup", "privilege_level": "read",
|
||||
base = {"name": "svc-x", "principal_type": "SERVICE", "conventions": ["bind"], "purpose": "Read-only lookup",
|
||||
"grants": [dict(READ)],
|
||||
"interactive_login": False, "second_factor": "not_applicable", "custody": ["delivered-secret"],
|
||||
"consumers": [{"name": "x", "kind": "service"}], "owner": "Owners",
|
||||
"rotation": {"path": "Rotate with the consumer", "coupling": ["x"]}, "status": "active"}
|
||||
|
|
@ -41,7 +49,8 @@ def declaration(*identities, exceptions=None, complete=True, confirmed=True):
|
|||
|
||||
def exception(**over):
|
||||
base = {"id": "SI-X9", "rules": ["SI-3"], "identity": "svc-x", "reason": "Reason for the test",
|
||||
"owner": "Owners", "expires": "2027-01-31", "compensating_control": "A compensating control"}
|
||||
"owner": "Owners", "expires": "2027-01-31", "compensating_control": "A compensating control",
|
||||
"elimination_path": "The change that ends it"}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
|
@ -53,6 +62,11 @@ def result_of(doc, as_of=NOW, name=None):
|
|||
return found[0]
|
||||
|
||||
|
||||
HUMAN_ADMIN = dict(name="ex-admin", principal_type="HUMAN", conventions=["service-admin"], grants=[grant("ADMINISTER")],
|
||||
interactive_login=True, second_factor="required", custody=["personal-safe"],
|
||||
consumers=[{"name": "p", "kind": "person"}])
|
||||
|
||||
|
||||
class StructureTests(unittest.TestCase):
|
||||
def test_conformant_fixture_is_valid_and_conformant(self):
|
||||
doc = json.loads((FIXTURES / "valid/conformant.json").read_text())
|
||||
|
|
@ -65,17 +79,21 @@ class StructureTests(unittest.TestCase):
|
|||
self.assertEqual(errors, ["value looks like secret material"])
|
||||
self.assertNotIn("hvs.", " ".join(errors))
|
||||
|
||||
def test_unknown_field_and_class_are_rejected(self):
|
||||
doc = declaration(identity(password="x"))
|
||||
self.assertTrue(V.structural_errors(doc, SCHEMA))
|
||||
doc = declaration(identity(classes=["wizard"]))
|
||||
self.assertTrue(V.structural_errors(doc, SCHEMA))
|
||||
def test_unknown_field_mode_and_principal_are_rejected(self):
|
||||
self.assertTrue(V.structural_errors(declaration(identity(password="x")), SCHEMA))
|
||||
self.assertTrue(V.structural_errors(declaration(identity(grants=[grant("ROOT")])), SCHEMA))
|
||||
self.assertTrue(V.structural_errors(declaration(identity(principal_type="ROBOT")), SCHEMA))
|
||||
|
||||
def test_duplicate_identity_and_undeclared_exception_target(self):
|
||||
self.assertIn("duplicate identity name", V.structural_errors(declaration(identity(), identity()), SCHEMA))
|
||||
doc = declaration(identity(), exceptions=[exception(identity="ghost")])
|
||||
self.assertIn("exception names an undeclared identity", V.structural_errors(doc, SCHEMA))
|
||||
|
||||
def test_exception_needs_an_elimination_path(self):
|
||||
bad = exception()
|
||||
del bad["elimination_path"]
|
||||
self.assertTrue(V.structural_errors(declaration(identity(), exceptions=[bad]), SCHEMA))
|
||||
|
||||
def test_legacy_identity_needs_a_disposition(self):
|
||||
self.assertTrue(V.structural_errors(declaration(identity(status="legacy")), SCHEMA))
|
||||
ok = identity(status="legacy", disposition={"action": "retire", "owner": "Owners", "date": "2026-12-31"})
|
||||
|
|
@ -86,58 +104,71 @@ class RuleTests(unittest.TestCase):
|
|||
def rules(self, **over):
|
||||
return result_of(declaration(identity(**over)))["rules"]
|
||||
|
||||
def test_si1_more_than_one_class(self):
|
||||
self.assertIn("SI-1", self.rules(name="admin", classes=["human", "bind"]))
|
||||
def test_si1_a_human_account_presented_by_a_service_or_two_conventions(self):
|
||||
self.assertIn("SI-1", self.rules(**{**HUMAN_ADMIN, "consumers": [{"name": "s", "kind": "service"}]}))
|
||||
self.assertIn("SI-1", self.rules(conventions=["bind", "service-admin"]))
|
||||
|
||||
def test_si2_person_and_service_share_a_credential(self):
|
||||
consumers = [{"name": "p", "kind": "person"}, {"name": "s", "kind": "service"}]
|
||||
self.assertIn("SI-2", self.rules(consumers=consumers))
|
||||
|
||||
def test_si3_bind_must_be_read_only_non_interactive_and_unshared(self):
|
||||
self.assertIn("SI-3", self.rules(privilege_level="admin"))
|
||||
def test_si3_bind_is_a_read_only_use_grant_with_one_consumer(self):
|
||||
self.assertEqual(self.rules(), [])
|
||||
self.assertIn("SI-3", self.rules(grants=[grant("ADMINISTER", "directory")]))
|
||||
self.assertIn("SI-3", self.rules(grants=[{**READ, "read_only": False}]))
|
||||
self.assertIn("SI-3", self.rules(interactive_login=True))
|
||||
self.assertIn("SI-3", self.rules(principal_type="HUMAN", consumers=[{"name": "x", "kind": "service"}]))
|
||||
shared = [{"name": "a", "kind": "service"}, {"name": "b", "kind": "service"}]
|
||||
self.assertIn("SI-3", self.rules(consumers=shared))
|
||||
self.assertEqual(self.rules(), [])
|
||||
|
||||
def test_si8_admin_without_enforced_second_factor_or_assurance(self):
|
||||
admin = dict(name="ex-admin", classes=["service-admin"], privilege_level="admin", interactive_login=True,
|
||||
consumers=[{"name": "p", "kind": "person"}], custody=["personal-safe"])
|
||||
self.assertIn("SI-8", self.rules(second_factor="not_enforced", **admin))
|
||||
self.assertEqual(self.rules(second_factor="required", **admin), [])
|
||||
self.assertIn("SI-8", self.rules(second_factor="required", grants_admin_policy=True, **admin))
|
||||
self.assertEqual(self.rules(second_factor="required", grants_admin_policy=True, assurance_claim_bound=True, **admin), [])
|
||||
def test_si4_service_admin_holds_administer_over_one_service(self):
|
||||
self.assertEqual(self.rules(**HUMAN_ADMIN), [])
|
||||
self.assertIn("SI-4", self.rules(**{**HUMAN_ADMIN, "grants": [grant("ADMINISTER"), grant("USE")]}))
|
||||
self.assertIn("SI-4", self.rules(**{**HUMAN_ADMIN, "grants": [grant("ADMINISTER", "a"), grant("ADMINISTER", "b")]}))
|
||||
|
||||
def test_si9_a_person_safe_must_not_hold_a_service_credential(self):
|
||||
def test_si6_sovereign_is_offline_and_break_glass_in_production(self):
|
||||
root = dict(name="platform-root", principal_type="HUMAN", conventions=[], interactive_login=True,
|
||||
second_factor="required", custody=["offline-key"], consumers=[{"name": "p", "kind": "person"}])
|
||||
ok = grant("SOVEREIGN", "platform", activation="BREAK_GLASS", environments=["PRODUCTION", "RECOVERY"])
|
||||
self.assertEqual(self.rules(grants=[ok], **root), [])
|
||||
self.assertIn("SI-6", self.rules(grants=[{**ok, "activation": "STANDING"}], **root))
|
||||
self.assertIn("SI-6", self.rules(grants=[ok], **{**root, "custody": ["personal-safe"]}))
|
||||
self.assertIn("SI-6", self.rules(grants=[ok], **{**root, "consumers": [{"name": "s", "kind": "service"}]}))
|
||||
|
||||
def test_si7_construct_is_never_available_in_production(self):
|
||||
dev = dict(name="dev", principal_type="HUMAN", conventions=[], interactive_login=True, second_factor="required",
|
||||
custody=["personal-safe"], consumers=[{"name": "p", "kind": "person"}])
|
||||
self.assertEqual(self.rules(grants=[grant("CONSTRUCT", "product", environments=["DEVELOPMENT", "TEST"])], **dev), [])
|
||||
self.assertIn("SI-7", self.rules(grants=[grant("CONSTRUCT", "product")], **dev))
|
||||
|
||||
def test_si8_second_factor_and_assurance_on_admin_roles(self):
|
||||
self.assertIn("SI-8", self.rules(**{**HUMAN_ADMIN, "second_factor": "not_enforced"}))
|
||||
role = dict(name="admins-role", principal_type="HUMAN", conventions=[], federated_role=True, grants=[grant("ADMINISTER")],
|
||||
interactive_login=True, second_factor="required", custody=["none"], consumers=[{"name": "g", "kind": "person"}])
|
||||
self.assertIn("SI-8", self.rules(**role))
|
||||
self.assertEqual(self.rules(**role, assurance_claim_bound=True), [])
|
||||
self.assertEqual(self.rules(**{**role, "grants": [grant("USE")]}), [])
|
||||
|
||||
def test_si9_a_persons_safe_never_holds_a_service_credential(self):
|
||||
self.assertIn("SI-9", self.rules(custody=["delivered-secret", "personal-safe"]))
|
||||
|
||||
def test_si6_break_glass_is_offline_and_has_no_service_consumer(self):
|
||||
bg = dict(name="platform-root", classes=["break-glass"], privilege_level="admin", interactive_login=True,
|
||||
second_factor="required", consumers=[{"name": "p", "kind": "person"}])
|
||||
self.assertEqual(self.rules(custody=["offline-key"], **bg), [])
|
||||
self.assertIn("SI-6", self.rules(custody=["personal-safe"], **bg))
|
||||
self.assertIn("SI-6", self.rules(custody=["offline-key"], **{**bg, "consumers": [{"name": "s", "kind": "service"}]}))
|
||||
|
||||
def test_si12_recovery_identity_must_be_in_the_crisis_list(self):
|
||||
self.assertIn("SI-12", self.rules(recovery={"needed": True, "works_with_sso_down": True, "in_crisis_list": False}))
|
||||
|
||||
def test_naming_is_reserved_by_class(self):
|
||||
def test_naming_is_reserved_by_convention(self):
|
||||
self.assertIn("NAMING", self.rules(name="lookup"))
|
||||
self.assertIn("NAMING", self.rules(name="x-admin", classes=["human"], second_factor="required", interactive_login=True,
|
||||
consumers=[{"name": "p", "kind": "person"}]))
|
||||
self.assertIn("NAMING", self.rules(**{**HUMAN_ADMIN, "name": "administrator"}))
|
||||
self.assertIn("NAMING", self.rules(**{**HUMAN_ADMIN, "name": "admin"})) # `admin` is the human's name everywhere
|
||||
self.assertEqual(self.rules(**{**HUMAN_ADMIN, "name": "ld-admin"}), [])
|
||||
person = dict(principal_type="HUMAN", conventions=[], grants=[grant("OPERATE", "platform")], interactive_login=True,
|
||||
second_factor="required", consumers=[{"name": "p", "kind": "person"}])
|
||||
self.assertIn("NAMING", self.rules(name="x-admin", custody=["personal-safe"], **person))
|
||||
# A federated role holds no credential and cannot be mistaken for an account.
|
||||
self.assertNotIn("NAMING", self.rules(name="group-admin", classes=["human"], second_factor="required", interactive_login=True,
|
||||
custody=["none"], consumers=[{"name": "p", "kind": "person"}]))
|
||||
admin = dict(classes=["service-admin"], privilege_level="admin", second_factor="required", interactive_login=True,
|
||||
consumers=[{"name": "p", "kind": "person"}], custody=["personal-safe"])
|
||||
self.assertIn("NAMING", self.rules(name="administrator", **admin))
|
||||
self.assertIn("NAMING", self.rules(name="admin", **admin)) # `admin` is the human's name everywhere
|
||||
self.assertEqual(self.rules(name="ld-admin", **admin), [])
|
||||
self.assertNotIn("NAMING", self.rules(name="group-admin", custody=["none"], **person))
|
||||
|
||||
def test_overdue_legacy_disposition_is_a_violation(self):
|
||||
legacy = identity(status="legacy", name="old-admin", classes=["service-admin"], privilege_level="admin",
|
||||
second_factor="required", interactive_login=True, consumers=[],
|
||||
disposition={"action": "defer", "owner": "Owners", "date": "2026-10-01"})
|
||||
legacy = identity(**{**HUMAN_ADMIN, "name": "old-admin", "consumers": [], "status": "legacy",
|
||||
"disposition": {"action": "defer", "owner": "Owners", "date": "2026-10-01"}})
|
||||
self.assertIn("SI-11", result_of(declaration(legacy))["rules"])
|
||||
legacy["disposition"]["date"] = "2026-12-31"
|
||||
self.assertNotIn("SI-11", result_of(declaration(legacy))["rules"])
|
||||
|
|
@ -148,11 +179,12 @@ class AgentTests(unittest.TestCase):
|
|||
agent = {"mode": "autonomous", "presents_principal_type": "agent", "max_token_ttl_seconds": 900,
|
||||
"boundary_policy": "agent-high-risk-boundary"}
|
||||
agent.update(agent_over)
|
||||
return identity(name="coding-agent", classes=["agent"], agent=agent, privilege_level="read",
|
||||
custody=["none"], consumers=[{"name": "agent-host", "kind": "service"}])
|
||||
return identity(name="coding-agent", principal_type="AGENT", conventions=[], agent=agent,
|
||||
grants=[grant("USE", "openbao")], custody=["none"],
|
||||
consumers=[{"name": "agent-host", "kind": "service"}])
|
||||
|
||||
def test_agent_class_needs_the_agent_object(self):
|
||||
doc = declaration(identity(name="a", classes=["agent"], custody=["none"]))
|
||||
def test_agent_principal_needs_the_agent_object(self):
|
||||
doc = declaration(identity(name="a", principal_type="AGENT", conventions=[], custody=["none"]))
|
||||
self.assertTrue(V.structural_errors(doc, SCHEMA))
|
||||
|
||||
def test_conformant_agent(self):
|
||||
|
|
@ -162,8 +194,9 @@ class AgentTests(unittest.TestCase):
|
|||
r = result_of(declaration(self.agent(presents_principal_type="service")))
|
||||
self.assertEqual((r["result"], r["rules"]), ("non-conformant", ["SI-14"]))
|
||||
|
||||
def test_agent_token_lifetime_above_the_ceiling_breaks_si14(self):
|
||||
self.assertIn("SI-14", result_of(declaration(self.agent(max_token_ttl_seconds=7200)))["rules"])
|
||||
def test_agent_token_lifetime_above_sixty_minutes_breaks_si14(self):
|
||||
self.assertIn("SI-14", result_of(declaration(self.agent(max_token_ttl_seconds=3601)))["rules"])
|
||||
self.assertNotIn("SI-14", result_of(declaration(self.agent(max_token_ttl_seconds=3600)))["rules"])
|
||||
|
||||
def test_unknown_agent_mode_is_unknown_not_a_pass(self):
|
||||
r = result_of(declaration(self.agent(mode="unknown")))
|
||||
|
|
@ -172,25 +205,25 @@ class AgentTests(unittest.TestCase):
|
|||
def test_agent_must_not_live_in_a_persons_safe_or_serve_a_person(self):
|
||||
doc = declaration({**self.agent(), "custody": ["personal-safe"],
|
||||
"consumers": [{"name": "p", "kind": "person"}]})
|
||||
self.assertLessEqual({"SI-9", "SI-2"}, set(result_of(doc)["rules"]))
|
||||
self.assertLessEqual({"SI-9", "SI-1"}, set(result_of(doc)["rules"]))
|
||||
|
||||
|
||||
class ResultTests(unittest.TestCase):
|
||||
def test_unknown_is_a_finding_for_every_class(self):
|
||||
for fact, value in (("privilege_level", "unknown"), ("custody", ["unknown"])):
|
||||
r = result_of(declaration(identity(classes=["workload"], name="client", **{fact: value})))
|
||||
self.assertEqual(r["result"], "unknown")
|
||||
self.assertEqual(r["unknown"], [fact])
|
||||
def test_unknown_is_a_finding_for_every_account(self):
|
||||
for over, fact in ((dict(grants=[grant("unknown")]), "grants"), (dict(custody=["unknown"]), "custody"),
|
||||
(dict(principal_type="unknown", conventions=[]), "principal_type")):
|
||||
r = result_of(declaration(identity(name="client", **{"conventions": [], **over})))
|
||||
self.assertEqual((r["result"], r["unknown"]), ("unknown", [fact]))
|
||||
|
||||
def test_violation_outranks_unknown(self):
|
||||
r = result_of(declaration(identity(privilege_level="unknown", interactive_login=True)))
|
||||
r = result_of(declaration(identity(custody=["unknown"], interactive_login=True)))
|
||||
self.assertEqual(r["result"], "non-conformant")
|
||||
|
||||
def test_planned_identity_is_not_counted_as_conformant(self):
|
||||
self.assertEqual(result_of(declaration(identity(status="planned")))["result"], "planned")
|
||||
|
||||
def test_live_exception_excepts_and_expired_one_does_not(self):
|
||||
doc = declaration(identity(privilege_level="admin"), exceptions=[exception()])
|
||||
doc = declaration(identity(interactive_login=True), exceptions=[exception()])
|
||||
r = result_of(doc)
|
||||
self.assertEqual((r["result"], r["excepted"]), ("excepted", {"SI-3": "SI-X9"}))
|
||||
r = result_of(doc, as_of=dt.date(2027, 2, 1))
|
||||
|
|
@ -198,7 +231,7 @@ class ResultTests(unittest.TestCase):
|
|||
self.assertIn("SI-EXC-EXPIRED", r["rules"])
|
||||
|
||||
def test_exception_for_another_rule_does_not_cover_the_violation(self):
|
||||
doc = declaration(identity(privilege_level="admin"), exceptions=[exception(rules=["SI-9"])])
|
||||
doc = declaration(identity(interactive_login=True), exceptions=[exception(rules=["SI-9"])])
|
||||
self.assertEqual(result_of(doc)["result"], "non-conformant")
|
||||
|
||||
def test_stale_exception_and_unconfirmed_declaration_are_advisories(self):
|
||||
|
|
@ -209,7 +242,7 @@ class ResultTests(unittest.TestCase):
|
|||
self.assertTrue(any("not confirmed" in a for a in advisories))
|
||||
|
||||
def test_evaluation_date_is_explicit(self):
|
||||
doc = declaration(identity(privilege_level="admin"), exceptions=[exception(expires="2026-10-30")])
|
||||
doc = declaration(identity(interactive_login=True), exceptions=[exception(expires="2026-10-30")])
|
||||
self.assertEqual(result_of(doc, as_of=dt.date(2026, 10, 30))["result"], "excepted")
|
||||
self.assertEqual(result_of(doc, as_of=dt.date(2026, 10, 31))["result"], "non-conformant")
|
||||
|
||||
|
|
@ -236,7 +269,7 @@ class CheckedInDeclarations(unittest.TestCase):
|
|||
return [i for r in reports for i in r["identities"] if r["service"] == "lldap" and i["identity"] == "admin"][0]
|
||||
today = admin(NOW)
|
||||
self.assertEqual(today["result"], "excepted")
|
||||
self.assertEqual(set(today["excepted"]), {"SI-1", "SI-2", "SI-3", "SI-9", "NAMING"})
|
||||
self.assertEqual(set(today["excepted"]), {"SI-1", "SI-2", "SI-3", "SI-4", "SI-9", "NAMING"})
|
||||
after = admin(dt.date(2027, 2, 1))
|
||||
self.assertEqual(after["result"], "non-conformant")
|
||||
|
||||
|
|
@ -282,20 +315,21 @@ class CommandLineTests(unittest.TestCase):
|
|||
self.assertEqual(code, 1)
|
||||
self.assertIn("invalid --as-of", err)
|
||||
|
||||
def test_more_than_one_break_glass_identity_is_an_advisory(self):
|
||||
def bg(name):
|
||||
return declaration(identity(name=name, classes=["break-glass"], privilege_level="admin", interactive_login=True,
|
||||
def test_more_than_one_sovereign_account_is_an_advisory(self):
|
||||
def root(name):
|
||||
return declaration(identity(name=name, principal_type="HUMAN", conventions=[], interactive_login=True,
|
||||
second_factor="required", custody=["offline-key"],
|
||||
grants=[grant("SOVEREIGN", "platform", activation="BREAK_GLASS")],
|
||||
consumers=[{"name": "p", "kind": "person"}]))
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
paths = []
|
||||
for n in ("platform-root", "king"):
|
||||
p = pathlib.Path(d) / f"{n}.json"
|
||||
p.write_text(json.dumps(bg(n)))
|
||||
p.write_text(json.dumps(root(n)))
|
||||
paths.append(p)
|
||||
reports, rejected = V.run(paths, NOW)
|
||||
self.assertEqual(rejected, [])
|
||||
self.assertTrue(any("more than one break-glass" in a for r in reports for a in r["advisories"]))
|
||||
self.assertTrue(any("more than one SOVEREIGN" in a for r in reports for a in r["advisories"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"""Check service identity declarations against the proposed standard (NK-WP-0054-T03).
|
||||
|
||||
Validates `canon/schemas/service-identity_v0.1.schema.json`, then evaluates the rules of
|
||||
NetKingdom Service Identity and Privilege v0.1 that can be decided from a declaration alone.
|
||||
NetKingdom Service Identity and Privilege v0.1 (draft-3, a SecurityCanon profile) that a declaration alone can decide.
|
||||
Each identity ends as `conformant`, `non-conformant`, `excepted` or `unknown`; unknown is a
|
||||
finding, not a pass. Observed state is the health check's job (T04), not this tool's.
|
||||
|
||||
|
|
@ -93,43 +93,74 @@ def structural_errors(document: Any, schema: dict[str, Any]) -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
ADMIN_MODES = {"ADMINISTER", "GOVERN", "CONSTRUCT", "SOVEREIGN"} # not a ladder: kinds that change rules or definitions
|
||||
NON_HUMAN = {"SERVICE", "WORKLOAD", "AGENT", "CONTROLLER", "DEVICE"}
|
||||
|
||||
|
||||
def _violations(identity: dict[str, Any]) -> tuple[set[str], list[str]]:
|
||||
"""Rules broken by one identity on declared facts, and the facts still unknown."""
|
||||
"""Rules broken by one account on declared facts, and the facts still unknown (standard draft-3)."""
|
||||
broken: set[str] = set()
|
||||
unknown: list[str] = []
|
||||
classes = identity["classes"]
|
||||
principal = identity["principal_type"]
|
||||
conventions = identity.get("conventions", [])
|
||||
grants = identity["grants"]
|
||||
consumers = identity["consumers"]
|
||||
persons = [c for c in consumers if c["kind"] == "person"]
|
||||
services = [c for c in consumers if c["kind"] == "service"]
|
||||
name = identity["name"]
|
||||
privilege, second, custody = identity["privilege_level"], identity["second_factor"], identity["custody"]
|
||||
second, custody = identity["second_factor"], identity["custody"]
|
||||
modes = {g["mode"] for g in grants}
|
||||
|
||||
if len(classes) != 1:
|
||||
# Unknown is a finding for every account: a fact nobody has established is not a pass.
|
||||
if principal == "unknown":
|
||||
unknown.append("principal_type")
|
||||
if "unknown" in modes:
|
||||
unknown.append("grants")
|
||||
if "unknown" in custody:
|
||||
unknown.append("custody")
|
||||
|
||||
# SI-1 one principal type, one purpose; SI-2 people and services never share a credential.
|
||||
if len(conventions) > 1 or (principal == "HUMAN" and services) or (principal in NON_HUMAN and persons):
|
||||
broken.add("SI-1")
|
||||
if (persons and services) or ("human" in classes and services):
|
||||
if persons and services:
|
||||
broken.add("SI-2")
|
||||
if len(services) > 1:
|
||||
broken.add("SI-3") # a credential used by more than one consumer
|
||||
# Unknown is a finding for every class: a fact nobody has established is not a pass.
|
||||
if privilege == "unknown":
|
||||
unknown.append("privilege_level")
|
||||
if "unknown" in custody:
|
||||
unknown.append("custody")
|
||||
if "bind" in classes:
|
||||
if privilege not in ("read", "unknown"):
|
||||
# SI-3 bind accounts: SERVICE, read-only USE, one consumer, never interactive.
|
||||
if "bind" in conventions:
|
||||
reads_only = all(g["mode"] == "USE" and g.get("read_only") is True for g in grants)
|
||||
if principal != "SERVICE" or not reads_only or identity["interactive_login"] or persons or len(services) != 1:
|
||||
broken.add("SI-3")
|
||||
if identity["interactive_login"] or persons or len(services) != 1:
|
||||
broken.add("SI-3")
|
||||
if "human" in classes or "service-admin" in classes:
|
||||
# SI-4 a service-admin account holds ADMINISTER over one service only.
|
||||
if "service-admin" in conventions:
|
||||
if {g["mode"] for g in grants} - {"ADMINISTER", "unknown"} or len({g["scope"] for g in grants}) != 1:
|
||||
broken.add("SI-4")
|
||||
# SI-6 SOVEREIGN is offline and break-glass in production; break-glass serves no automation.
|
||||
for grant in grants:
|
||||
prod = "PRODUCTION" in grant.get("environments", [])
|
||||
if grant["mode"] == "SOVEREIGN":
|
||||
if "unknown" not in custody and custody != ["offline-key"]:
|
||||
broken.add("SI-6")
|
||||
if prod and grant.get("activation") not in ("BREAK_GLASS", "RECOVERY"):
|
||||
broken.add("SI-6")
|
||||
if grant.get("activation") == "BREAK_GLASS" and services:
|
||||
broken.add("SI-6")
|
||||
# SI-7 CONSTRUCT (build mode, development) is never available in production.
|
||||
if grant["mode"] == "CONSTRUCT" and prod:
|
||||
broken.add("SI-7")
|
||||
# SI-8 assurance: people and service admins need a second factor; admin roles bind the claim.
|
||||
if principal == "HUMAN" or "service-admin" in conventions:
|
||||
if second == "unknown":
|
||||
unknown.append("second_factor")
|
||||
elif second == "not_enforced":
|
||||
broken.add("SI-8")
|
||||
if identity.get("grants_admin_policy") and not identity.get("assurance_claim_bound"):
|
||||
if identity.get("federated_role") and modes & ADMIN_MODES and not identity.get("assurance_claim_bound"):
|
||||
broken.add("SI-8")
|
||||
if ("bind" in classes or "workload" in classes or "agent" in classes) and "personal-safe" in custody:
|
||||
broken.add("SI-9") # a person's safe must not be the home of a service or agent credential
|
||||
if "agent" in classes:
|
||||
# SI-9 a person's safe never holds a service, workload or agent credential.
|
||||
if (principal in NON_HUMAN or "bind" in conventions) and "personal-safe" in custody:
|
||||
broken.add("SI-9")
|
||||
# SI-14 agents present as agents, with short-lived tokens.
|
||||
if principal == "AGENT":
|
||||
agent = identity["agent"]
|
||||
if agent["presents_principal_type"] == "unknown":
|
||||
unknown.append("agent.presents_principal_type")
|
||||
|
|
@ -139,28 +170,21 @@ def _violations(identity: dict[str, Any]) -> tuple[set[str], list[str]]:
|
|||
unknown.append("agent.mode")
|
||||
if agent["max_token_ttl_seconds"] > AGENT_MAX_TTL_SECONDS:
|
||||
broken.add("SI-14")
|
||||
if persons:
|
||||
broken.add("SI-2")
|
||||
if "break-glass" in classes:
|
||||
if "unknown" not in custody and custody != ["offline-key"]:
|
||||
broken.add("SI-6")
|
||||
if services:
|
||||
broken.add("SI-6")
|
||||
# SI-12 recovery accounts are in the crisis custody list.
|
||||
recovery = identity.get("recovery")
|
||||
if recovery and recovery["needed"]:
|
||||
if not recovery["in_crisis_list"]:
|
||||
broken.add("SI-12")
|
||||
if recovery["works_with_sso_down"] == "unknown":
|
||||
unknown.append("recovery.works_with_sso_down")
|
||||
# Naming (section 4). Reserved names belong to their class.
|
||||
reserved_bind, reserved_admin = BIND_NAME.match(name), ADMIN_NAME.match(name)
|
||||
if "bind" in classes and not reserved_bind:
|
||||
# Naming (section 5). Reserved names belong to their convention; `admin` is the human's name.
|
||||
if "bind" in conventions and not BIND_NAME.match(name):
|
||||
broken.add("NAMING")
|
||||
if "service-admin" in classes and not reserved_admin:
|
||||
if "service-admin" in conventions and not ADMIN_NAME.match(name):
|
||||
broken.add("NAMING")
|
||||
holds_credential = custody != ["none"]
|
||||
if "human" in classes and holds_credential and (name.startswith("svc-") or name.endswith("-admin")):
|
||||
broken.add("NAMING") # svc- and -admin names are reserved for their classes; a federated role holds no credential
|
||||
if principal == "HUMAN" and holds_credential and not conventions and (name.startswith("svc-") or name.endswith("-admin")):
|
||||
broken.add("NAMING")
|
||||
return broken, sorted(set(unknown))
|
||||
|
||||
|
||||
|
|
@ -204,7 +228,7 @@ def evaluate(document: dict[str, Any], as_of: dt.date) -> dict[str, Any]:
|
|||
if exception["id"] not in used and as_of <= parse_date(exception["expires"]):
|
||||
advisories.append(f"{exception['id']}: no matching violation (stale exception)")
|
||||
admins = [i["name"] for i in document["identities"]
|
||||
if "service-admin" in i["classes"] and i["status"] != "planned"]
|
||||
if "service-admin" in i.get("conventions", []) and i["status"] != "planned"]
|
||||
if len(admins) > 1:
|
||||
advisories.append("SI-4: more than one service-admin identity: " + ", ".join(sorted(admins)))
|
||||
if not document["complete"]:
|
||||
|
|
@ -231,10 +255,11 @@ def run(paths: list[pathlib.Path], as_of: dt.date) -> tuple[list[dict[str, Any]]
|
|||
rejected.append(f"{path.name}: rejected ({len(errors)} errors)")
|
||||
continue
|
||||
reports.append(evaluate(document, as_of))
|
||||
breakglass |= {i["name"] for i in document["identities"] if "break-glass" in i["classes"]}
|
||||
breakglass |= {i["name"] for i in document["identities"]
|
||||
if any(g["mode"] == "SOVEREIGN" for g in i["grants"])}
|
||||
if len(breakglass) > 1:
|
||||
reports.append({"service": "<run>", "as_of": as_of.isoformat(), "identities": [],
|
||||
"advisories": ["SI-6: more than one break-glass identity across declarations: "
|
||||
"advisories": ["SI-6: more than one SOVEREIGN account across declarations: "
|
||||
+ ", ".join(sorted(breakglass))]})
|
||||
return reports, rejected
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ SI-10 and SI-13 are not decidable from a declaration.
|
|||
|
||||
```task
|
||||
id: NK-WP-0054-T03b
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
assignee: claude-code
|
||||
depends_on: [NK-WP-0054-T03]
|
||||
|
|
@ -158,6 +158,23 @@ same non-conformant, excepted and unknown accounts unless a difference is explai
|
|||
**Done when:** the schema expresses principal type and grants, the declarations follow §3 of the standard,
|
||||
the tests pass, and a before/after comparison is recorded.
|
||||
|
||||
Completed 2026-10-24. Schema, validator, fixtures, declarations and tests (40 pass) follow draft-3: imported
|
||||
`principal_type`, grants `MODE @ SCOPE` with activation and environments, estate conventions `bind` and
|
||||
`service-admin`, `unknown` placeholders, and an `elimination_path` on every exception. SI-4 and SI-7 became
|
||||
decidable. Before/after, as of 2026-10-24 over the same eight declarations:
|
||||
|
||||
| Result | Draft-2 fields | Draft-3 grants |
|
||||
| --- | --- | --- |
|
||||
| non-conformant | 1 | 1 (`openbao/coding-agent`, SI-14) |
|
||||
| unknown | 11 | 10 |
|
||||
| excepted | 2 | 2 (`lldap/admin` SI-X1, `openbao/netkingdom-platform-admin` SI-X4) |
|
||||
| planned | 5 | 5 |
|
||||
| conformant | 6 | 7 |
|
||||
|
||||
The only account that moved is KeyCape's `secrets-engine-openbao` client, unknown to conformant: its scope
|
||||
(`openbao:login` only) is established, so it can be stated as a grant instead of an unknown privilege level.
|
||||
`lldap/admin` is now also excepted under SI-4, which grants made decidable; SI-X1 lists it.
|
||||
|
||||
## Live conformance health check
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue