NK-WP-0048: canon accepted; SecurityCaseType schemas, validator and template (T01, T03)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s

security-case-types_v0.1 accepted by the founder 2026-10-08. Adds JSON schemas
for types and cases, tools/security-case-types/security_case_validator.py
(validate-type, validate-case, render via the scenario composer, plan-only),
and a runnable _template type with README sections, an example case and tests.

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
This commit is contained in:
tegwick 2026-10-08 22:29:13 +02:00
parent dcacdf57f3
commit d8cf42bab6
11 changed files with 1122 additions and 7 deletions

View file

@ -0,0 +1,40 @@
# SecurityCaseType template
Copy `_template/` to `security-case-types/<type-id>/` and rewrite every
section. Canon: `canon/standards/security-case-types_v0.1.md`. A type is
acknowledged through informed-decision only when this README, `type.yaml`, the
example cases and the tests are complete and green.
## Purpose
Which recurring use case the type serves, and the hand-built instances it
replaces or adopts (reference instances). For the template: a loopback bootstrap
identity for one operator, composed from `net-kingdom.local-identity`.
## Threat model
What the type protects, from whom, and how. Name the assets (credentials,
sessions, signing keys), the actors (anonymous, low-assurance operator, tenant,
compromised workload) and what each step does to prevent misuse. For the
template: the store is loopback-only and private to its owner; the signing key
is referenced from custody, never written into a case.
## Alternatives considered
The other shapes this pattern could have taken and why they were not chosen.
This is the part an informed-decision is mostly about.
## Attended steps
Every step marked `attended: true`, why it cannot run unattended, and how
ops-warden groups such steps into as few attended sessions as possible.
## Known limits
What the type does not cover, and when a request should become a new type or a
new major version instead of a case.
## Verification and rollback
How the checks in `type.yaml` prove a case is live without reading any value,
and how rollback restores the state from before the case.

View file

@ -0,0 +1,15 @@
# Example SecurityCase for the template type. Example values only.
apiVersion: netkingdom.io/security-case/v0.1
kind: SecurityCase
spec:
id: example-operator
type: template-example@0.1
status: proposed
owner:
repo: net-kingdom
person: example-owner
parameters:
operator_username: bootstrap-admin
operator_fullname: Bootstrap Reference Operator
operator_email: bootstrap@example.invalid
signing_key_ref: bao:platform/workloads/example/local-identity-signing

View file

@ -0,0 +1,53 @@
"""Tests every SecurityCaseType carries: the type validates, its example cases
validate against it, and each example renders into a composer plan.
Copied with the template; keep it and add type-specific tests beside it.
"""
import importlib.util
import sys
from pathlib import Path
import yaml
TYPE_DIR = Path(__file__).resolve().parents[1]
REPO = TYPE_DIR.parents[1]
VALIDATOR = REPO / "tools" / "security-case-types" / "security_case_validator.py"
SPEC = importlib.util.spec_from_file_location("security_case_validator", VALIDATOR)
validator = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = validator
SPEC.loader.exec_module(validator)
TYPE = yaml.safe_load((TYPE_DIR / "type.yaml").read_text())
EXAMPLES = sorted((TYPE_DIR / "examples").glob("*.yaml"))
DECLARATIONS = sorted(str(p) for p in (REPO / "capabilities" / "playbooks").glob("*.yaml"))
# Until the type is implemented, examples are checked as if it were.
IMPLEMENTED = TYPE["spec"]["status"] == "implemented"
def errors(issues):
return [f"{i.path}: {i.message}" for i in issues if i.level == "ERROR"]
def test_type_is_complete():
assert errors(validator.validate_type(TYPE, TYPE_DIR)) == []
def test_examples_exist_and_validate():
assert EXAMPLES
for path in EXAMPLES:
case = yaml.safe_load(path.read_text())
assert errors(validator.validate_case(case, TYPE, require_implemented=IMPLEMENTED)) == [], path.name
def test_examples_render_deterministically():
declarations, load_issues = validator.composer.load_declarations(DECLARATIONS)
assert errors(load_issues) == []
for path in EXAMPLES:
case = yaml.safe_load(path.read_text())
issues, first = validator.render_case(case, TYPE, declarations, require_implemented=IMPLEMENTED)
assert errors(issues) == [], path.name
_, second = validator.render_case(case, TYPE, declarations, require_implemented=IMPLEMENTED)
assert first["digest"] == second["digest"]
assert first["execution"] == {"permitted": False}
assert first["composition"]["execution"]["permitted"] is False

View file

@ -0,0 +1,115 @@
# SecurityCaseType template (canon/standards/security-case-types_v0.1.md).
#
# Copy this directory to security-case-types/<type-id>/ and replace every part.
# This template is itself a valid, runnable type: it composes the checked-in
# net-kingdom.local-identity declaration (a C0 bootstrap identity), so that its
# tests prove the validator and renderer end to end. It is not for real cases.
apiVersion: netkingdom.io/security-case-type/v0.1
kind: SecurityCaseType
spec:
id: template-example
version: "0.1"
status: draft
title: "Template: local bootstrap identity for one operator"
purpose: >-
Shows every section a type must define, using a loopback bootstrap identity
as the example. A real type states which recurring use case it serves and
which hand-built instances it replaces.
# What a case fills in. Secret values are never parameters: a secret appears
# only as a custody reference (sensitivity: secret_reference).
parameters:
- name: operator_username
type: string
case_settable: true
constraints: {pattern: "^[a-z_][a-z0-9_-]{0,31}$"}
sensitivity: operational
description: Username of the one operator this case is for.
- name: operator_fullname
type: string
case_settable: true
constraints: {pattern: "^[^\\n\\r]{1,120}$"}
sensitivity: operational
description: Display name of the operator.
- name: operator_email
type: string
case_settable: true
constraints: {pattern: "^[^@\\s]+@[^@\\s]+$"}
sensitivity: operational
description: Contact address of the operator.
- name: signing_key_ref
type: string
case_settable: true
constraints: {pattern: "^bao:platform/workloads/[a-z0-9/-]+$"}
sensitivity: secret_reference
description: Custody reference for the signing key; never the key itself.
- name: store_home
type: string
case_settable: false
default: .local/local-identity
sensitivity: operational
description: Where the store lives; fixed by the type, not by cases.
# Which declared playbook capabilities the type composes, with exact provider
# pins, and how case parameters bind to the declarations' parameters.
composition:
authority: netkingdom
initial_trust: [bare_host_trust]
capabilities: [c0.bootstrap-identity]
providers:
c0.bootstrap-identity: net-kingdom.local-identity
parameter_bindings:
net-kingdom.local-identity.bootstrap_username: operator_username
net-kingdom.local-identity.bootstrap_fullname: operator_fullname
net-kingdom.local-identity.bootstrap_email: operator_email
net-kingdom.local-identity.local_identity_home: store_home
# What ops-mason builds per case (structure only, never values).
materialization:
- id: signing-key-path
kind: kv_path
template: "{signing_key_ref}"
- id: signing-key-policy
kind: openbao_policy
template: "local-identity-{operator_username}-read"
- id: catalog-entry
kind: catalog_entry
template: "local-identity-{operator_username}"
# What ops-warden carries out, step by step, on whose entry point.
follow_through:
- id: init-store
owner: net-kingdom
entry_point: "uv run --project local-identity local-identity init"
auth: "local operator shell on the target host"
warden_executes: false
attended: true
attended_justification: >-
The bootstrap identity exists before any managed identity, so no
unattended principal can run it.
# Checks that prove the case is live; never reading a secret value.
verification:
- id: discovery-serves
check: "loopback discovery document issuer matches the planned port"
expect: present
reads_secret_value: false
- id: store-private
check: "store under {store_home} is not readable by other users"
expect: deny
reads_secret_value: false
# The exact inverse of every follow-through step and materialized item.
rollback:
- reverses: init-store
action: "stop the loopback issuer and remove {store_home}"
- reverses: signing-key-path
action: "remove the KV path {signing_key_ref}"
- reverses: signing-key-policy
action: "delete policy local-identity-{operator_username}-read"
- reverses: catalog-entry
action: "remove catalog entry local-identity-{operator_username}"
acceptance:
accepted_by: [owner]
risk_class: low