MASON-WP-0015: gate CCR evidence and qualify the kube wait
T07 generates a CCR policy through the platform generator and refuses record-evidence unless the built policy matches the CCR file. T06 stays wait on the open founder decision after a fresh can-i of yes, yes, yes. Assistant: grok Assistant-Session: 01a16071-5425-78d3-80b4-20fe2ee30f0a
This commit is contained in:
parent
4e46aa3b4f
commit
9b705ec62c
6 changed files with 385 additions and 14 deletions
|
|
@ -164,7 +164,7 @@ tooling_contacts:
|
||||||
until: "2026-12-21"
|
until: "2026-12-21"
|
||||||
rule: "Direct ADMINISTER under activation=APPROVED, each change recorded as production-tier, until ArgoCD onboarding."
|
rule: "Direct ADMINISTER under activation=APPROVED, each change recorded as production-tier, until ArgoCD onboarding."
|
||||||
relies_on_limits: "One expected namespace per plan; Pod and Secret kinds refused; no data or stringData. Widening them is a new decision."
|
relies_on_limits: "One expected namespace per plan; Pod and Secret kinds refused; no data or stringData. Widening them is a new decision."
|
||||||
identity_gap: "2026-10-18 (MASON-IN-0006): the kube identity applied with is system:admin (system:masters). Refusing Pod and Secret kinds is a code limit in kubernetes_plane, not an RBAC limit; the identity can mint any ServiceAccount token."
|
identity_gap: "2026-10-22 (MASON-IN-0006, still open): context default, name-only kubectl auth can-i --all-namespaces at 2026-10-21T23:55:17Z returned yes for create serviceaccounts/token, yes for create pods, and yes for impersonate serviceaccounts. The kube identity applied with is still system:admin (system:masters). Refusing Pod and Secret kinds is a code limit in kubernetes_plane, not an RBAC limit; the identity can mint any ServiceAccount token. Options (a), (b), and (c) are unchanged and unchosen. T06 waits on founder decision 30d29060-8d14-4a8e-9cde-34aa826eb8d1."
|
||||||
enforcement: "src/ops_mason/readiness.py: inspect_readiness and resolve_tier; kubernetes_plane.apply refuses before Kubernetes writes. Source/history verification, explicit whitehat placement, dated policy-nexus transition, and recorded BREAK_GLASS."
|
enforcement: "src/ops_mason/readiness.py: inspect_readiness and resolve_tier; kubernetes_plane.apply refuses before Kubernetes writes. Source/history verification, explicit whitehat placement, dated policy-nexus transition, and recorded BREAK_GLASS."
|
||||||
- id: bao-session-grant
|
- id: bao-session-grant
|
||||||
shape: "5.2"
|
shape: "5.2"
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
| task | MASON-WP-0015-T04 | done | — | workplans/MASON-WP-0015-construct-grant.md |
|
| task | MASON-WP-0015-T04 | done | — | workplans/MASON-WP-0015-construct-grant.md |
|
||||||
| task | MASON-WP-0015-T05 | done | — | workplans/MASON-WP-0015-construct-grant.md |
|
| task | MASON-WP-0015-T05 | done | — | workplans/MASON-WP-0015-construct-grant.md |
|
||||||
| task | MASON-WP-0015-T06 | wait | — | workplans/MASON-WP-0015-construct-grant.md |
|
| task | MASON-WP-0015-T06 | wait | — | workplans/MASON-WP-0015-construct-grant.md |
|
||||||
| task | MASON-WP-0015-T07 | wait | — | workplans/MASON-WP-0015-construct-grant.md |
|
| task | MASON-WP-0015-T07 | done | — | workplans/MASON-WP-0015-construct-grant.md |
|
||||||
| intake | MASON-IN-0001 | closed | green | intakes/intakes.md |
|
| intake | MASON-IN-0001 | closed | green | intakes/intakes.md |
|
||||||
| intake | MASON-IN-0002 | closed | — | intakes/intakes.md |
|
| intake | MASON-IN-0002 | closed | — | intakes/intakes.md |
|
||||||
| intake | MASON-IN-0003 | open | — | intakes/intakes.md |
|
| intake | MASON-IN-0003 | open | — | intakes/intakes.md |
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,11 @@ description: >-
|
||||||
The only control against Secret reads is the guard-secret-reads transcript
|
The only control against Secret reads is the guard-secret-reads transcript
|
||||||
hook, which is a convention, not access control. Checked by capability
|
hook, which is a convention, not access control. Checked by capability
|
||||||
queries only; not exercised.
|
queries only; not exercised.
|
||||||
|
Fresh check 2026-10-22 (captured 2026-10-21T23:55:17Z), context default,
|
||||||
|
name-only, all namespaces: create serviceaccounts/token yes, create pods
|
||||||
|
yes, impersonate serviceaccounts yes. Still cluster-admin. Options (a),
|
||||||
|
(b), and (c) below are unchanged and unchosen. MASON-WP-0015-T06 waits on
|
||||||
|
founder decision 30d29060-8d14-4a8e-9cde-34aa826eb8d1.
|
||||||
Side note: ~/.kube/config-hosteurope points at 127.0.0.1:16443, where nothing
|
Side note: ~/.kube/config-hosteurope points at 127.0.0.1:16443, where nothing
|
||||||
is listening, but the railiance-platform runbooks still reference it.
|
is listening, but the railiance-platform runbooks still reference it.
|
||||||
options: >-
|
options: >-
|
||||||
|
|
@ -269,7 +274,7 @@ options: >-
|
||||||
date.
|
date.
|
||||||
Cluster RBAC and the kubeconfig distribution belong to railiance-platform
|
Cluster RBAC and the kubeconfig distribution belong to railiance-platform
|
||||||
(and railiance-infra for node credentials); this is a founder decision.
|
(and railiance-infra for node credentials); this is a founder decision.
|
||||||
updated: '2026-10-18T00:00:00Z'
|
updated: '2026-10-22T00:00:00Z'
|
||||||
state_hub_intake_id: "01a14ea8-3bd8-7da1-b51e-849067a39aff"
|
state_hub_intake_id: "01a14ea8-3bd8-7da1-b51e-849067a39aff"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
278
src/ops_mason/ccr_gate.py
Normal file
278
src/ops_mason/ccr_gate.py
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
"""Client gate for a CCR drafted from a plan (MASON-WP-0015-T07).
|
||||||
|
|
||||||
|
The policy text comes from railiance-platform's generator
|
||||||
|
(``credential-change.py render-policy``). Plan approval is the single hub
|
||||||
|
decision already stored on the draft. This module does not run
|
||||||
|
``sync-decision``; that stays a platform or founder step.
|
||||||
|
|
||||||
|
``record_evidence`` refuses to call ``record-evidence`` unless the built
|
||||||
|
policy text matches the CCR policy file after a trailing newline is
|
||||||
|
normalised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Sequence
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
PLATFORM = Path.home() / "railiance-platform"
|
||||||
|
|
||||||
|
_UUID = re.compile(
|
||||||
|
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||||
|
)
|
||||||
|
_COMMIT = re.compile(r"^[0-9a-f]{7,40}$")
|
||||||
|
# Same markers the platform scanner rejects. Values never belong in a draft.
|
||||||
|
_SECRET_MARKERS = (
|
||||||
|
"AGE-SECRET-KEY-1",
|
||||||
|
"-----BEGIN PRIVATE KEY-----",
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||||
|
"OPENBAO_ROOT_TOKEN=",
|
||||||
|
"VAULT_TOKEN=",
|
||||||
|
"BAO_TOKEN=",
|
||||||
|
"hvb.",
|
||||||
|
"hvc.",
|
||||||
|
"hvs.",
|
||||||
|
"npm_",
|
||||||
|
"ghp_",
|
||||||
|
"sk-",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceRefused(RuntimeError):
|
||||||
|
"""``record-evidence`` was not run."""
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_env() -> dict[str, str]:
|
||||||
|
venv = os.environ.get("VIRTUAL_ENV")
|
||||||
|
env = {
|
||||||
|
key: value
|
||||||
|
for key, value in os.environ.items()
|
||||||
|
if key not in {"VIRTUAL_ENV", "UV_PROJECT_ENVIRONMENT", "PYTHONPATH"}
|
||||||
|
}
|
||||||
|
if venv:
|
||||||
|
env["PATH"] = os.pathsep.join(
|
||||||
|
part for part in env.get("PATH", "").split(os.pathsep) if not part.startswith(venv)
|
||||||
|
)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_secrets(text: str) -> None:
|
||||||
|
for marker in _SECRET_MARKERS:
|
||||||
|
if marker == "sk-":
|
||||||
|
if re.search(r"(?<![A-Za-z])sk-[A-Za-z0-9]", text):
|
||||||
|
raise EvidenceRefused("refusing secret-bearing text; record-evidence not run")
|
||||||
|
continue
|
||||||
|
if marker in text:
|
||||||
|
raise EvidenceRefused("refusing secret-bearing text; record-evidence not run")
|
||||||
|
|
||||||
|
|
||||||
|
def _generator(platform: Path | None) -> Path:
|
||||||
|
root = platform or PLATFORM
|
||||||
|
script = root / "scripts" / "credential-change.py"
|
||||||
|
if not script.is_file():
|
||||||
|
raise EvidenceRefused(f"platform generator not found at {script}")
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
def generate_policy(kv_path: str, *, platform: Path | None = None) -> str:
|
||||||
|
"""Return the standard-lane policy HCL from the platform generator.
|
||||||
|
|
||||||
|
This prints a draft. It does not submit a CCR.
|
||||||
|
"""
|
||||||
|
script = _generator(platform)
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(script), "render-policy", "--kv-path", kv_path],
|
||||||
|
cwd=script.parents[1],
|
||||||
|
env=_platform_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
raise EvidenceRefused("platform generator refused the KV path; record-evidence not run")
|
||||||
|
policy = completed.stdout
|
||||||
|
if not policy.strip():
|
||||||
|
raise EvidenceRefused("platform generator returned an empty policy")
|
||||||
|
_reject_secrets(policy)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_id(ccr: dict[str, Any]) -> str:
|
||||||
|
state = ccr.get("state_hub")
|
||||||
|
if not isinstance(state, dict):
|
||||||
|
raise EvidenceRefused("plan approval must be the single hub decision")
|
||||||
|
decision_id = state.get("decision_id")
|
||||||
|
if not isinstance(decision_id, str) or _UUID.fullmatch(decision_id) is None:
|
||||||
|
raise EvidenceRefused("plan approval must be the single hub decision")
|
||||||
|
extra = [key for key in state if key not in {"decision_id", "decision_api_url"}]
|
||||||
|
if extra:
|
||||||
|
raise EvidenceRefused("plan approval must be the single hub decision")
|
||||||
|
return decision_id
|
||||||
|
|
||||||
|
|
||||||
|
def draft_from_plan(
|
||||||
|
*,
|
||||||
|
kv_path: str,
|
||||||
|
plan_ref: dict[str, str],
|
||||||
|
decision_id: str,
|
||||||
|
out_dir: Path,
|
||||||
|
platform: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Write a non-secret CCR draft and the generated policy. Does not submit."""
|
||||||
|
if _UUID.fullmatch(decision_id) is None:
|
||||||
|
raise EvidenceRefused("plan approval must be the single hub decision")
|
||||||
|
for key in ("repo", "path", "commit"):
|
||||||
|
value = plan_ref.get(key)
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise EvidenceRefused("plan_ref needs repo, path, and commit")
|
||||||
|
if _COMMIT.fullmatch(plan_ref["commit"]) is None:
|
||||||
|
raise EvidenceRefused("plan_ref commit must be a hex git commit")
|
||||||
|
policy = generate_policy(kv_path, platform=platform)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
policy_path = out_dir / "policy.hcl"
|
||||||
|
policy_path.write_text(policy if policy.endswith("\n") else policy + "\n", encoding="utf-8")
|
||||||
|
slug = kv_path.rstrip("/").rsplit("/", 1)[-1]
|
||||||
|
policy_name = f"workload-kv-read-{slug}"
|
||||||
|
draft = {
|
||||||
|
"id": "CCR-DRAFT",
|
||||||
|
"kind": "credential-change-request",
|
||||||
|
"schema_version": 1,
|
||||||
|
"request_type": "workload-kv-read",
|
||||||
|
"title": f"Draft reader for {kv_path}",
|
||||||
|
"status": "draft",
|
||||||
|
"created": "2026-10-22",
|
||||||
|
"updated": "2026-10-22",
|
||||||
|
"requester": {"agent": "ops-mason"},
|
||||||
|
"plan_ref": {key: plan_ref[key] for key in ("repo", "path", "commit")},
|
||||||
|
"state_hub": {"decision_id": decision_id},
|
||||||
|
"target": {
|
||||||
|
"domain": "infotech",
|
||||||
|
"tenant": "railiance",
|
||||||
|
"workload": slug,
|
||||||
|
"environment": "build",
|
||||||
|
"purpose": "standard-lane KV read drafted from the construction plan",
|
||||||
|
},
|
||||||
|
"openbao": {
|
||||||
|
"mount": kv_path.split("/", 1)[0],
|
||||||
|
"kv_path": kv_path,
|
||||||
|
"policy_name": policy_name,
|
||||||
|
"policy_file": str(policy_path),
|
||||||
|
"metadata_read": False,
|
||||||
|
"token_self_lifecycle": True,
|
||||||
|
"fields": ["FIELD"],
|
||||||
|
"auth": {
|
||||||
|
"method": "kubernetes",
|
||||||
|
"mount": "kubernetes",
|
||||||
|
"role": f"external-secrets-{slug}",
|
||||||
|
"audience": "openbao",
|
||||||
|
"ttl": "15m",
|
||||||
|
"policies": [policy_name],
|
||||||
|
"bound_claims": {
|
||||||
|
"service_account_names": [slug],
|
||||||
|
"service_account_namespaces": ["workloads"],
|
||||||
|
},
|
||||||
|
"bound_claims_confirmed": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"access_frontdoor": {
|
||||||
|
"type": "ops-warden",
|
||||||
|
"catalog_id": f"draft-{slug}",
|
||||||
|
"readiness": "pending-review",
|
||||||
|
"resolvable": False,
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"classification": "low",
|
||||||
|
"notes": ["Draft only. No value is stored in this file."],
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"positive": ["Authorized caller can read the path without printing a value."],
|
||||||
|
"negative": ["Unauthorized caller cannot read the path."],
|
||||||
|
"activation_conditions": ["Founder resolves the single hub decision."],
|
||||||
|
},
|
||||||
|
"lifecycle": {
|
||||||
|
"deactivate": "Delete the Kubernetes auth role and the workload policy.",
|
||||||
|
"rotate": "Replace the value out of band and record the rotation.",
|
||||||
|
"compromised": "Revoke the role and treat the path as exposed.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
text = yaml.safe_dump(draft, sort_keys=False)
|
||||||
|
_reject_secrets(text)
|
||||||
|
draft_path = out_dir / "ccr-draft.yaml"
|
||||||
|
draft_path.write_text(text, encoding="utf-8")
|
||||||
|
return {"draft": draft_path, "policy_file": policy_path, "built_policy": policy}
|
||||||
|
|
||||||
|
|
||||||
|
def _policy_text(ccr: dict[str, Any], draft_path: Path) -> str:
|
||||||
|
policy_file = (ccr.get("openbao") or {}).get("policy_file")
|
||||||
|
if not isinstance(policy_file, str) or not policy_file:
|
||||||
|
raise EvidenceRefused("CCR draft has no policy file")
|
||||||
|
path = Path(policy_file)
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = draft_path.parent / path
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def record_evidence(
|
||||||
|
draft_path: Path,
|
||||||
|
*,
|
||||||
|
built_policy: str,
|
||||||
|
actor: str,
|
||||||
|
kind: str,
|
||||||
|
result: str,
|
||||||
|
detail: Sequence[str],
|
||||||
|
platform: Path | None = None,
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
"""Append evidence only when the built policy matches the CCR policy file.
|
||||||
|
|
||||||
|
A trailing newline is ignored. Any other difference refuses the call.
|
||||||
|
The command is ``record-evidence``. ``sync-decision`` is never invoked.
|
||||||
|
"""
|
||||||
|
draft_path = Path(draft_path)
|
||||||
|
loaded = yaml.safe_load(draft_path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(loaded, dict):
|
||||||
|
raise EvidenceRefused("CCR draft is not a document")
|
||||||
|
_decision_id(loaded)
|
||||||
|
file_text = _policy_text(loaded, draft_path)
|
||||||
|
rendered_detail = "\n".join(detail)
|
||||||
|
_reject_secrets(built_policy)
|
||||||
|
_reject_secrets(file_text)
|
||||||
|
_reject_secrets(rendered_detail)
|
||||||
|
_reject_secrets(actor)
|
||||||
|
if built_policy.rstrip("\n") != file_text.rstrip("\n"):
|
||||||
|
raise EvidenceRefused(
|
||||||
|
"built policy does not match the CCR policy file; record-evidence refused"
|
||||||
|
)
|
||||||
|
script = _generator(platform)
|
||||||
|
argv = [
|
||||||
|
sys.executable,
|
||||||
|
str(script),
|
||||||
|
"record-evidence",
|
||||||
|
str(draft_path),
|
||||||
|
"--actor",
|
||||||
|
actor,
|
||||||
|
"--kind",
|
||||||
|
kind,
|
||||||
|
"--result",
|
||||||
|
result,
|
||||||
|
]
|
||||||
|
for line in detail:
|
||||||
|
argv.extend(["--detail", line])
|
||||||
|
if "sync-decision" in argv:
|
||||||
|
raise EvidenceRefused("sync-decision stays a platform or founder step")
|
||||||
|
completed = subprocess.run(
|
||||||
|
argv,
|
||||||
|
cwd=script.parents[1],
|
||||||
|
env=_platform_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
raise EvidenceRefused("record-evidence failed; no evidence was accepted")
|
||||||
|
return completed
|
||||||
72
tests/test_ccr_gate.py
Normal file
72
tests/test_ccr_gate.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""The CCR record-evidence gate matches a policy and refuses a different one."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ops_mason.ccr_gate import EvidenceRefused, draft_from_plan, record_evidence
|
||||||
|
|
||||||
|
DECISION_ID = "00000000-0000-4000-8000-000000000001"
|
||||||
|
KV_PATH = "platform/workloads/fixture/reader"
|
||||||
|
|
||||||
|
|
||||||
|
def _draft(tmp_path: Path) -> dict:
|
||||||
|
return draft_from_plan(
|
||||||
|
kv_path=KV_PATH,
|
||||||
|
plan_ref={
|
||||||
|
"repo": "ops-mason",
|
||||||
|
"path": "plans/fixture.md",
|
||||||
|
"commit": "0123456789abcdef",
|
||||||
|
},
|
||||||
|
decision_id=DECISION_ID,
|
||||||
|
out_dir=tmp_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence(draft: Path) -> list:
|
||||||
|
document = yaml.safe_load(draft.read_text(encoding="utf-8"))
|
||||||
|
return (document.get("verification") or {}).get("evidence") or []
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_evidence_accepts_a_trailing_newline_and_refuses_a_different_policy(tmp_path):
|
||||||
|
drafted = _draft(tmp_path)
|
||||||
|
policy_path = Path(drafted["policy_file"])
|
||||||
|
# The platform generator's text, plus one trailing newline, still matches.
|
||||||
|
policy_path.write_text(policy_path.read_text(encoding="utf-8") + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
completed = record_evidence(
|
||||||
|
drafted["draft"],
|
||||||
|
built_policy=drafted["built_policy"],
|
||||||
|
actor="ops-mason",
|
||||||
|
kind="metadata_apply",
|
||||||
|
result="passed",
|
||||||
|
detail=["built policy matched the CCR policy file"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert completed.returncode == 0
|
||||||
|
assert "record-evidence" in completed.args
|
||||||
|
assert "sync-decision" not in completed.args
|
||||||
|
recorded = _evidence(drafted["draft"])
|
||||||
|
assert len(recorded) == 1
|
||||||
|
assert recorded[0]["kind"] == "metadata_apply"
|
||||||
|
assert recorded[0]["result"] == "passed"
|
||||||
|
assert "matched the CCR policy file" in recorded[0]["details"][0]
|
||||||
|
|
||||||
|
policy_path.write_text(
|
||||||
|
policy_path.read_text(encoding="utf-8").replace('["read"]', '["create"]', 1),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with pytest.raises(EvidenceRefused, match="record-evidence refused"):
|
||||||
|
record_evidence(
|
||||||
|
drafted["draft"],
|
||||||
|
built_policy=drafted["built_policy"],
|
||||||
|
actor="ops-mason",
|
||||||
|
kind="metadata_apply",
|
||||||
|
result="passed",
|
||||||
|
detail=["built policy matched the CCR policy file"],
|
||||||
|
)
|
||||||
|
assert len(_evidence(drafted["draft"])) == 1
|
||||||
|
stored = drafted["draft"].read_text(encoding="utf-8")
|
||||||
|
assert "sync-decision" not in stored
|
||||||
|
assert DECISION_ID in stored
|
||||||
|
|
@ -8,7 +8,7 @@ status: active
|
||||||
owner: claude
|
owner: claude
|
||||||
topic_slug: netkingdom-security-architecture
|
topic_slug: netkingdom-security-architecture
|
||||||
created: "2026-10-18"
|
created: "2026-10-18"
|
||||||
updated: "2026-10-18"
|
updated: "2026-10-22"
|
||||||
flavor: implementation
|
flavor: implementation
|
||||||
depends_on: []
|
depends_on: []
|
||||||
state_hub_workstream_id: "384b3efc-f9a5-5ea9-b224-0ce900e79804"
|
state_hub_workstream_id: "384b3efc-f9a5-5ea9-b224-0ce900e79804"
|
||||||
|
|
@ -115,6 +115,9 @@ only. They edit their own files.
|
||||||
id: MASON-WP-0015-T06
|
id: MASON-WP-0015-T06
|
||||||
status: wait
|
status: wait
|
||||||
priority: medium
|
priority: medium
|
||||||
|
needs_human: true
|
||||||
|
decision_id: "30d29060-8d14-4a8e-9cde-34aa826eb8d1"
|
||||||
|
intervention_note: "Founder chooses MASON-IN-0006 option (a), (b), or (c). ops-mason did not choose and did not change cluster RBAC."
|
||||||
state_hub_task_id: "feb17422-9490-51f4-a62d-55647a5f6b05"
|
state_hub_task_id: "feb17422-9490-51f4-a62d-55647a5f6b05"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -127,24 +130,37 @@ the Pod and Secret kinds. Record the result in INTENT.md's
|
||||||
|
|
||||||
Result 2026-10-18: **open.** The kube identity is system:admin
|
Result 2026-10-18: **open.** The kube identity is system:admin
|
||||||
(system:masters). It can create serviceaccounts/token, pods and impersonate,
|
(system:masters). It can create serviceaccounts/token, pods and impersonate,
|
||||||
cluster-wide. Raised as MASON-IN-0006; this waits on the founder's decision
|
cluster-wide. Raised as MASON-IN-0006.
|
||||||
there.
|
|
||||||
|
Result 2026-10-22, context `default`, captured 2026-10-21T23:55:17Z,
|
||||||
|
`--all-namespaces`, name-only: create serviceaccounts/token **yes**, create
|
||||||
|
pods **yes**, impersonate serviceaccounts **yes**. The identity is still
|
||||||
|
cluster-admin. MASON-IN-0006 stays the open residual and still names options
|
||||||
|
(a), (b), and (c). This task does not choose one and does not change cluster
|
||||||
|
RBAC. It waits on founder decision
|
||||||
|
`30d29060-8d14-4a8e-9cde-34aa826eb8d1` (open, pending).
|
||||||
|
|
||||||
## Submit CCRs from approved plans (RPF-WP-0061)
|
## Submit CCRs from approved plans (RPF-WP-0061)
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: MASON-WP-0015-T07
|
id: MASON-WP-0015-T07
|
||||||
status: wait
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "ebc420c6-be1a-5abb-b766-fda0bdf2680b"
|
state_hub_task_id: "ebc420c6-be1a-5abb-b766-fda0bdf2680b"
|
||||||
```
|
```
|
||||||
|
|
||||||
railiance-platform RPF-WP-0061 makes ops-mason the only CCR submitter
|
railiance-platform RPF-WP-0061 makes ops-mason the only CCR submitter
|
||||||
(`credential-change.py submit`). ops-mason's side:
|
(`credential-change.py submit`). RPF-WP-0061-T03 and T04 are done, so this
|
||||||
- generate the CCR draft and policy file from the plan, using platform's
|
no longer waits on them. RPF-WP-0061-T05 still waits for a first live
|
||||||
generator once it is exposed;
|
self-service submission; none was filed here because no approved plan is
|
||||||
- take plan approval from the single hub decision after `sync-decision`;
|
waiting.
|
||||||
- compare the built policy with the CCR policy file, then `record-evidence`.
|
|
||||||
|
|
||||||
Comment sent 2026-10-18, as a reply to `f49ddd0d`. This waits on
|
2026-10-22: ops-mason generates the CCR draft and its policy from the plan
|
||||||
RPF-WP-0061-T03 and T04.
|
through the platform generator (`credential-change.py render-policy`,
|
||||||
|
`src/ops_mason/ccr_gate.py` `draft_from_plan`). Plan approval is the single
|
||||||
|
hub decision recorded on the draft (`state_hub.decision_id`). ops-mason did
|
||||||
|
not run `sync-decision`; that stays a platform or founder step. Before
|
||||||
|
`record-evidence`, `record_evidence` compares the built policy text with the
|
||||||
|
CCR policy file and refuses the call unless they match, with a trailing
|
||||||
|
newline normalised. The draft, the policy, and the evidence contain no
|
||||||
|
secret value. Tests: `tests/test_ccr_gate.py` and `tests/test_construct_policy.py`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue