Add the silent MAS publisher gate for the prefix policy and CAS=0 pair.
All checks were successful
CCR intake gate / gate (push) Successful in 13s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

The attended platform-admin login timed out before the gate ran, so the live policy, workload pair, and delivery admission are unchanged.

Assistant: grok
Assistant-Session: 01a198fa-fd27-7e41-a0a2-57355c3e4efd
This commit is contained in:
codex 2026-11-02 12:24:47 +01:00
parent 814469944c
commit 5b0a2a06c4
5 changed files with 505 additions and 4 deletions

View file

@ -1,7 +1,8 @@
# Attended writer for the two application-archive publisher pairs.
# Founder admission 2026-10-08. CAS=0 and one-write behavior are enforced by
# scripts/application_backup_pair_custody.py. This policy cannot delete,
# list, or read secret values.
# Attended writer for the application-archive publisher pairs.
# Founder admission 2026-10-08 for coulomb-social and email-connect.
# The MAS path is included for the one CAS=0 pair write. CAS=0 and one-write
# behavior are enforced by scripts/application_backup_pair_custody.py.
# This policy cannot delete, list, or read secret values.
path "platform/data/workloads/coulomb-social/backup-primary" {
capabilities = ["create", "update"]
@ -11,6 +12,10 @@ path "platform/data/workloads/email-connect/backup-primary" {
capabilities = ["create", "update"]
}
path "platform/data/workloads/mas/backup-primary" {
capabilities = ["create", "update"]
}
path "platform/metadata/workloads/coulomb-social/backup-primary" {
capabilities = ["read"]
}
@ -19,6 +24,10 @@ path "platform/metadata/workloads/email-connect/backup-primary" {
capabilities = ["read"]
}
path "platform/metadata/workloads/mas/backup-primary" {
capabilities = ["read"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}

View file

@ -24,6 +24,7 @@ WRITER_ADMITTED = True
PROFILES = {
"coulomb-social": "CCR-2026-0036",
"email-connect": "CCR-2026-0037",
"mas": "CCR-2026-0062",
}
VALUE = re.compile(r"[!-~]{16,128}\Z")

View file

@ -0,0 +1,395 @@
#!/usr/bin/env python3
"""Apply the MAS publisher boundary and write its one CAS=0 pair.
Stdout stays empty. Receipts record statement ids, HTTP statuses, versions,
and field names. Values are never written to a receipt or to stderr.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path("/home/worsch/railiance-platform")
DESIRED = ROOT / "docs/object-storage/railiance-platform-pg-backup-policy.json"
POLICY_FILE = ROOT / "openbao/policies/backup-pair-writer.hcl"
RECEIPT_DIR = Path("/home/worsch/.local/state/railiance/receipts")
BUCKET = "railiance-platform-pg-backup"
ENDPOINT = "https://s3.nl-ams.scw.cloud"
APPLICATION_ID = "25a4cbca-220e-40c0-9af5-6c7713000174"
PREFIX = "platform-pg/application-archives/mas/"
SIBLING = "platform-pg/application-archives/email-connect/"
MAS_SIDS = (
"MasEvidenceBackupProbe",
"MasEvidenceBackupList",
"MasEvidenceBackupObjects",
)
SUPPLIER = "platform/suppliers/scaleway/mas-evidence-backup-primary"
WORKLOAD = "platform/workloads/mas/backup-primary"
PREFERRED_PROJECT = "mail-address-semantics"
FIELD_ALIASES = (
("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"),
("ACCESS_KEY", "SECRET_KEY"),
("access_key", "secret_key"),
("SCW_ACCESS_KEY", "SCW_SECRET_KEY"),
)
class GateError(RuntimeError):
def __init__(self, code):
super().__init__(code)
self.code = code
def supplier_pair(data):
"""Return the two workload fields. Refuse a partial or identical pair."""
if not isinstance(data, dict):
raise GateError("supplier_shape_rejected")
for access_name, secret_name in FIELD_ALIASES:
access = data.get(access_name)
secret = data.get(secret_name)
if access is None and secret is None:
continue
if not isinstance(access, str) or not isinstance(secret, str):
raise GateError("supplier_shape_rejected")
access = access.strip()
secret = secret.strip()
if access == secret or not access or not secret:
raise GateError("supplier_shape_rejected")
return {"AWS_ACCESS_KEY_ID": access, "AWS_SECRET_ACCESS_KEY": secret}
raise GateError("supplier_shape_rejected")
def mas_statements(document):
found = []
for item in document.get("Statement", []):
if item.get("Sid") in MAS_SIDS:
found.append(item)
return found
def policy_observation(document):
"""Non-secret view of the MAS statements and whether object delete is absent."""
rows = mas_statements(document)
actions = []
for item in rows:
value = item.get("Action") or []
if isinstance(value, str):
value = [value]
actions.extend(value)
return {
"sids": [item.get("Sid") for item in rows],
"mas_sids_present": [sid for sid in MAS_SIDS if sid in {item.get("Sid") for item in rows}],
"object_delete_absent": not any(
action in {"s3:DeleteObject", "s3:DeleteObjectVersion"} for action in actions
),
"principal": APPLICATION_ID,
}
def denial_observation(results):
required = ("sibling_prefix", "retained_delete", "administration")
denied = {name: bool(results.get(name, {}).get("denied")) for name in required}
return {
"application_id": APPLICATION_ID,
"denied": denied,
"http_status": {
name: results.get(name, {}).get("http_status") for name in (*required, "own_prefix")
},
"passed": all(denied.values()),
}
def version_from_put(payload):
data = payload.get("data") or {}
version = data.get("version")
if version != 1:
raise GateError("unexpected_version")
return {"version": 1, "request_id": payload.get("request_id")}
def logical_path(path):
for marker in ("/metadata/", "/data/"):
if path.startswith("platform" + marker):
return "platform/" + path.split(marker, 1)[1]
return path
def _write_json(path, value):
path.parent.mkdir(mode=0o700, exist_ok=True)
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
path.chmod(0o600)
def _bao(args, token=None):
env = os.environ.copy()
env.setdefault("BAO_ADDR", "http://127.0.0.1:18200")
env.setdefault("VAULT_ADDR", env["BAO_ADDR"])
if token is None:
pass
else:
env["BAO_TOKEN"] = token
env["VAULT_TOKEN"] = token
return subprocess.run(["bao", *args], capture_output=True, env=env)
def _kv_data(path):
proc = _bao(["kv", "get", "-format=json", path])
if proc.returncode:
raise GateError("kv_read_failed")
body = json.loads(proc.stdout)
data = ((body.get("data") or {}).get("data")) or {}
if not isinstance(data, dict):
raise GateError("kv_read_failed")
return data
def _client(pair):
import boto3
from botocore.config import Config
return boto3.client(
"s3",
region_name="nl-ams",
endpoint_url=ENDPOINT,
aws_access_key_id=pair["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=pair["AWS_SECRET_ACCESS_KEY"],
config=Config(
signature_version="s3v4",
connect_timeout=15,
read_timeout=30,
retries={"max_attempts": 2},
s3={"addressing_style": "path"},
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
),
)
def _status(call):
from botocore.exceptions import ClientError
try:
result = call()
body = result.get("Body") if isinstance(result, dict) else None
if body is not None:
body.close()
code = result.get("ResponseMetadata", {}).get("HTTPStatusCode", 200)
return {"http_status": code, "denied": False}
except ClientError as exc:
code = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
return {"http_status": code, "denied": code == 403}
def _ensure_boto():
try:
import boto3 # noqa: F401
except ImportError:
candidate = Path("/tmp/mas-bucket-policy-venv/bin/python")
if candidate.exists() and Path(sys.executable) != candidate:
os.execv(candidate, [str(candidate), *sys.argv])
raise GateError("boto3_required") from None
def apply_live_policy(reader_pair, writer_pair):
desired = json.loads(DESIRED.read_text(encoding="utf-8"))
reader = _client(reader_pair)
live = json.loads(reader.get_bucket_policy(Bucket=BUCKET)["Policy"])
present = {item.get("Sid") for item in live.get("Statement", [])}
statements = list(live.get("Statement", []))
for item in desired.get("Statement", []):
if item.get("Sid") not in present:
statements.append(item)
present.add(item.get("Sid"))
policy = {
"Version": live.get("Version") or desired.get("Version"),
"Id": live.get("Id") or desired.get("Id"),
"Statement": statements,
}
receipt = {"before": [item.get("Sid") for item in live.get("Statement", [])]}
if receipt["before"] == [item.get("Sid") for item in statements]:
receipt["put"] = "unchanged"
else:
_client(writer_pair).put_bucket_policy(Bucket=BUCKET, Policy=json.dumps(policy))
receipt["put"] = "ok"
after = json.loads(reader.get_bucket_policy(Bucket=BUCKET)["Policy"])
receipt["after"] = [item.get("Sid") for item in after.get("Statement", [])]
receipt["mas"] = policy_observation(after)
_write_json(RECEIPT_DIR / "backup-bucket-policy-apply.json", receipt)
if not receipt["mas"]["object_delete_absent"] or receipt["mas"]["mas_sids_present"] != list(MAS_SIDS):
raise GateError("mas_policy_not_installed")
return receipt
def prove_denials(pair):
client = _client(pair)
results = {
"sibling_prefix": _status(
lambda: client.list_objects_v2(Bucket=BUCKET, Prefix=SIBLING, MaxKeys=1)
),
"retained_delete": _status(
lambda: client.delete_object(Bucket=BUCKET, Key=PREFIX + "retained-delete-probe")
),
"administration": _status(lambda: client.get_bucket_policy(Bucket=BUCKET)),
"own_prefix": _status(
lambda: client.list_objects_v2(Bucket=BUCKET, Prefix=PREFIX, MaxKeys=1)
),
}
observation = denial_observation(results)
observation["preferred_project"] = PREFERRED_PROJECT
_write_json(RECEIPT_DIR / "mas-evidence-publisher-denials.json", observation)
if not observation["passed"]:
raise GateError("publisher_denials_failed")
return observation
def _writer_identity(auth):
"""Use the create response so the two token uses stay available for put and revoke."""
policies = list(auth.get("policies") or [])
ttl = auth.get("lease_duration")
uses = auth.get("num_uses")
if "default" in policies or policies != ["backup-pair-writer"]:
raise GateError("writer_identity_rejected")
if not isinstance(ttl, int) or not isinstance(uses, int):
raise GateError("writer_identity_rejected")
return {
"policies": policies,
"identity_policies": list(auth.get("identity_policies") or []),
"ttl": ttl,
"num_uses": uses,
}
class _Transport:
def __init__(self, token, absent):
self.token = token
self.absent = absent
def metadata(self, path):
if logical_path(path) != WORKLOAD or not self.absent:
raise GateError("pair_metadata_rejected")
return None
def put_cas0(self, path, body):
handle = tempfile.NamedTemporaryFile(
"w", prefix="mas-pair-", dir=RECEIPT_DIR, delete=False
)
name = Path(handle.name)
try:
json.dump(body["data"], handle)
handle.flush()
os.fchmod(handle.fileno(), 0o600)
handle.close()
proc = _bao(
["kv", "put", "-format=json", "-cas=0", logical_path(path), f"@{name}"],
token=self.token,
)
finally:
name.unlink(missing_ok=True)
if proc.returncode:
raise GateError("pair_write_failed")
return version_from_put(json.loads(proc.stdout))
def revoke(self):
_bao(["token", "revoke", "-self"], token=self.token)
def _custody():
import importlib.util
spec = importlib.util.spec_from_file_location(
"application_backup_pair_custody", ROOT / "scripts/application_backup_pair_custody.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def write_pair(pair):
custody = _custody()
existing = _bao(["kv", "metadata", "get", "-format=json", WORKLOAD])
if existing.returncode == 0:
raise GateError("existing_custody_requires_rotation_review")
written = _bao(["policy", "write", "backup-pair-writer", str(POLICY_FILE)])
if written.returncode:
raise GateError("writer_policy_install_failed")
created = _bao(
[
"token",
"create",
"-policy=backup-pair-writer",
"-ttl=5m",
"-use-limit=2",
"-no-default-policy",
"-orphan",
"-format=json",
]
)
if created.returncode:
raise GateError("writer_token_failed")
auth = json.loads(created.stdout)["auth"]
token = auth["client_token"]
try:
identity = _writer_identity(auth)
receipt = custody.provision("mas", pair, identity, _Transport(token, absent=True))
except Exception:
_bao(["token", "revoke", "-self"], token=token)
raise
meta = _bao(["kv", "metadata", "get", "-format=json", WORKLOAD])
if meta.returncode:
raise GateError("pair_metadata_unreadable")
data = json.loads(meta.stdout).get("data") or {}
names = sorted(_kv_data(WORKLOAD))
summary = {
"path": WORKLOAD,
"current_version": data.get("current_version"),
"field_names": names,
"cas": 0,
"kv_version": receipt["kv_version"],
"values_recorded": False,
}
if summary["current_version"] != 1 or names != ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]:
raise GateError("pair_metadata_rejected")
_write_json(RECEIPT_DIR / "mas-evidence-pair.json", summary)
return summary
def main():
sys.stdout = open(os.devnull, "w")
sys.stderr = open(os.devnull, "w")
try:
_ensure_boto()
RECEIPT_DIR.mkdir(mode=0o700, exist_ok=True)
supplier = supplier_pair(_kv_data(SUPPLIER))
backup = _kv_data("platform/workloads/railiance/backup/object-storage")
bootstrap = _kv_data("platform/workloads/railiance/scaleway/bootstrap")
reader = supplier_pair(backup)
writer = supplier_pair(bootstrap)
apply_live_policy(reader, writer)
prove_denials(supplier)
write_pair(supplier)
except GateError as exc:
_write_json(RECEIPT_DIR / "mas-evidence-publisher-gate.json", {"status": "failed", "code": exc.code})
return 71
except Exception:
_write_json(
RECEIPT_DIR / "mas-evidence-publisher-gate.json",
{"status": "failed", "code": "contained_gate_failure"},
)
return 72
_write_json(RECEIPT_DIR / "mas-evidence-publisher-gate.json", {"status": "passed", "values_recorded": False})
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except SystemExit:
raise
except Exception:
raise SystemExit(72) from None

View file

@ -0,0 +1,94 @@
"""MAS publisher gate records boundaries and refuses to keep secret values."""
import importlib.util
import json
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"mas_gate", ROOT / "scripts/mas_evidence_publisher_gate.py"
)
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
def test_supplier_pair_maps_aliases_and_refuses_partial_or_equal_values():
mapped = gate.supplier_pair({"ACCESS_KEY": "access-one", "SECRET_KEY": "secret-two"})
assert list(mapped) == ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
assert mapped["AWS_ACCESS_KEY_ID"] != mapped["AWS_SECRET_ACCESS_KEY"]
with pytest.raises(gate.GateError):
gate.supplier_pair({"ACCESS_KEY": "access-one"})
with pytest.raises(gate.GateError):
gate.supplier_pair({"ACCESS_KEY": "same-value", "SECRET_KEY": "same-value"})
def test_policy_observation_requires_the_three_prefix_statements_without_delete():
document = {
"Statement": [
{
"Sid": "MasEvidenceBackupObjects",
"Action": ["s3:GetObject", "s3:PutObject"],
},
{"Sid": "MasEvidenceBackupProbe", "Action": ["s3:GetBucketLocation"]},
{"Sid": "MasEvidenceBackupList", "Action": ["s3:ListBucket"]},
{"Sid": "Other", "Action": ["s3:DeleteObject"]},
]
}
observed = gate.policy_observation(document)
assert observed["mas_sids_present"] == list(gate.MAS_SIDS)
assert observed["object_delete_absent"] is True
assert observed["principal"] == gate.APPLICATION_ID
document["Statement"][0]["Action"].append("s3:DeleteObject")
assert gate.policy_observation(document)["object_delete_absent"] is False
def test_denial_observation_passes_only_when_all_three_boundaries_deny():
results = {
"sibling_prefix": {"http_status": 403, "denied": True},
"retained_delete": {"http_status": 403, "denied": True},
"administration": {"http_status": 403, "denied": True},
"own_prefix": {"http_status": 200, "denied": False},
}
passed = gate.denial_observation(results)
assert passed["passed"] is True
assert passed["application_id"] == "25a4cbca-220e-40c0-9af5-6c7713000174"
results["retained_delete"]["denied"] = False
assert gate.denial_observation(results)["passed"] is False
rendered = json.dumps(passed)
assert "access-one" not in rendered
assert "secret-two" not in rendered
def test_put_parser_keeps_version_and_drops_any_nested_value():
parsed = gate.version_from_put(
{"request_id": "req-1", "data": {"version": 1, "data": {"AWS_SECRET_ACCESS_KEY": "not-a-live-value"}}}
)
assert parsed == {"version": 1, "request_id": "req-1"}
assert "not-a-live-value" not in json.dumps(parsed)
with pytest.raises(gate.GateError):
gate.version_from_put({"data": {"version": 2}})
def test_logical_path_strips_only_the_openbao_storage_marker():
assert gate.logical_path("platform/metadata/workloads/mas/backup-primary") == (
"platform/workloads/mas/backup-primary"
)
assert gate.logical_path("platform/data/workloads/mas/backup-primary") == (
"platform/workloads/mas/backup-primary"
)
def test_pair_custody_preview_includes_mas_and_the_writer_policy_names_it():
spec_custody = importlib.util.spec_from_file_location(
"pair_custody_for_gate", ROOT / "scripts/application_backup_pair_custody.py"
)
custody = importlib.util.module_from_spec(spec_custody)
spec_custody.loader.exec_module(custody)
preview = custody.preview("mas")
assert preview["kv_path"] == "platform/workloads/mas/backup-primary"
assert preview["openbao_write"] is False
text = (ROOT / "openbao/policies/backup-pair-writer.hcl").read_text(encoding="utf-8")
assert 'path "platform/data/workloads/mas/backup-primary"' in text
assert "read" not in text.split('path "platform/data/workloads/mas/backup-primary"')[1].split("path ")[0]

View file

@ -77,3 +77,5 @@ workload KV pair and `/etc/mas-backup/primary.env` do not exist.
Pair-custody lists still
name only coulomb-social and email-connect.
Design: `docs/credential-lane-designs/mas-evidence-backup-grant-2026-11-02.md`.
2026-11-02: `scripts/mas_evidence_publisher_gate.py` is the silent gate for the live prefix policy, the three denials, and the one CAS=0 workload pair. The pair-writer source now names the MAS path. An attended platform-admin login exited 10 with category `callback_timeout` before that gate started. The live policy, workload pair, delivery admission, node file, and offsite object are unchanged.