Fix rotation review evidence and reconcile blocked S1 workplans
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e3b9-b19e-7ba1-9eb4-4faea76af3ea
This commit is contained in:
parent
41f25fe42d
commit
9886567b40
10 changed files with 255 additions and 18 deletions
8
Makefile
8
Makefile
|
|
@ -16,7 +16,7 @@ IMG ?= ubuntu-24.04
|
||||||
USER ?= admin
|
USER ?= admin
|
||||||
|
|
||||||
# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally)
|
# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally)
|
||||||
HCLOUD_TOKEN := $(shell sops -d --extract '["hetzner"]["token"]' secrets/hetzner-token.yaml 2>/dev/null)
|
HCLOUD_TOKEN = $(shell sops -d --extract '["hetzner"]["token"]' secrets/hetzner-token.yaml 2>/dev/null)
|
||||||
|
|
||||||
# ---- Help ----
|
# ---- Help ----
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
|
|
@ -112,7 +112,7 @@ tf-destroy: ## Terraform destroy (exact approval required before init)
|
||||||
|
|
||||||
# --- Terraform provider/lockfile helpers ---
|
# --- Terraform provider/lockfile helpers ---
|
||||||
TF_DIR := terraform/hetzner
|
TF_DIR := terraform/hetzner
|
||||||
TF_TOKEN := $(HCLOUD_TOKEN)
|
TF_TOKEN = $(HCLOUD_TOKEN)
|
||||||
LOCKFILE := $(TF_DIR)/.terraform.lock.hcl
|
LOCKFILE := $(TF_DIR)/.terraform.lock.hcl
|
||||||
|
|
||||||
tf-lock-commit: ## Commit the current provider lockfile
|
tf-lock-commit: ## Commit the current provider lockfile
|
||||||
|
|
@ -287,8 +287,8 @@ PLAY := $(ANS_DIR)/playbooks/bootstrap.yaml
|
||||||
SSH_USER ?=
|
SSH_USER ?=
|
||||||
ANSIBLE_USER_FLAG := $(if $(SSH_USER),-u $(SSH_USER),)
|
ANSIBLE_USER_FLAG := $(if $(SSH_USER),-u $(SSH_USER),)
|
||||||
|
|
||||||
# Load your SOPS key for decryption when running playbooks (optional if you use keys.txt)
|
# SOPS reads its standard key file itself; preserve an explicitly supplied
|
||||||
export SOPS_AGE_KEY := $(shell cat ~/.config/sops/age/keys.txt 2>/dev/null)
|
# SOPS_AGE_KEY without reading/exporting private keys for unrelated Make targets.
|
||||||
|
|
||||||
ansible-help: ## Show common Ansible commands
|
ansible-help: ## Show common Ansible commands
|
||||||
@echo "Convergence targets:"
|
@echo "Convergence targets:"
|
||||||
|
|
|
||||||
54
docs/evidence/2026-09-27-loose-ends.md
Normal file
54
docs/evidence/2026-09-27-loose-ends.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Existing workplan closeout review — 2026-09-27
|
||||||
|
|
||||||
|
Reviewed all root and archived workplans. The only unfinished plans are
|
||||||
|
RAIL-HO-WP-0011, 0012 and 0013; all now have state `blocked`, with eight
|
||||||
|
remaining tasks in `wait`. No new task or workplan was opened. No residual task
|
||||||
|
has been marked done without its required live acceptance evidence.
|
||||||
|
|
||||||
|
## Implemented under WP-0011 T06/T08
|
||||||
|
|
||||||
|
- Rotation dry-run output exposes the exact metadata-only review plan.
|
||||||
|
- Approval binds each changed ciphertext's SHA-256 as well as recipients/path.
|
||||||
|
- Applied receipts retain the original and resulting recipient/hash evidence.
|
||||||
|
- Unrelated Make targets no longer eagerly decrypt the Hetzner token or read
|
||||||
|
and export the local age private key.
|
||||||
|
|
||||||
|
Validation: 54 Python unit tests pass, including three new rotation regression
|
||||||
|
tests. Inventory, baseline parity, read-only handoff contract, protected secret
|
||||||
|
paths, SOPS recipient metadata and whitespace checks pass. Ansible-core 2.17.13
|
||||||
|
host-time playbook syntax check passes in a disposable controller environment.
|
||||||
|
No production decryption, recipient rotation or credential retrieval occurred.
|
||||||
|
|
||||||
|
## Read-only host verification
|
||||||
|
|
||||||
|
`ansible-playbook playbooks/verify.yaml`, Ansible-core 2.17.13, reached both hosts:
|
||||||
|
|
||||||
|
| Host | Checks completed | Blocking assertion | Changes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| CoulombCore | Executable and baseline stat | `/usr/local/bin/goss` absent | 0 |
|
||||||
|
| Railiance01 | Executable and baseline stat | Installed baseline digest differs from source render | 0 |
|
||||||
|
|
||||||
|
The initial sandboxed attempt failed writing Ansible's connection cache; the
|
||||||
|
rerun with that access produced the host findings above. Neither attempt is a
|
||||||
|
passing handoff. Host refresh includes installation/configuration and an hourly
|
||||||
|
timer, so the concrete rendered diff must be reviewed before that separate
|
||||||
|
mutation; subsequent baseline failures must also be resolved before T05 closes.
|
||||||
|
|
||||||
|
## Backup dependency correction
|
||||||
|
|
||||||
|
The exact S1 offsite contract remains pending:
|
||||||
|
`d150eb3e6a19d658aa76c930b32fc20ef75ffa399558fc193fbde0738551ee62`.
|
||||||
|
Warden's route reports unknown execution workload identity. Platform WP-0029's
|
||||||
|
September 15 closure resolves the old upload-share incident, but does not accept
|
||||||
|
this contract or prove S1 transfer/restore. Keep WP-0012 T05/T06 waiting for those
|
||||||
|
specific owner and recovery receipts.
|
||||||
|
|
||||||
|
## Clock dependency check
|
||||||
|
|
||||||
|
The existing railiance-clock collector produced
|
||||||
|
`2026-09-27-railiance01-clock-inventory.json`. Read-only `dpkg-query -W systemd
|
||||||
|
systemd-timesyncd` returned `255.4-1ubuntu8.17` for both. Effective source/poll
|
||||||
|
observations and configuration hashes are recorded in the receipt and WP-0013.
|
||||||
|
RCLK-WP-0002 still lacks completed policy review; no disposable Ubuntu VM is
|
||||||
|
declared here for reboot/outage/rollback testing. The already deployed authority
|
||||||
|
does not close those gates. No clocks or services were changed.
|
||||||
48
docs/evidence/2026-09-27-railiance01-clock-inventory.json
Normal file
48
docs/evidence/2026-09-27-railiance01-clock-inventory.json
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"host_alias": "railiance01",
|
||||||
|
"collector_reported_at": "2026-09-27T16:46:14.477816+00:00",
|
||||||
|
"read_only": true,
|
||||||
|
"authority_usable": null,
|
||||||
|
"synchronized_reported": true,
|
||||||
|
"limitations": [
|
||||||
|
"OS synchronization flag is not an independent UTC error bound",
|
||||||
|
"host and collector timestamps are observations, not authenticated time samples",
|
||||||
|
"SSH uses existing caller identity/trust; no sudo, installation or time mutation"
|
||||||
|
],
|
||||||
|
"collector_sha256": "3aadb6fb888d87cefb63e94ac87c9c640ef63ef6f404b09a01e8e8642b26784e",
|
||||||
|
"remote_script_sha256": "597e08b95855c4bd1fee40ad8e7c1a8359312c1583ac8363e5bd922b493f074a",
|
||||||
|
"inventory": {
|
||||||
|
"remote_reported_at": "2026-09-27T16:46:16.708853+00:00",
|
||||||
|
"commands": {
|
||||||
|
"clock": {
|
||||||
|
"returncode": 0,
|
||||||
|
"stdout": "Timezone=Etc/UTC\nLocalRTC=no\nNTP=yes\nNTPSynchronized=yes\n"
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"returncode": 0,
|
||||||
|
"stdout": "LinkNTPServers=\nSystemNTPServers=ntp.ubuntu.com\nRuntimeNTPServers=\nFallbackNTPServers=ntp.ubuntu.com\nServerName=ntp.ubuntu.com\nServerAddress=2620:2d:4000:1::41\nRootDistanceMaxUSec=500ms\nPollIntervalMinUSec=32s\nPollIntervalMaxUSec=1min 4s\nPollIntervalUSec=1min 4s\nNTPMessage={ Leap=0, Version=4, Mode=4, Stratum=2, Precision=-25, RootDelay=6.454ms, RootDispersion=183us, Reference=1D586304, OriginateTimestamp=Sun 2026-09-27 16:45:18 UTC, ReceiveTimestamp=Sun 2026-09-27 16:45:18 UTC, TransmitTimestamp=Sun 2026-09-27 16:45:18 UTC, DestinationTimestamp=Sun 2026-09-27 16:45:18 UTC, Ignored=no, PacketCount=15904, Jitter=3.125ms }\nFrequency=1174453\n"
|
||||||
|
},
|
||||||
|
"timesyncd": {
|
||||||
|
"returncode": 0,
|
||||||
|
"stdout": "User=systemd-timesync\nActiveState=active\nFragmentPath=/usr/lib/systemd/system/systemd-timesyncd.service\nDropInPaths=\nUnitFileState=enabled\n"
|
||||||
|
},
|
||||||
|
"other_daemons": {
|
||||||
|
"returncode": 4,
|
||||||
|
"stdout": "inactive\ninactive\ninactive\n"
|
||||||
|
},
|
||||||
|
"virtualization": {
|
||||||
|
"returncode": 0,
|
||||||
|
"stdout": "kvm\n"
|
||||||
|
},
|
||||||
|
"ntp_listeners": {
|
||||||
|
"returncode": 0,
|
||||||
|
"stdout": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_sha256": {
|
||||||
|
"/etc/systemd/timesyncd.conf": "e6734751f8aaf19fddfff891ad246387f5f59bd9ff1a5f0cac2c34bc81941c62",
|
||||||
|
"/etc/systemd/timesyncd.conf.d/60-railiance-clock.conf": "c787da5279c983dc6284ed16258fa2ff069cc62b53f360f4f3fed5bb94f79d5f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Metadata-only example. Copy outside Git for an attended approved rotation.
|
# Metadata-only example. Copy outside Git for an attended approved rotation.
|
||||||
approved: false
|
approved: false
|
||||||
approved_by: "operator-name"
|
approved_by: "reviewer-name"
|
||||||
approved_at: "2026-08-23T00:00:00Z"
|
approved_at: "2026-08-23T00:00:00Z"
|
||||||
changes:
|
changes:
|
||||||
- path: secrets/hetzner-token.yaml
|
- path: secrets/hetzner-token.yaml
|
||||||
|
sha256: "replace-with-exact-ciphertext-sha256-from-plan"
|
||||||
before_recipients:
|
before_recipients:
|
||||||
- age1old-example-not-valid
|
- age1old-example-not-valid
|
||||||
after_recipients:
|
after_recipients:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ python3 scripts/sops_rotation.py --check
|
||||||
|
|
||||||
It compares each protected file's public age-recipient metadata with the first
|
It compares each protected file's public age-recipient metadata with the first
|
||||||
matching rule in `.sops.yaml`. CI runs this check to detect recipient drift.
|
matching rule in `.sops.yaml`. CI runs this check to detect recipient drift.
|
||||||
|
The JSON output includes the exact file paths, ciphertext SHA-256 hashes, and
|
||||||
|
before/after recipient sets. Run without `--check` to inspect proposed drift.
|
||||||
|
|
||||||
An attended non-printing decryption check may emit a receipt:
|
An attended non-printing decryption check may emit a receipt:
|
||||||
|
|
||||||
|
|
@ -21,7 +23,10 @@ receipt or command output.
|
||||||
|
|
||||||
Actual key updates require `--apply` and an approval YAML containing
|
Actual key updates require `--apply` and an approval YAML containing
|
||||||
`approved: true`, `approved_by`, `approved_at`, and an exact `changes` list from
|
`approved: true`, `approved_by`, `approved_at`, and an exact `changes` list from
|
||||||
the current plan. The command fails if that list differs from current metadata.
|
the current plan (including each changed file's `sha256`). The command fails if
|
||||||
|
that list differs from current metadata or the reviewed ciphertext has changed.
|
||||||
|
Applied receipts retain the reviewed before/after recipient sets and original
|
||||||
|
ciphertext hash, plus `after_sha256` for the resulting ciphertext.
|
||||||
Review and preserve recovery-key custody before approving recipient removal.
|
Review and preserve recovery-key custody before approving recipient removal.
|
||||||
Start from `docs/sops-rotation-approval.example.yaml`; the committed example is
|
Start from `docs/sops-rotation-approval.example.yaml`; the committed example is
|
||||||
deliberately unapproved and contains no usable recipient.
|
deliberately unapproved and contains no usable recipient.
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ def _load_approval(path: Path, plan: list[dict[str, Any]]) -> None:
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
"path": item["path"],
|
"path": item["path"],
|
||||||
|
"sha256": item["sha256"],
|
||||||
"before_recipients": item["before_recipients"],
|
"before_recipients": item["before_recipients"],
|
||||||
"after_recipients": item["after_recipients"],
|
"after_recipients": item["after_recipients"],
|
||||||
}
|
}
|
||||||
|
|
@ -202,9 +203,13 @@ def main() -> int:
|
||||||
raise RotationError("--apply requires at least one recipient change")
|
raise RotationError("--apply requires at least one recipient change")
|
||||||
_load_approval(args.approval_file, plan)
|
_load_approval(args.approval_file, plan)
|
||||||
_apply(plan)
|
_apply(plan)
|
||||||
plan = rotation_plan()
|
after_plan = rotation_plan()
|
||||||
if any(item["changed"] for item in plan):
|
if any(item["changed"] for item in after_plan):
|
||||||
raise RotationError("recipient drift remains after rotation")
|
raise RotationError("recipient drift remains after rotation")
|
||||||
|
if [item["path"] for item in after_plan] != [item["path"] for item in plan]:
|
||||||
|
raise RotationError("protected file inventory changed during rotation")
|
||||||
|
for before, after in zip(plan, after_plan):
|
||||||
|
before["after_sha256"] = after["sha256"]
|
||||||
verified = _verify_decryption(protected_files()) if args.verify_decryption or args.apply else False
|
verified = _verify_decryption(protected_files()) if args.verify_decryption or args.apply else False
|
||||||
receipt = build_receipt(plan, verified, args.apply)
|
receipt = build_receipt(plan, verified, args.apply)
|
||||||
if args.receipt:
|
if args.receipt:
|
||||||
|
|
@ -219,6 +224,7 @@ def main() -> int:
|
||||||
"changes": sum(1 for item in plan if item["changed"]),
|
"changes": sum(1 for item in plan if item["changed"]),
|
||||||
"decryption_verified": verified,
|
"decryption_verified": verified,
|
||||||
"applied": args.apply,
|
"applied": args.apply,
|
||||||
|
"plan": plan,
|
||||||
},
|
},
|
||||||
sort_keys=True,
|
sort_keys=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(ROOT / "scripts"))
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
@ -13,6 +19,7 @@ sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
from check_secret_paths import is_encrypted_content, is_protected_path # noqa: E402
|
from check_secret_paths import is_encrypted_content, is_protected_path # noqa: E402
|
||||||
from s1_receipt import ReceiptError, load_receipt, validate_receipt # noqa: E402
|
from s1_receipt import ReceiptError, load_receipt, validate_receipt # noqa: E402
|
||||||
from sops_rotation import rotation_plan # noqa: E402
|
from sops_rotation import rotation_plan # noqa: E402
|
||||||
|
import sops_rotation # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class SecretAndReceiptContractTests(unittest.TestCase):
|
class SecretAndReceiptContractTests(unittest.TestCase):
|
||||||
|
|
@ -74,6 +81,53 @@ class SecretAndReceiptContractTests(unittest.TestCase):
|
||||||
self.assertTrue(plan)
|
self.assertTrue(plan)
|
||||||
self.assertFalse(any(item["changed"] for item in plan))
|
self.assertFalse(any(item["changed"] for item in plan))
|
||||||
|
|
||||||
|
def test_rotation_approval_binds_ciphertext(self) -> None:
|
||||||
|
plan = [{"path": "secrets/example.yaml", "sha256": "a" * 64,
|
||||||
|
"before_recipients": ["age1before"],
|
||||||
|
"after_recipients": ["age1after"], "changed": True}]
|
||||||
|
approval = {"approved": True, "approved_by": "reviewer",
|
||||||
|
"approved_at": "2026-09-27T00:00:00Z",
|
||||||
|
"changes": [{k: v for k, v in plan[0].items() if k != "changed"}]}
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp) / "approval.yaml"
|
||||||
|
path.write_text(yaml.safe_dump(approval))
|
||||||
|
sops_rotation._load_approval(path, plan)
|
||||||
|
plan[0]["sha256"] = "b" * 64
|
||||||
|
with self.assertRaises(sops_rotation.RotationError):
|
||||||
|
sops_rotation._load_approval(path, plan)
|
||||||
|
|
||||||
|
def test_applied_rotation_keeps_reviewed_before_and_after(self) -> None:
|
||||||
|
before = [{"path": "secrets/example.yaml", "sha256": "a" * 64,
|
||||||
|
"before_recipients": ["age1before"],
|
||||||
|
"after_recipients": ["age1after"], "changed": True}]
|
||||||
|
after = [{"path": "secrets/example.yaml", "sha256": "b" * 64,
|
||||||
|
"before_recipients": ["age1after"],
|
||||||
|
"after_recipients": ["age1after"], "changed": False}]
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
receipt_path = Path(tmp) / "receipt.json"
|
||||||
|
with patch.object(sys, "argv", ["rotation", "--apply", "--approval-file",
|
||||||
|
"approval.yaml", "--receipt", str(receipt_path)]), \
|
||||||
|
patch.object(sops_rotation, "rotation_plan", side_effect=[before, after]), \
|
||||||
|
patch.object(sops_rotation, "_load_approval"), \
|
||||||
|
patch.object(sops_rotation, "_apply"), \
|
||||||
|
patch.object(sops_rotation, "_verify_decryption", return_value=True), \
|
||||||
|
contextlib.redirect_stdout(io.StringIO()) as output:
|
||||||
|
self.assertEqual(sops_rotation.main(), 0)
|
||||||
|
receipt = json.loads(receipt_path.read_text())
|
||||||
|
self.assertEqual(receipt["before_recipients"], ["age1before"])
|
||||||
|
self.assertEqual(receipt["after_recipients"], ["age1after"])
|
||||||
|
self.assertEqual(receipt["file_metadata"][0]["sha256"], "a" * 64)
|
||||||
|
self.assertEqual(receipt["file_metadata"][0]["after_sha256"], "b" * 64)
|
||||||
|
self.assertEqual(json.loads(output.getvalue())["changes"], 1)
|
||||||
|
|
||||||
|
def test_default_rotation_output_contains_reviewable_plan(self) -> None:
|
||||||
|
with patch.object(sys, "argv", ["rotation"]), \
|
||||||
|
contextlib.redirect_stdout(io.StringIO()) as output:
|
||||||
|
self.assertEqual(sops_rotation.main(), 0)
|
||||||
|
payload = json.loads(output.getvalue())
|
||||||
|
self.assertEqual(payload["plan"], rotation_plan(ROOT))
|
||||||
|
self.assertFalse(payload["decryption_verified"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ type: workplan
|
||||||
title: "Make the S1 declaration reproducible and the handoff verifiably green"
|
title: "Make the S1 declaration reproducible and the handoff verifiably green"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-infra
|
repo: railiance-infra
|
||||||
status: active
|
status: blocked
|
||||||
flavor: implementation
|
flavor: implementation
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-08-23"
|
created: "2026-08-23"
|
||||||
updated: "2026-08-23"
|
updated: "2026-09-27"
|
||||||
related:
|
related:
|
||||||
- RAIL-HO-WP-0002
|
- RAIL-HO-WP-0002
|
||||||
- RAIL-HO-WP-0009
|
- RAIL-HO-WP-0009
|
||||||
|
|
@ -313,3 +313,27 @@ Current evidence (2026-08-23):
|
||||||
- Forgejo Actions run 79 is green for revision `4f2312a` across all three jobs.
|
- Forgejo Actions run 79 is green for revision `4f2312a` across all three jobs.
|
||||||
- Pending before finish: an attended fresh all-host handoff receipt and an
|
- Pending before finish: an attended fresh all-host handoff receipt and an
|
||||||
attended non-printing SOPS decryption receipt.
|
attended non-printing SOPS decryption receipt.
|
||||||
|
|
||||||
|
## Closeout review — 2026-09-27
|
||||||
|
|
||||||
|
T08 source gaps repaired: the default metadata-only output now includes exact
|
||||||
|
file paths, ciphertext hashes and before/after recipient sets; approval binds
|
||||||
|
the ciphertext hash; applied receipts preserve the reviewed original recipients
|
||||||
|
and hash alongside the resulting hash. Three regression tests cover these cases.
|
||||||
|
Unrelated Make targets no longer decrypt the provider token or read/export the
|
||||||
|
private age key during Makefile evaluation. No credential was rotated.
|
||||||
|
|
||||||
|
T05 remains `wait`: a read-only Ansible 2.17.13 run reached both hosts with
|
||||||
|
`changed=0`. CoulombCore lacks `/usr/local/bin/goss`; Railiance01's installed
|
||||||
|
baseline checksum differs from the source-rendered profile. The next step is a
|
||||||
|
reviewed Goss refresh for each host, followed by remediation of any actual
|
||||||
|
baseline failures and a clean-revision all-host handoff. A failed surface check
|
||||||
|
is not green host evidence. See `docs/evidence/2026-09-27-loose-ends.md`.
|
||||||
|
|
||||||
|
T08 remains `wait`: source tests and metadata checks pass, but this workstation
|
||||||
|
has no SOPS executable or approved decryption session for the repository's
|
||||||
|
recipient. The existing attended non-printing decryption receipt is still
|
||||||
|
required; mock tests do not substitute for it. The host having SOPS installed
|
||||||
|
does not establish approved recovery-key custody.
|
||||||
|
|
||||||
|
Workplan is `blocked` until those live verification prerequisites are met.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ type: workplan
|
||||||
title: "Close the encrypted S1 backup and recovery loop"
|
title: "Close the encrypted S1 backup and recovery loop"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-infra
|
repo: railiance-infra
|
||||||
status: active
|
status: blocked
|
||||||
flavor: implementation
|
flavor: implementation
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-08-23"
|
created: "2026-08-23"
|
||||||
updated: "2026-08-23"
|
updated: "2026-09-27"
|
||||||
related:
|
related:
|
||||||
- RAIL-HO-WP-0011
|
- RAIL-HO-WP-0011
|
||||||
state_hub_workstream_id: "5ea28f8f-376c-5230-8bb7-ca871c1a75f4"
|
state_hub_workstream_id: "5ea28f8f-376c-5230-8bb7-ca871c1a75f4"
|
||||||
|
|
@ -132,7 +132,7 @@ no timer is installed merely by running a verification command.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-HO-WP-0012-T05
|
id: RAIL-HO-WP-0012-T05
|
||||||
status: progress
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "783dff8b-849e-5ed9-a668-af1184be7bdd"
|
state_hub_task_id: "783dff8b-849e-5ed9-a668-af1184be7bdd"
|
||||||
```
|
```
|
||||||
|
|
@ -216,3 +216,21 @@ decrypted configuration.
|
||||||
- T05 is source-prepared and pending owner acceptance; T06 remains `wait`. No
|
- T05 is source-prepared and pending owner acceptance; T06 remains `wait`. No
|
||||||
off-host write, retained-artifact deletion, private-key access, or live-host
|
off-host write, retained-artifact deletion, private-key access, or live-host
|
||||||
restore occurred.
|
restore occurred.
|
||||||
|
|
||||||
|
## Closeout review — 2026-09-27
|
||||||
|
|
||||||
|
T05 is `wait`, not ongoing implementation. The exact S1 acceptance file remains
|
||||||
|
`pending`; no owner decision accepts this envelope/projection. Route lookup still
|
||||||
|
reports unknown workload identity. Source upload/collision/redirect/retention
|
||||||
|
and isolated fixture recovery tests pass, but no approved live S1 transfer or
|
||||||
|
owner retrieval receipt exists. T06 consequently remains `wait` for that copy
|
||||||
|
and attended recovery-key custody and isolated restore.
|
||||||
|
|
||||||
|
The older upload-credential incident is no longer a blocker: platform
|
||||||
|
`RPF-WP-0029` closed on 2026-09-15 with predecessor-share invalidation attestation
|
||||||
|
and replacement transport/application recovery evidence. That evidence concerns
|
||||||
|
the platform's archive, not this S1 bundle, and does not accept the S1 contract.
|
||||||
|
Historical recovery-key exposure remains separately recorded by the owner.
|
||||||
|
|
||||||
|
Workplan is `blocked` on exact owner acceptance, controlled S1 transfer and an
|
||||||
|
attended S1 restore drill. No new task or workplan was created.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ type: workplan
|
||||||
title: "Declare and verify the Railiance host UTC baseline"
|
title: "Declare and verify the Railiance host UTC baseline"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-infra
|
repo: railiance-infra
|
||||||
status: active
|
status: blocked
|
||||||
flavor: planning
|
flavor: planning
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-09-14"
|
created: "2026-09-14"
|
||||||
updated: "2026-09-15"
|
updated: "2026-09-27"
|
||||||
related_workplans:
|
related_workplans:
|
||||||
- RCLK-WP-0005
|
- RCLK-WP-0005
|
||||||
- RCLK-WP-0002
|
- RCLK-WP-0002
|
||||||
|
|
@ -33,7 +33,7 @@ and tools/observe_host_clock.py. No configuration or clock change was made.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-HO-WP-0013-T01
|
id: RAIL-HO-WP-0013-T01
|
||||||
status: todo
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "9e5db140-0642-58de-8a9c-1c5a3cfd7b6e"
|
state_hub_task_id: "9e5db140-0642-58de-8a9c-1c5a3cfd7b6e"
|
||||||
```
|
```
|
||||||
|
|
@ -49,7 +49,7 @@ Define what source health can honestly claim before exposing it to the clock app
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-HO-WP-0013-T02
|
id: RAIL-HO-WP-0013-T02
|
||||||
status: progress
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "75f17ffa-b781-549d-82ce-d19d431c2618"
|
state_hub_task_id: "75f17ffa-b781-549d-82ce-d19d431c2618"
|
||||||
```
|
```
|
||||||
|
|
@ -81,7 +81,7 @@ A mocked systemctl result or container-only check is not host synchronization pr
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-HO-WP-0013-T04
|
id: RAIL-HO-WP-0013-T04
|
||||||
status: progress
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "9adae3a6-8ee6-538b-9bb3-1ee32ed185e1"
|
state_hub_task_id: "9adae3a6-8ee6-538b-9bb3-1ee32ed185e1"
|
||||||
```
|
```
|
||||||
|
|
@ -111,3 +111,30 @@ railiance-platform docs/evidence/2026-09-15-railiance-clock-production.json.
|
||||||
Health collection needs read-only adjtimex; ProtectClock is disabled only on
|
Health collection needs read-only adjtimex; ProtectClock is disabled only on
|
||||||
that exporter, while both services retain empty capability sets. Disposable
|
that exporter, while both services retain empty capability sets. Disposable
|
||||||
outage/reboot/rollback rehearsals remain tracked in T03.
|
outage/reboot/rollback rehearsals remain tracked in T03.
|
||||||
|
|
||||||
|
## Closeout review — 2026-09-27
|
||||||
|
|
||||||
|
All four residual tasks are `wait`; deployed source alone does not meet their
|
||||||
|
acceptance criteria. T01 is blocked on RCLK-WP-0002 source/leap/error-model
|
||||||
|
acceptance; that owner's T01–T03 are still in progress and T04 is todo. T02 has
|
||||||
|
an implemented opt-in role and passes native Ansible syntax checking, but still
|
||||||
|
requires T01's reviewed policy and baseline/Goss health integration. T03 requires
|
||||||
|
an admitted disposable Ubuntu VM for convergence/no-op, drift, reboot,
|
||||||
|
source-outage/recovery and rollback evidence. No such test target is declared in
|
||||||
|
this repository. T04's prior live deployment evidence stands, but its required
|
||||||
|
T03 recovery evidence and steady-state handoff remain outstanding.
|
||||||
|
|
||||||
|
Refreshed read-only inventory:
|
||||||
|
`docs/evidence/2026-09-27-railiance01-clock-inventory.json`, collected with the
|
||||||
|
existing railiance-clock collector. Installed systemd and systemd-timesyncd:
|
||||||
|
`255.4-1ubuntu8.17`. Effective system/fallback source: `ntp.ubuntu.com`;
|
||||||
|
per-link and runtime source lists empty. Poll bounds 32–64 seconds, root-distance
|
||||||
|
threshold 500ms, observed leap 0. UTC, timesyncd active, synchronization reported,
|
||||||
|
no UDP/123 listener and no observed competing daemon. These are diagnostics,
|
||||||
|
not independent accuracy or source-independence proof. Configuration hashes are
|
||||||
|
in the receipt; no host configuration was changed.
|
||||||
|
|
||||||
|
The September 14 duplicate-workplan inbox warning is historical: the current
|
||||||
|
checkout has one WP-0013 file and already contains consolidation commit
|
||||||
|
`c402245`. Preserve its existing task UUIDs. Workplan is `blocked` on the
|
||||||
|
remaining review and native recovery prerequisites.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue