Compare commits
5 commits
3a01b8f1b5
...
3aedd8f844
| Author | SHA1 | Date | |
|---|---|---|---|
| 3aedd8f844 | |||
| 1e3eb8df2a | |||
| 83fdd08f88 | |||
| 8e1b621116 | |||
| df48ee96e0 |
11 changed files with 637 additions and 10 deletions
119
history/2026-09-09-layer-model-v08-review.md
Normal file
119
history/2026-09-09-layer-model-v08-review.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# security-layer-model v0.8 — ops-warden review
|
||||
|
||||
**Date:** 2026-09-09
|
||||
**Reviewing:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (status
|
||||
`proposed`, net-kingdom@66eeaba), circulated for assent by gate-house
|
||||
**Prior reviews:** `2026-08-29-layer-model-v04-review.md`, `-v06-review.md`,
|
||||
`-v07-scope-intent-assessment.md`
|
||||
|
||||
## The cell in question
|
||||
|
||||
v0.8 §6.4 obligation 3 adds: *unknown is not a zone and MUST resolve to
|
||||
`fail_closed`.* `pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`.
|
||||
§13.1 records the cell as non-conformant.
|
||||
|
||||
## Finding 1 — assent. The rule is right and our falsifier check fails.
|
||||
|
||||
gate-house wrote the falsifier into GH-DEC-2026-009's reversal: a scope that is
|
||||
genuinely unknown **and** genuinely low-consequence, where failing closed costs
|
||||
availability with no security gain. Their expectation is that any such path is a
|
||||
§5.1 read-only diagnostic carrying no PEP obligation at all.
|
||||
|
||||
We looked for one on our side and do not have it. The stance map governs exactly
|
||||
one protected action — `warden sign`, SSH certificate issuance. That is a
|
||||
credential-issuing side effect, not a diagnostic; §5.1 does not reach it. So the
|
||||
predicted escape hatch is absent here, and the argument stands unrebutted on its
|
||||
own terms:
|
||||
|
||||
- §9.3 licenses trading availability for openness **per zone**, knowingly, for a
|
||||
named scope. Where the scope is unknown that trade was never made for the
|
||||
request in hand, so resolving permissively does not extend a considered
|
||||
decision — it invents one.
|
||||
- Unknown is the cheapest state to induce. Our own `unknown` reasons are
|
||||
`workload_resolution_absent` and `catalog_applicability_absent` — an
|
||||
unregistered workload or a missing declaration, neither of which requires a
|
||||
credential to arrange.
|
||||
|
||||
We accept that being unclassifiable must not buy permissiveness. The cell is
|
||||
wrong as doctrine.
|
||||
|
||||
## Finding 2 — the rule is right and adopting it today would be a global fail-closed flag in all but name
|
||||
|
||||
This is the finding we would like recorded, and it is about adoption cost, not
|
||||
about the rule.
|
||||
|
||||
The stance map applies only when the engine is unreachable, so the cell's bite is
|
||||
proportional to how much of the target population sits in `unknown`. Measured
|
||||
today:
|
||||
|
||||
| Population | Resolved to a zone | `unknown` | `not-applicable` |
|
||||
| --- | --- | --- | --- |
|
||||
| Signing targets (actor resources in the flex-auth snapshot) | **0** | **3** | 1 |
|
||||
| Routing catalog lanes | 3 | 18 | 12 |
|
||||
|
||||
Every signing target that resolves at all resolves to `unknown`. So
|
||||
`unknown: fail_closed` adopted today does not fail closed on an edge case; it
|
||||
fails closed on **essentially every certificate ops-warden issues** whenever
|
||||
flex-auth is unreachable.
|
||||
|
||||
That is the precise configuration `ADR-0006` rejected and `ADR-0009` replaced: one
|
||||
value making the decision engine a uniform dependency of every signing path,
|
||||
*including the continuity paths needed to repair that dependency*. If flex-auth is
|
||||
unreachable and the operator needs an SSH certificate to reach the host and
|
||||
restore it, `unknown: fail_closed` denies the certificate — and the target is
|
||||
`unknown` precisely because nobody has classified the repair path.
|
||||
|
||||
The zone model already answers this: classify continuity paths `z2-continuity`,
|
||||
whose stance is `fail_open`, and the deadlock does not arise. We accept that as
|
||||
the right answer. The point is that it is *work not yet done*, and the reason it
|
||||
is not done is not ours to fix — 18 of our 18 unknown lanes are unknown because
|
||||
another repo has not published an authoritative workload identity declaration.
|
||||
`ADR-0009` rule 3 forbids us from closing that gap with inference, correctly.
|
||||
|
||||
So as written, v0.8 §6.4 obligation 3 transfers an availability cost onto the
|
||||
consumer for a classification gap the consumer cannot close alone, and it does so
|
||||
at the moment of assent rather than at the moment the gap closes.
|
||||
|
||||
**What we ask for**, in preference order:
|
||||
|
||||
1. **§6.4 obligation 3 names a transition.** A consumer may declare
|
||||
`unknown: fail_open` as a dated, published transitional state with a
|
||||
classification-coverage figure attached, converting to `fail_closed` on
|
||||
coverage rather than on calendar. This keeps the doctrine and prices the
|
||||
adoption honestly.
|
||||
2. Failing that, **§13.1 records classification coverage alongside stance.** A
|
||||
register row reading `unknown: fail_closed` while 100% of that consumer's
|
||||
targets are unknown is conformant and materially misleading; a reader cannot
|
||||
tell a strict consumer from an unclassified one. This is the same defect
|
||||
§11's *published map equals shipped behaviour* rule exists to prevent, one
|
||||
level up: the map is now accurate about itself and inaccurate about its
|
||||
effect.
|
||||
|
||||
## Finding 3 — §12's derived-artifact rule, small and in agreement
|
||||
|
||||
Six instances in one week matches our experience. `registry/generated/high-risk-data-paths.yaml`
|
||||
carries `catalog_revision` and a `catalog_dirty` flag for exactly the reason §12
|
||||
now generalises: a derived artifact whose revision does not describe the file it
|
||||
came from is worse than one with no revision, because the stale one is trusted.
|
||||
No change requested; recording the agreement since the rule is being generalised
|
||||
from other repos' incidents.
|
||||
|
||||
## What ops-warden is doing about it
|
||||
|
||||
Not flipping the cell on a `proposed` standard, and not flipping it before
|
||||
coverage exists — that ordering is `ADR-0006`'s whole holding. `WARDEN-WP-0040`
|
||||
records the adoption sequence: raise signing-target classification coverage
|
||||
first, then convert the cell, with the deadlock case classified before rather
|
||||
than after. If v0.8 is accepted with obligation 3 unchanged, ops-warden's cell
|
||||
remains a **declared gap** under §11 — tracked non-conformance with a named
|
||||
owner and a route, which is the mark §9.1 exists for and which we argued for in
|
||||
the v0.6 round.
|
||||
|
||||
## Axis declaration (second change touching us)
|
||||
|
||||
`pep-stance.yaml` already carries `scope: security-zone`. No change needed. We
|
||||
agree with rejecting a forced migration onto zones — secrets-engine scoping by
|
||||
catalog stage is an accurate statement about their control, and making them
|
||||
assert a zone they cannot know would trade a real fact for a conformant fiction.
|
||||
The unavailability of cross-axis aggregation is better recorded than papered
|
||||
over, and §13.1 now says so.
|
||||
|
|
@ -10,10 +10,10 @@
|
|||
# declares it, and is null where the field set has not been established --
|
||||
# null means unknown, never 'one field'.
|
||||
|
||||
generated_at: "2026-09-08T12:55:03Z"
|
||||
generated_at: "2026-09-09T12:43:48Z"
|
||||
source: ops-warden/registry/routing/catalog.yaml
|
||||
catalog_revision: "c1330049d9535f701676aa955d56f2138bb70d19"
|
||||
catalog_revision_date: "2026-09-08T14:55:02+02:00"
|
||||
catalog_revision: "83fdd08f881ce6a5bd9c3fbcb4dc79d1ab62b4d5"
|
||||
catalog_revision_date: "2026-09-09T14:43:44+02:00"
|
||||
catalog_dirty: false
|
||||
high_risk_lane_count: 24
|
||||
concrete_path_count: 15
|
||||
|
|
|
|||
|
|
@ -241,7 +241,15 @@ entries:
|
|||
# form is superseded; do not reintroduce it.
|
||||
auth_method: "bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read"
|
||||
path_template: "platform/workloads/coulomb/whynot-design/npm-publish"
|
||||
fetch_command: "bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish"
|
||||
# Field corrected 2026-09-09 on the owner's statement (secrets-engine msg
|
||||
# 15f0c0ca): `npm_token` is the KV FIELD; `NPM_AUTH_TOKEN` is the environment
|
||||
# variable the publication-scope policy injects, and ops-warden had copied the
|
||||
# env var in as the field name. That is ADR-0001's failure mode exactly — a
|
||||
# pointer layer restating an owner's procedure and getting it wrong — so it is
|
||||
# corrected from the owner's doc (secrets-engine docs/whynot-design-real-publish-closeout.md),
|
||||
# never re-derived here. The path is a separate question, open with
|
||||
# railiance-platform; do not change it on this correction.
|
||||
fetch_command: "bao kv get -field=npm_token platform/workloads/coulomb/whynot-design/npm-publish"
|
||||
policy_ref: "flex-auth check secret.read:whynot-design"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
|
|
@ -257,7 +265,7 @@ entries:
|
|||
automatable: false
|
||||
steps:
|
||||
- "In the coulomb Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity."
|
||||
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file)."
|
||||
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish npm_token=@file` (value from a mode-0600 file). The field is `npm_token`; `NPM_AUTH_TOKEN` is the injected env var, not a KV key."
|
||||
- "Verify capabilities-safe, then publish a fresh version and confirm it with Forgejo-supported `npm view <package>@<version>` through the governed execution lane (value used, not printed)."
|
||||
|
||||
- id: policy-nexus-forgejo-source-read
|
||||
|
|
|
|||
161
scripts/check_flex_auth_manifest_coverage.py
Normal file
161
scripts/check_flex_auth_manifest_coverage.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Assert every ssh-certificate resource declares its ceilings in the manifest.
|
||||
|
||||
Read-only. Reads `inventory.yaml` and the flex-auth registry snapshot; touches
|
||||
no network, no OpenBao, and no secret material.
|
||||
|
||||
Why this exists (FLEX-DEC-2026-012, 2026-09-07). flex-auth's enrichment used to
|
||||
overlay registry facts additive-if-absent, so a caller-supplied value for a key
|
||||
won and the registry's ceiling never applied. That is fixed on their side:
|
||||
registry facts now win. But they win only where the registry HAS a value —
|
||||
|
||||
"A resource whose manifest omits max_ttl_hours hands that ceiling back to
|
||||
the caller, and registering the resource is not sufficient; the specific
|
||||
key must be present."
|
||||
|
||||
So there are two ways to hand a ceiling back, and this check covers both:
|
||||
|
||||
1. An actor with no manifest resource at all. `warden sign` names
|
||||
`ssh-cert:actor/<name>` for any actor in inventory, whether or not the
|
||||
snapshot was regenerated. Adding an actor and forgetting to rebuild is an
|
||||
honour-system step in `SCOPE.md`, and honour-system steps are what
|
||||
`ADR-0004`'s WARDEN_AGENT_ID marker taught us to stop relying on.
|
||||
2. A manifest resource missing one of the ceiling or allowlist keys. The
|
||||
builder emits all of them today; this asserts it stays true, including for
|
||||
resources added by hand or by a future code path.
|
||||
|
||||
ops-warden sends no `resource.attributes` on a CheckRequest (`src/warden/policy.py`),
|
||||
which is why FLEX-DEC-2026-012 was defence-in-depth rather than a live hole for
|
||||
this repo. That property is asserted separately in `tests/test_policy.py`; this
|
||||
check covers the half that survives it — if anything ever does reach a request
|
||||
field, the ceiling it would have to beat must actually exist.
|
||||
|
||||
Usage:
|
||||
python scripts/check_flex_auth_manifest_coverage.py \
|
||||
--inventory examples/inventory.seed.yaml \
|
||||
--registry registry/flex-auth/production_registry_snapshot.json [--json]
|
||||
|
||||
Exit: 0 covered, 2 coverage gap, 1 usage/IO error.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
#: Every key ops-warden's shipped policy package branches on as a ceiling or an
|
||||
#: allowlist. Named by flex-auth in FLEX-DEC-2026-012; kept here rather than
|
||||
#: derived from the package so a package edit that drops a branch does not
|
||||
#: silently shrink what this check requires.
|
||||
REQUIRED_CEILING_KEYS = (
|
||||
"actor_id",
|
||||
"actor_type",
|
||||
"allowed_principals",
|
||||
"allowed_subjects",
|
||||
"max_ttl_hours",
|
||||
"security_zone",
|
||||
"security_zone_admission",
|
||||
)
|
||||
|
||||
RESOURCE_TYPE = "ssh-certificate"
|
||||
|
||||
|
||||
def _resources(registry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
for manifest in registry.get("resource_manifests") or []:
|
||||
out.extend(manifest.get("resources") or [])
|
||||
return out
|
||||
|
||||
|
||||
def audit(inventory: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]:
|
||||
actors = sorted((inventory.get("actors") or {}).keys())
|
||||
resources = _resources(registry)
|
||||
by_id = {str(r.get("id")): r for r in resources}
|
||||
|
||||
unregistered: list[str] = []
|
||||
missing_keys: list[dict[str, Any]] = []
|
||||
|
||||
for name in actors:
|
||||
if f"ssh-cert:actor/{name}" not in by_id:
|
||||
unregistered.append(name)
|
||||
|
||||
# Check every ssh-certificate resource in the manifest, not only the ones an
|
||||
# inventory actor maps to: a resource flex-auth can be asked about is one it
|
||||
# holds, regardless of where it came from.
|
||||
for resource in resources:
|
||||
if str(resource.get("type")) != RESOURCE_TYPE:
|
||||
continue
|
||||
attributes = resource.get("attributes") or {}
|
||||
absent = [
|
||||
key for key in REQUIRED_CEILING_KEYS
|
||||
# A null is not a declaration: flex-auth overlays a registry value
|
||||
# only where one exists, so `key: null` hands the ceiling back
|
||||
# exactly as an absent key does.
|
||||
if attributes.get(key) is None
|
||||
]
|
||||
if absent:
|
||||
missing_keys.append({"resource": str(resource.get("id")), "missing": absent})
|
||||
|
||||
# Not a security defect — a stale resource has ceilings, it just has no
|
||||
# actor. Reported so drift is visible rather than accumulating silently.
|
||||
orphaned = sorted(
|
||||
rid for rid, r in by_id.items()
|
||||
if str(r.get("type")) == RESOURCE_TYPE
|
||||
and rid.removeprefix("ssh-cert:actor/") not in actors
|
||||
)
|
||||
|
||||
return {
|
||||
"actors": len(actors),
|
||||
"resources": sum(1 for r in resources if str(r.get("type")) == RESOURCE_TYPE),
|
||||
"required_keys": list(REQUIRED_CEILING_KEYS),
|
||||
"unregistered_actors": unregistered,
|
||||
"resources_missing_keys": missing_keys,
|
||||
"orphaned_resources": orphaned,
|
||||
"covered": not unregistered and not missing_keys,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--inventory", default="examples/inventory.seed.yaml", type=Path)
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
default="registry/flex-auth/production_registry_snapshot.json",
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", dest="as_json")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
inventory = yaml.safe_load(args.inventory.read_text()) or {}
|
||||
registry = json.loads(args.registry.read_text())
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
report = audit(inventory, registry)
|
||||
report["inventory"] = str(args.inventory)
|
||||
report["registry"] = str(args.registry)
|
||||
|
||||
if args.as_json:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
print(f"inventory : {args.inventory} ({report['actors']} actors)")
|
||||
print(f"registry : {args.registry} ({report['resources']} ssh-certificate resources)")
|
||||
for name in report["unregistered_actors"]:
|
||||
print(f" MISSING {name} — no manifest resource; every ceiling is caller-supplied")
|
||||
for row in report["resources_missing_keys"]:
|
||||
print(f" UNDECLARED {row['resource']} — {', '.join(row['missing'])}")
|
||||
for rid in report["orphaned_resources"]:
|
||||
print(f" orphaned {rid} — manifest resource with no inventory actor")
|
||||
print("covered" if report["covered"] else "NOT COVERED")
|
||||
|
||||
return 0 if report["covered"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -81,3 +81,86 @@ def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
|
|||
assert attrs["security_zone"] == "z2-continuity"
|
||||
assert attrs["security_zone_admission"] == "satisfied"
|
||||
assert attrs["security_zone_revision"] == "sha256:zone-revision"
|
||||
|
||||
|
||||
# --- manifest ceiling coverage (FLEX-DEC-2026-012) ----------------------------
|
||||
#
|
||||
# flex-auth fixed enrichment so registry facts beat caller-supplied ones, then
|
||||
# asked every consumer to confirm the ceiling keys are actually declared: the
|
||||
# fix wins only where the registry HAS a value. These assert the two ways a
|
||||
# ceiling gets handed back to the caller.
|
||||
|
||||
import importlib.util # noqa: E402
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
_REPO = ROOT
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"check_flex_auth_manifest_coverage",
|
||||
_REPO / "scripts" / "check_flex_auth_manifest_coverage.py",
|
||||
)
|
||||
_coverage = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_coverage)
|
||||
REQUIRED_CEILING_KEYS = _coverage.REQUIRED_CEILING_KEYS
|
||||
audit = _coverage.audit
|
||||
|
||||
_build_spec = importlib.util.spec_from_file_location("build_flex_auth_registry", SCRIPT)
|
||||
_builder = importlib.util.module_from_spec(_build_spec)
|
||||
_build_spec.loader.exec_module(_builder)
|
||||
build_registry = _builder.build_registry
|
||||
|
||||
|
||||
def _repo_inventory_and_registry():
|
||||
inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text())
|
||||
registry = json.loads(
|
||||
(_REPO / "registry" / "flex-auth" / "production_registry_snapshot.json").read_text()
|
||||
)
|
||||
return inventory, registry
|
||||
|
||||
|
||||
def test_shipped_snapshot_declares_every_ceiling_key():
|
||||
"""The confirmation flex-auth asked for, as a test rather than a reading."""
|
||||
report = audit(*_repo_inventory_and_registry())
|
||||
assert report["unregistered_actors"] == []
|
||||
assert report["resources_missing_keys"] == []
|
||||
assert report["covered"] is True
|
||||
|
||||
|
||||
def test_actor_with_no_manifest_resource_is_a_coverage_gap():
|
||||
"""`warden sign` names ssh-cert:actor/<name> whether or not the snapshot was rebuilt."""
|
||||
inventory, registry = _repo_inventory_and_registry()
|
||||
inventory["actors"]["adm-added-after-the-last-build"] = {
|
||||
"type": "adm", "principals": ["adm-full"], "ttl_hours": 48,
|
||||
}
|
||||
report = audit(inventory, registry)
|
||||
assert report["unregistered_actors"] == ["adm-added-after-the-last-build"]
|
||||
assert report["covered"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS)
|
||||
def test_dropping_any_single_ceiling_key_is_a_coverage_gap(key):
|
||||
inventory, registry = _repo_inventory_and_registry()
|
||||
resource = registry["resource_manifests"][0]["resources"][0]
|
||||
resource["attributes"].pop(key)
|
||||
report = audit(inventory, registry)
|
||||
assert report["resources_missing_keys"] == [
|
||||
{"resource": resource["id"], "missing": [key]}
|
||||
]
|
||||
assert report["covered"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS)
|
||||
def test_a_null_ceiling_is_not_a_declaration(key):
|
||||
"""flex-auth overlays a registry value only where one exists."""
|
||||
inventory, registry = _repo_inventory_and_registry()
|
||||
resource = registry["resource_manifests"][0]["resources"][0]
|
||||
resource["attributes"][key] = None
|
||||
report = audit(inventory, registry)
|
||||
assert report["covered"] is False
|
||||
|
||||
|
||||
def test_builder_output_is_covered_by_construction(tmp_path):
|
||||
"""Every resource the builder emits carries the full ceiling set."""
|
||||
inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text())
|
||||
report = audit(inventory, build_registry(inventory))
|
||||
assert report["covered"] is True
|
||||
|
|
|
|||
|
|
@ -307,3 +307,47 @@ def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
|
|||
assert check_sign_policy(cfg, spec) == "decision:advisory"
|
||||
assert spec.policy_zone == "unknown"
|
||||
assert spec.policy_outcome == "audit_only"
|
||||
|
||||
|
||||
def test_check_request_asserts_no_resource_attributes(tmp_path, monkeypatch):
|
||||
"""The property FLEX-DEC-2026-012 turned on, asserted rather than assumed.
|
||||
|
||||
flex-auth's enrichment used to overlay registry facts additive-if-absent, so
|
||||
a caller-supplied `resource.attributes` value won and the registry ceiling
|
||||
never applied. Their exploitability assessment rested on ops-warden sending
|
||||
no `resource.attributes` at all -- true when they read it, and nothing here
|
||||
held it true. secrets-engine sends them on every request, so this is a
|
||||
property of this code rather than of the protocol.
|
||||
|
||||
Ceilings must arrive from the registry. If a future change ever needs to send
|
||||
an attribute, this test is the place that argument gets made.
|
||||
"""
|
||||
from warden import policy as policy_mod
|
||||
|
||||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
seen = {}
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"effect": "allow", "id": "decision:49350f1064f674d7"}
|
||||
|
||||
def fake_post(url, json=None, headers=None, timeout=None):
|
||||
seen["body"] = json
|
||||
return _Response()
|
||||
|
||||
monkeypatch.setattr(policy_mod.httpx, "post", fake_post)
|
||||
policy_mod.check_sign_policy(cfg, _spec(pubkey))
|
||||
|
||||
assert "attributes" not in seen["body"]["resource"]
|
||||
# The requested TTL is a policy input (ttl_out_of_bounds is denied against
|
||||
# the registry ceiling), so it must travel as context, never as a resource
|
||||
# attribute that would be compared against itself.
|
||||
assert seen["body"]["context"]["ttl_hours"] == 24
|
||||
assert set(seen["body"]["resource"]) == {"id", "type", "system", "tenant"}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ Owner: `railiance-platform` (OpenBao) · provisioning CCR-2026-0001 (commit 8f61
|
|||
> plan-authorized Forgejo admin recovery lane. Treat this dedicated lane as
|
||||
> unverified for writes until its package token is rotated and re-proven.
|
||||
|
||||
The `NPM_AUTH_TOKEN` that publishes `@whynot/design` to the coulomb Forgejo npm registry
|
||||
The npm publish token for `@whynot/design` on the coulomb Forgejo npm registry
|
||||
(KV field `npm_token`; reaching the publish command as `NPM_AUTH_TOKEN`)
|
||||
(`https://forgejo.coulomb.social/api/packages/coulomb/npm/`). ops-warden **does not hold
|
||||
this token** — it is the access front door: `warden access` proxies the read from OpenBao
|
||||
**as the caller** and never persists, caches, or logs the value.
|
||||
|
|
@ -22,7 +23,8 @@ this token** — it is the access front door: `warden access` proxies the read f
|
|||
| Field | Value |
|
||||
| --- | --- |
|
||||
| OpenBao path | `platform/workloads/coulomb/whynot-design/npm-publish` |
|
||||
| Field | `NPM_AUTH_TOKEN` |
|
||||
| KV field | `npm_token` |
|
||||
| Injected env var | `NPM_AUTH_TOKEN` (set by the publication-scope policy; not a KV key) |
|
||||
| KV mount | `platform` |
|
||||
| Read policy | `workload-kv-read-whynot-design-npm-publish` |
|
||||
| OIDC login | `bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read` |
|
||||
|
|
@ -59,12 +61,12 @@ this token** — it is the access front door: `warden access` proxies the read f
|
|||
token fragment at `forgejo.coulomb.social`:
|
||||
```bash
|
||||
# --exec needs the env-var name. The zone-aware policy gate always runs first.
|
||||
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
|
||||
warden access whynot-design-npm-publish --field npm_token \
|
||||
--exec -- npm view @whynot/design@<version> version \
|
||||
--registry=https://forgejo.coulomb.social/api/packages/coulomb/npm/
|
||||
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
|
||||
warden access whynot-design-npm-publish --field npm_token \
|
||||
--exec -- npm publish
|
||||
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch
|
||||
warden access whynot-design-npm-publish --field npm_token --fetch
|
||||
```
|
||||
On either path the value transits to you (or the child env) and never enters
|
||||
ops-warden's memory, disk, or audit log.
|
||||
|
|
|
|||
|
|
@ -198,6 +198,36 @@ current access-engine implementation, received the decision-lifetime semantics
|
|||
question (message `02c99a74`). No implementation or policy decision was assigned
|
||||
to another owner by these messages.
|
||||
|
||||
**One of three answered, 2026-09-09.** `flex-auth` answered the decision-lifetime
|
||||
question as `FLEX-DEC-2026-004` (flex-auth commit `6a6464f`), and answered it
|
||||
rather than declining it: a decision lifetime shorter than the certificate TTL is
|
||||
meaningful, but only as authority to **issue**, never as authority to **use** an
|
||||
already-issued certificate. The action is `sign` and it completes at issuance, so
|
||||
the decision is consumed once the certificate exists and no field in the decision
|
||||
record reaches the artifact. 15m against a 48h `adm` certificate was never an
|
||||
inconsistency to reconcile — the question mistook a decision lifetime for a
|
||||
credential lifetime.
|
||||
|
||||
Two obligations land on this side and are accepted: no replay of an allow past
|
||||
`expires_at`, and a fresh Check per sign. The second is where the value is, and it
|
||||
is real rather than nominal because claims are not cached PDP-side (deadline 0 for
|
||||
the claim class), so a revoked principal stops obtaining *new* certificates
|
||||
immediately.
|
||||
|
||||
`pep-stance.yaml` §9.7.2 is unchanged and stays here: flex-auth explicitly declined
|
||||
to move the residue to their side, on the ground that a PDP recalling an issued
|
||||
credential would be "the same shape of lie as fail-open". Declining it was the
|
||||
right call even though absorbing it would have made their record look tidier.
|
||||
|
||||
The TTL lever they named — capping requested TTL per actor class or zone, already a
|
||||
policy input since `ttl_out_of_bounds` is denied before OpenBao is reached — is
|
||||
noted and deliberately not requested. Shortening `adm` from 48h has an operator
|
||||
cost, and issuance on this lane is too sparse (3 signature records across 79 days,
|
||||
`pep-stance.yaml` `emission_cadence`) to price it from intuition.
|
||||
|
||||
T05 stays `wait` on the remaining two: `ops-mason` (no stance map published) and
|
||||
`railiance-infra` (KRL distribution versus TTL expiry). Neither has replied.
|
||||
|
||||
## Related
|
||||
|
||||
Review 2026-09-05: reviewed the pending implementation and fixed the identity
|
||||
|
|
|
|||
|
|
@ -79,3 +79,33 @@ authenticated exact-version lookup proved that Forgejo's remote integrity
|
|||
matches the local dry-run. T03 waits only on rotating the dedicated package
|
||||
token and proving the next fresh version through `whynot-design-npm-publish`;
|
||||
the package migration itself is complete.
|
||||
|
||||
**2026-09-09 — field claim corrected, path routed.** `secrets-engine` (msg
|
||||
`15f0c0ca`) corrected two things and declined a third, all correctly.
|
||||
|
||||
The KV field is `npm_token`. `NPM_AUTH_TOKEN` is the environment variable their
|
||||
publication-scope policy injects, and ops-warden had copied the env var in as the
|
||||
field name — so the catalog's `fetch_command` named a field that does not exist
|
||||
and could only ever have failed. Corrected in `registry/routing/catalog.yaml` and
|
||||
`wiki/playbooks/whynot-design-npm-publish.md` from the owner's statement rather
|
||||
than re-derived here. This was `ADR-0001`'s failure mode rather than a typo: a
|
||||
pointer layer restating an owner's procedure and getting a detail wrong.
|
||||
|
||||
The endpoint claim already agreed; `delivery_config.npm.registry` has been the
|
||||
Forgejo URL throughout.
|
||||
|
||||
The **path** is routed to `railiance-platform` and the catalog is unchanged pending
|
||||
their answer. Which location backs the lane for reads is custody state they own;
|
||||
`secrets-engine` has no lane read authority to confirm it and refused to rewrite a
|
||||
production lane pointer from a coordination message (`SECRETS-WP-0006-T06`). Their
|
||||
reasoning is right, and asserting our own pointer is authoritative because it is
|
||||
ours would route around it. The ask names a location only and flags that a
|
||||
`bao kv get` answer would be the 2026-07-16 disclosure vector on a `risk: high`
|
||||
lane.
|
||||
|
||||
`ready: false` is not being read as path evidence: a source checkout with no
|
||||
production authority reports not-ready regardless of which path the catalog names.
|
||||
|
||||
T03 still waits on the human rotation of the dedicated Forgejo package token; the
|
||||
path question does not block that, it determines whether the pointer is correct
|
||||
once it rotates.
|
||||
|
|
|
|||
|
|
@ -74,3 +74,31 @@ authorized by this workplan alone. HFACT-WP-0001-T03 consumes this return.
|
|||
Validation: 429 tests passed (4 integration tests deselected by the repository default); Ruff passed for changed Python files. Full tests used the declared phase-memory source and an isolated temporary memory store. The focused policy/proxy suite passed 69 tests. The existing authenticated SSH policy probe still returns HTTP 200/ALLOW, decision:f3f7c88f9585582a; the credential-owner request returns 403. No CA issue or credential read was performed by these probes.
|
||||
|
||||
Installed verification: source guard present after refreshed owner installation (`19bb75c`). `warden access forgejo-admin-api-token --exec --field API_TOKEN -- true` exits 4 with explicit HTTP 403 before credential transport or child execution. The previous fail-open warning is absent. The probe requested no output value; no credential was fetched. T01/T02 are complete; T03 owns the remaining exact policy-binding admission.
|
||||
|
||||
**T03 routed 2026-09-09.** Asked `flex-auth` whether an admitted contract exists
|
||||
for a delegated credential read where the caller (ops-warden) and the resource
|
||||
owner differ, since that is exactly the shape drawing the live 403. Three outcomes
|
||||
were named as equally acceptable answers, with no preference stated between the
|
||||
first two: an existing contract to conform to; no contract yet, making this a
|
||||
design question they should raise rather than one ops-warden invents a shape for;
|
||||
or that there should be **no** such contract — the interim proxy transport is
|
||||
itself the defect and the answer is to finish the native handoff
|
||||
(`WARDEN-WP-0033` / `SECRETS-WP-0006`).
|
||||
|
||||
The third is a real possibility and was not argued against. Eleven catalog lanes
|
||||
are ops-warden proxies with a named intended owner precisely because no owner
|
||||
front door exists; if delegated reads should not be made policy-admissible, that
|
||||
shortens WP-0033 rather than blocking it.
|
||||
|
||||
Two fixes were explicitly ruled out in the message rather than left unmentioned:
|
||||
broadening the ops-warden caller binding so it may read other systems' resources,
|
||||
and relabelling `resource.system` as ops-warden so the existing binding matches.
|
||||
The second is worse — it would make the audit trail assert ops-warden owns
|
||||
credentials it deliberately does not, which is the claim `ADR-0002` exists to
|
||||
prevent, achieved by editing a field instead of making an argument.
|
||||
|
||||
The refusal stays in place until an answer yields positive evidence plus
|
||||
wrong-caller, wrong-owner and wrong-tenant negatives. No grant was requested.
|
||||
`secrets-engine` was told this bears on SECRETS-WP-0007-T04 and on how many lanes
|
||||
stay proxied; `gate-house` was told it may reach how the signing lane's
|
||||
approval-consume is built (GH-DEC-2026-005).
|
||||
|
|
|
|||
122
workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md
Normal file
122
workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
id: WARDEN-WP-0040
|
||||
type: workplan
|
||||
title: "Adopt unknown -> fail_closed behind signing-target classification coverage"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: proposed
|
||||
owner: ops-warden
|
||||
topic_slug: netkingdom
|
||||
planning_priority: P1
|
||||
depends_on_workplans:
|
||||
- WARDEN-WP-0032
|
||||
- WARDEN-WP-0034
|
||||
created: "2026-09-09"
|
||||
updated: "2026-09-09"
|
||||
---
|
||||
|
||||
# WARDEN-WP-0040 — `unknown` → `fail_closed`, in the right order
|
||||
|
||||
security-layer-model v0.8 §6.4 obligation 3 (proposed, 2026-09-06) rules that
|
||||
`unknown` is not a zone and must resolve to `fail_closed`. ops-warden's
|
||||
`pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`.
|
||||
|
||||
**We agree with the rule.** `history/2026-09-09-layer-model-v08-review.md` records
|
||||
the assent and why our falsifier check failed: the cell governs certificate
|
||||
issuance, not a §5.1 read-only diagnostic, so nothing rescues it.
|
||||
|
||||
**We disagree with adopting it first.** Measured 2026-09-09: of four signing
|
||||
targets, zero resolve to a zone, three are `unknown`, one is `not-applicable`.
|
||||
Flipping the cell today makes flex-auth a hard dependency of essentially every
|
||||
certificate — `ADR-0006`'s rejected configuration, reached by a different route,
|
||||
including the continuity path needed to repair flex-auth itself.
|
||||
|
||||
So: coverage first, then the cell. That ordering is the whole holding of
|
||||
`ADR-0006` and the reason `ADR-0009` replaced a global switch with a zone map.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0040-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Classify the continuity path before anything else changes.**
|
||||
|
||||
The deadlock is specific and worth naming before it is designed around: flex-auth
|
||||
unreachable → operator needs an SSH certificate to reach the host → target is
|
||||
`unknown` because nobody classified the repair path → `fail_closed` denies it.
|
||||
|
||||
`z2-continuity` exists for this and its stance is `fail_open`, so the fix is
|
||||
classification, not an exception. Determine which actor(s) constitute the
|
||||
repair path, establish whether the declaration is ops-warden's to make (our own
|
||||
`tenancy.yaml` covers ops-warden as a workload; the actors' target workloads may
|
||||
not be ours), and route what is not.
|
||||
|
||||
Blocks T03. Converting the cell with the repair path unclassified is the one
|
||||
outcome this workplan exists to prevent.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0040-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Raise signing-target coverage, by asking owners — never by inferring.**
|
||||
|
||||
Three actor resources report `workload_resolution_absent`:
|
||||
`agt-codex-interhub-bootstrap`, `agt-state-hub-bridge`, `atm-backup-daily`.
|
||||
Their owners (ops-bridge and the backup execution unit among them) have not
|
||||
published an authoritative workload identity declaration.
|
||||
|
||||
`ADR-0009` rule 3 forbids closing this with path or repository-name inference,
|
||||
and that prohibition holds under pressure from this workplan specifically —
|
||||
adopting a stricter stance is not a licence to manufacture the membership that
|
||||
makes it survivable.
|
||||
|
||||
Route to each owner with the concrete consequence: while the declaration is
|
||||
absent their actor cannot be issued a certificate during a flex-auth outage once
|
||||
the cell converts. That is a better ask than a generic request to publish a
|
||||
declaration, and it is true.
|
||||
|
||||
Report coverage with `scripts/report_workload_join.py`; this task is done when
|
||||
coverage is stated, not when it reaches a threshold — the threshold is T03's
|
||||
gate, and owners may legitimately decline.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0040-T03
|
||||
status: wait
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Convert the cell and supersede `ADR-0009`'s unknown row.**
|
||||
|
||||
Gated on T01, and on v0.8 (or its successor) reaching `accepted`. Do not convert
|
||||
against a `proposed` standard.
|
||||
|
||||
`ADR-0009` is `owner: ops-warden`, so changing it means a superseding ADR, never
|
||||
an in-place edit (`.claude/rules/architecture.md`). The superseding record should
|
||||
carry the v0.8 argument rather than restate it: unknown is not a zone, so a §9.3
|
||||
per-zone trade was never made for that request.
|
||||
|
||||
`pep-stance.yaml` is asserted equal to `PolicyConfig.failure_modes` by
|
||||
`tests/test_layer_conformance.py`, so the map and the code convert in one commit
|
||||
or the test fails — which is the property that makes the map worth publishing.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0040-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
**Return findings to gate-house and track the transition ask.**
|
||||
|
||||
Sent 2026-09-09 with the review. Two asks, in preference order: obligation 3
|
||||
names a dated transitional state gated on coverage; or failing that, §13.1
|
||||
records classification coverage alongside stance, so a register row cannot read
|
||||
`unknown: fail_closed` while every target is unknown.
|
||||
|
||||
If both are declined, that is an answer and this workplan proceeds unchanged —
|
||||
the cell stays a **declared gap** under §11 with this workplan as its route,
|
||||
which is what the `declared-gap` mark (our own v0.6 finding) exists to express.
|
||||
Loading…
Add table
Add a link
Reference in a new issue