Prepare independent ledger backup and isolated no-send recovery
Some checks failed
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Transactional mail acceptance / mail (push) Failing after 1m15s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11538-05ea-7b63-b3c7-a2df5f38898d
This commit is contained in:
tegwick 2026-10-08 00:06:59 +02:00
parent c8c018496a
commit 2ff24e991d
11 changed files with 1837 additions and 0 deletions

View file

@ -0,0 +1,21 @@
[Unit]
Description=Coherent encrypted email-connect transactional ledger backup
After=network-online.target k3s.service
Wants=network-online.target
[Service]
Type=oneshot
User=root
UMask=0077
StateDirectory=email-connect-backup
StateDirectoryMode=0700
EnvironmentFile=/etc/email-connect-backup/primary.env
ExecStart=/usr/bin/python3 /opt/email-connect-backup/backup-ledger.py backup --execute
TimeoutStartSec=40min
TimeoutStopSec=15min
KillMode=mixed
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/email-connect-backup
ProtectHome=true

View file

@ -0,0 +1,11 @@
[Unit]
Description=Daily independent email-connect ledger recovery point
[Timer]
OnCalendar=*-*-* 04:15:00 UTC
Persistent=true
RandomizedDelaySec=5min
Unit=email-connect-ledger-backup.service
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,10 @@
[Unit]
Description=Check email-connect ledger archive age and local ciphertext hash
[Service]
Type=oneshot
User=root
ExecStart=/usr/bin/python3 /opt/email-connect-backup/backup-ledger.py freshness
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true

View file

@ -0,0 +1,10 @@
[Unit]
Description=Hourly email-connect ledger backup freshness check
[Timer]
OnCalendar=hourly
Persistent=true
Unit=email-connect-ledger-freshness.service
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,142 @@
# Email ledger backup/recovery candidate — 2026-10-08
EMAIL-WP-0007 implements the independent transactional SQLite recovery scope
requested by CSOC-WP-0010-T04/T05. The verification retry release remains live;
this candidate does not change that deployment, SMTP/caller credentials or inbox
access. The standalone node scripts and four systemd units are prepared for
platform admission. No live capture, primary upload or timer activation occurred.
## Scope and coherence
The ledger contains deliveries, send_attempts, verification_attempts,
verifications and suppressions. Preserve every row, including uncertain outcomes,
consumed/cancelled links and failed reservation history. Scanner databases,
provider mailboxes/message bodies, SMTP passwords and ingest tokens are separate
scopes and are not included. Application database/content backup cannot recover
this ledger.
capture-ledger.py is default server dry-run. It checks exact cluster UID,
current immutable receiver image, single Ready replica, Bound email-connect-data
PVC and /data/email-connect.db configuration. The helper runs UID10001, mounts
only the read-only PVC and bounded scratch space, has no environment credential
or ServiceAccount token, and must be selected by no-egress NetworkPolicies.
No SMTP code is started in the helper.
Under admitted --execute, a guarded replica patch pauses the receiver, waits for
its pods to disappear and rejects any other ledger PVC consumer. SQLite's backup
API reads committed data including WAL; copying only the main file would miss
uncheckpointed records. Integrity checks, exact five-table inventory and schema/
row fingerprints validate the private download. The default 128MiB SQLite ceiling
is a reviewed candidate budget, not a promise of unlimited ledger growth.
Finally removes the helper and resumes the unchanged receiver. Replica patches
test the original deployment template and expected count; a foreign rollout is
never overwritten or silently resumed. Failed cleanup/resume cannot declare a
successful capture. A nonblocking lock prevents overlapping captures. SIGTERM
attempts the same cleanup; host loss/SIGKILL still needs an explicit replica/helper
check. Receipts contain counts/hashes/booleans, never row contents or credentials.
## Publication and recovery
backup-ledger.py validates the capture, packages only database.sqlite3 and
receipt.json, then seals once using the platform public age recipient. A failed
publication preserves one private pending directory and blocks another capture;
reviewed retry reuses the ciphertext and object key without another pause.
Partial encryption remains for review. Successful publication clears only its
own staging directory; historical private pre-release copies remain untouched.
The platform primary_application_archive.py email-connect profile uses
platform-pg/application-archives/email-connect/ in the existing primary bucket.
Conditional multipart completion, returned version GET and ciphertext size/hash
verification gate last-success.json and inventory.json. Independent retrieval
requires only a separately retained version/hash/size receipt and recovery caller.
No publisher may delete retained archives, access other app prefixes, or hold an
age private identity. The dedicated hash-locked platform SDK runtime is reused.
The candidate node worker expects owner-delivered AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY in root-owned mode 0600
/etc/email-connect-backup/primary.env, parent mode 0700. A metadata-only node check found
that file absent. Existing SMTP custody and the databases backup Secret do not
admit this consumer. RPF-WP-0053-T05 tracks its separate prefix/caller, retention/
capacity, independently retained receipt and recovery identity admission. No
Secret was fetched/copied, ESO scope widened or provider key minted in this work.
After retrieving ciphertext, the attended custodian runs:
```bash
python3 scripts/backup-ledger.py recover --execute \
--archive /private/retrieved.tar.age --transfer-receipt /private/last-success.json \
--identity /private/recovery-identity --destination /private/recovered-ledger
python3 scripts/rehearse-ledger.py --execute \
--recovered /private/recovered-ledger --receipt /private/recovery-proof.json
```
Recovery verifies exact provider key/version/hash/size evidence, age-decrypts
privately, inspects the entire packet before extraction, accepts exactly two
regular files and rejects traversal, links, duplicates, extras and oversized
entries. It checks the inner image/capture/database/schema/row bindings. Files
are mode 0600 and directories mode 0700. A failed drill preserves private evidence; no
production database path is modified.
Rehearsal uses the already-cached pinned receiver image under UID10001, read-only
root filesystem, bounded tmpfs, dropped capabilities and --network none. It
compares schema/all-table row fingerprints before and after opening the current
SQLiteDeliveryStore, probes accepted/unknown reservations without a provider,
checks invalidated handles/suppression, and verifies the ledger remains unchanged.
No SMTP configuration is supplied. Only its own named disposable container is
removed on completion/failure. application_restore_verified means this isolated
ledger rehearsal; production_restored remains false and recipient inbox/reply
operations are not inferred from it.
## Historical-point recovery gate
A backup preserves state at its capture point. Operations after that point may
include new provider submissions, suppressions, or challenge consumption/
cancellation. Restoring an older point alone can lose a reservation and revive a
handle consumed later. Therefore an isolated successful drill never releases
production traffic automatically. Incident recovery must first fence callers and
outbox replay, keep sending/challenge consumption disabled, reconcile post-capture
operations with user-engine/provider evidence and the identity owner, then
invalidate or reconcile recovered challenges under an explicit incident decision.
Only that owner-approved reconciliation may release the receiver. Unknown outcomes
must remain unknown; no automatic resend or inference of non-delivery is allowed.
An RPO 24h proposal needs explicit acceptance of this loss window; consider a
shorter schedule if the ledger owner requires it. Daily timing is not a guarantee
of acceptable authentication recovery.
## Scheduling and operating acceptance
Proposed daily 04:15 UTC (+up to 5min jitter), hourly freshness threshold 26h and
provisional attended RTO 60min are pending targets. Install reviewed scripts in
/opt/email-connect-backup/ and four deploy/backup units disabled. Use root-owned mode 0700
/var/lib/email-connect-backup. Record source and lock hashes and custody delivery
reference. Update both capture/rehearsal image pins under a release review.
Publisher local encrypted inventory grows until separate owner retention acts.
The platform retention preview can propose 14 daily/4 weekly distinct generations
and protects newest plus the exact recovered version/hash, but owns no deletion.
Admit capacity/headroom and compatibility with the bucket's documented 30-day
lifecycle before enabling a schedule. Connect service/freshness failures to the
established evidence owner and prove reception/response. Retain public recovery
receipts and the recovery image/key independently of node/workstation/provider
loss. Production restore and private-copy expiration are separate owner actions.
## Evidence
- Full email suite: 120 passed, zero skips, with the local GreenMail SMTP/IMAP harness;
19 new synthetic backup/recovery cases. No production account, URL or mail send
is used by those tests. Existing 39 transactional regression cases pass.
- Actual age synthetic seal/decrypt and packaged rehearsal pass: all five tables,
schema and rows preserved; accepted verification replay, unknown verification,
invalidated handles, suppression and invitation reservations checked without
sending. This is local synthetic evidence, no provider retrieval claim.
- Node default server dry-run passes: current pinned image/PVC, no-egress helper,
and unchanged receiver specification. No pause, helper creation or upload.
- All four systemd units pass systemd-analyze verify. Dedicated primary SDK source
is already hash-locked and offline-verified by RPF-WP-0053; actual email caller
admission remains missing.
T01/T02 implementation/rehearsal can close on this evidence; T03/T04 remain open
for actual custody, offsite capture/recovery, post-capture reconciliation contract,
monitored operation and founder activation. CSOC-WP-0010 remains active for those
and caller/sender/rate-limit, reply/bounce/inbox and cross-owner journey acceptance.

260
scripts/backup-ledger.py Normal file
View file

@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""Node ledger backup or attended recovery; default performs preview only."""
from __future__ import annotations
import argparse
import fcntl
import json
import os
import re
import shutil
import signal
import sys
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from ledger_archive import (
PREFIX,
Refused,
checkpoint,
command,
digest,
private_directory,
private_file,
recover,
seal,
)
NAME = re.compile(r"\d{8}T\d{6}Z-[a-f0-9]{24}\.tar\.age")
PRIMARY_TOOL = Path("/opt/railiance-platform/scripts/primary_application_archive.py")
PRIMARY_PYTHON = Path("/opt/railiance-platform/backup-venv/bin/python")
RECIPIENT = "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz"
def publish(encrypted, fetched, receipt_path, captured, tool, python):
command(
[
str(python),
str(tool),
"transfer",
"--application",
"email-connect",
"--source",
str(encrypted),
"--output",
str(fetched),
"--receipt",
str(receipt_path),
"--captured-at",
captured["captured_at"],
"--execute",
]
)
private_file(fetched)
receipt = json.loads(receipt_path.read_text())
if (
receipt.get("schema") != "platform.application-primary-archive.v1"
or receipt.get("status") != "offsite_fetched_verified"
or receipt.get("application") != "email-connect"
or receipt.get("captured_at") != captured["captured_at"]
or receipt.get("remote_retrieval_verified") is not True
or receipt.get("immutable_create") is not True
or not receipt.get("version_id")
or receipt["version_id"] == "null"
or receipt.get("ciphertext_sha256") != digest(encrypted)
or receipt.get("ciphertext_sha256") != digest(fetched)
or receipt.get("ciphertext_bytes") != encrypted.stat().st_size
or not receipt.get("destination", "").startswith(PREFIX)
):
raise Refused("primary_receipt_not_verified")
return receipt
def backup(state, primary_tool, primary_python, recipient, resume=False):
state.mkdir(mode=0o700, parents=True, exist_ok=True)
private_directory(state)
fd = os.open(state / "backup.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise Refused("backup_already_running") from None
pending = list(state.glob("pending-*"))
if resume:
if len(pending) != 1:
raise Refused("one_pending_capture_required")
stage = pending[0]
private_directory(stage)
else:
if pending:
raise Refused("pending_backup_requires_review")
stage = Path(tempfile.mkdtemp(prefix="pending-", dir=state))
command(
[
sys.executable,
str(Path(__file__).with_name("capture-ledger.py")),
"--execute",
"--destination",
str(stage / "capture"),
],
timeout=1800,
)
encrypted, captured = seal(stage / "capture", stage, recipient)
fd, name = tempfile.mkstemp(prefix="readback-", dir=stage)
os.close(fd)
fetched = Path(name)
fetched.unlink()
transfer = publish(
encrypted,
fetched,
stage / "primary-transfer.json",
captured,
primary_tool,
primary_python,
)
filename = transfer["object_key"].rsplit("/", 1)[-1]
if not NAME.fullmatch(filename):
raise Refused("invalid_archive_name")
receipt = dict(
transfer,
image=captured["image"],
database_sha256=captured["database_sha256"],
database_bytes=captured["database_bytes"],
ledger_fingerprint=captured["ledger_fingerprint"],
pause_and_capture_seconds=captured["elapsed_seconds"],
local_archive=filename,
application_restore_verified=False,
completed_at=datetime.now(UTC).isoformat(),
)
output = state / filename
if output.exists() or output.is_symlink():
private_file(output)
if digest(output) != receipt["ciphertext_sha256"]:
raise Refused("local_archive_collision")
else:
# Create private file from the start, including manual invocations.
fd = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "wb") as dst, fetched.open("rb") as src:
shutil.copyfileobj(src, dst)
dst.flush()
os.fsync(dst.fileno())
inventory_path = state / "inventory.json"
inventory = json.loads(inventory_path.read_text()) if inventory_path.exists() else []
if not any(item["object_key"] == receipt["object_key"] for item in inventory):
inventory.append(receipt)
checkpoint(inventory_path, inventory)
checkpoint(state / "last-success.json", receipt)
shutil.rmtree(stage)
return receipt
def freshness(state, now=None, hours=26):
now = now or datetime.now(UTC)
private_directory(state)
private_file(state / "last-success.json")
receipt = json.loads((state / "last-success.json").read_text())
stamp = datetime.fromisoformat(receipt["captured_at"].replace("Z", "+00:00"))
age = (now - stamp).total_seconds()
if not 0 <= age <= hours * 3600:
raise Refused("backup_stale")
name = receipt.get("local_archive", "")
if (
receipt.get("status") != "offsite_fetched_verified"
or receipt.get("application") != "email-connect"
or receipt.get("remote_retrieval_verified") is not True
or not receipt.get("version_id")
or receipt["version_id"] == "null"
or not NAME.fullmatch(name)
):
raise Refused("verified_backup_required")
private_file(state / name)
if digest(state / name) != receipt["ciphertext_sha256"]:
raise Refused("local_ciphertext_mismatch")
return {"status": "fresh", "age_hours": round(age / 3600, 2), "remote_retrieval_verified": True}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["backup", "recover", "freshness"])
parser.add_argument("--execute", action="store_true")
parser.add_argument("--resume-pending", action="store_true")
parser.add_argument("--state-dir", type=Path, default=Path("/var/lib/email-connect-backup"))
parser.add_argument("--primary-tool", type=Path, default=PRIMARY_TOOL)
parser.add_argument("--primary-python", type=Path, default=PRIMARY_PYTHON)
parser.add_argument("--age-recipient", default=RECIPIENT)
parser.add_argument("--archive", type=Path)
parser.add_argument("--transfer-receipt", type=Path)
parser.add_argument("--identity", type=Path)
parser.add_argument("--destination", type=Path)
args = parser.parse_args()
if args.action == "freshness":
print(json.dumps(freshness(args.state_dir)))
elif args.action == "recover":
if not args.execute:
print(json.dumps({"status": "reviewed", "decryption_performed": False}))
return 0
if not all([args.archive, args.transfer_receipt, args.identity, args.destination]):
raise Refused("recovery_arguments_required")
print(
json.dumps(
recover(args.archive, args.transfer_receipt, args.identity, args.destination)
)
)
elif args.execute:
if (
not args.state_dir.is_absolute()
or not args.primary_tool.is_file()
or not args.primary_python.is_file()
):
raise Refused("invalid_backup_configuration")
command(["age", "--version"])
command(
[
str(args.primary_python),
str(args.primary_tool),
"check",
"--application",
"email-connect",
]
)
print(
json.dumps(
backup(
args.state_dir,
args.primary_tool,
args.primary_python,
args.age_recipient,
args.resume_pending,
)
)
)
else:
if args.resume_pending:
raise Refused("resume_requires_execute")
command(
[
sys.executable,
str(Path(__file__).with_name("capture-ledger.py")),
"--destination",
"/tmp/email-ledger-capture-review",
]
)
print(
json.dumps({"status": "reviewed", "live_changes": False, "provider_contacted": False})
)
return 0
if __name__ == "__main__":
def terminate(signum, frame):
raise Refused("backup_interrupted")
signal.signal(signal.SIGTERM, terminate)
try:
raise SystemExit(main())
except Exception as error:
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
raise SystemExit(1)

356
scripts/capture-ledger.py Normal file
View file

@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""Node-only ledger capture, preview by default; execution briefly pauses email."""
from __future__ import annotations
import argparse
import copy
import fcntl
import json
import os
import secrets
import signal
import subprocess
import time
from pathlib import Path
from ledger_archive import MAX_BYTES, Refused, digest, fingerprint, private_directory, write_private
NS = "email-connect"
CLUSTER_UID = "a553c742-0115-43d4-99a4-a5ca56fe0786"
IMAGE = (
"forgejo.coulomb.social/coulomb/email-connect@sha256:"
"1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c"
)
LOCK_PATH = Path("/var/lib/email-connect-backup/capture.lock")
SELECTOR = "app.kubernetes.io/name=email-connect"
def k(*args, body=None, timeout=180):
result = subprocess.run(["kubectl", *args], input=body, capture_output=True, timeout=timeout)
if result.returncode:
raise Refused("cluster_command_failed")
return result.stdout
def document(*args, body=None):
return json.loads(k(*args, "-o", "json", body=body, timeout=30))
def helper(name):
return {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": name, "namespace": NS, "labels": {"email-ledger-capture": name}},
"spec": {
"restartPolicy": "Never",
"automountServiceAccountToken": False,
"securityContext": {
"runAsNonRoot": True,
"runAsUser": 10001,
"runAsGroup": 10001,
"fsGroup": 10001,
"fsGroupChangePolicy": "OnRootMismatch",
"seccompProfile": {"type": "RuntimeDefault"},
},
"containers": [
{
"name": "capture",
"image": IMAGE,
"command": ["python", "-c", "import time;time.sleep(1800)"],
"resources": {
"requests": {"cpu": "10m", "memory": "32Mi"},
"limits": {"cpu": "100m", "memory": "256Mi"},
},
"securityContext": {
"allowPrivilegeEscalation": False,
"readOnlyRootFilesystem": True,
"capabilities": {"drop": ["ALL"]},
},
"volumeMounts": [
{"name": "data", "mountPath": "/data", "readOnly": True},
{"name": "tmp", "mountPath": "/tmp"},
],
}
],
"volumes": [
{
"name": "data",
"persistentVolumeClaim": {"claimName": "email-connect-data", "readOnly": True},
},
{"name": "tmp", "emptyDir": {"sizeLimit": "160Mi"}},
],
},
}
def replica_patch(template, expected, desired):
return json.dumps(
[
{"op": "test", "path": "/spec/template", "value": template},
{"op": "test", "path": "/spec/replicas", "value": expected},
{"op": "replace", "path": "/spec/replicas", "value": desired},
]
).encode()
def selected(selector, labels):
if any(labels.get(key) != value for key, value in selector.get("matchLabels", {}).items()):
return False
for expression in selector.get("matchExpressions", []):
key, op, values = expression["key"], expression["operator"], expression.get("values", [])
if op == "In" and labels.get(key) not in values:
return False
if op == "NotIn" and labels.get(key) in values:
return False
if op == "Exists" and key not in labels:
return False
if op == "DoesNotExist" and key in labels:
return False
if op not in {"In", "NotIn", "Exists", "DoesNotExist"}:
raise Refused("unknown_network_selector")
return True
def require_no_helper_egress(policies, labels):
applicable = [
p["spec"]
for p in policies
if "Egress" in p["spec"].get("policyTypes", [])
and selected(p["spec"].get("podSelector", {}), labels)
]
if not applicable or any(spec.get("egress") for spec in applicable):
raise Refused("helper_egress_not_denied")
def preflight():
if document("get", "namespace", "kube-system")["metadata"]["uid"] != CLUSTER_UID:
raise Refused("wrong_cluster")
before = document("get", "deployment", "email-connect", "-n", NS)
spec = before["spec"]
containers = spec["template"]["spec"]["containers"]
if (
spec["replicas"] != 1
or before["status"].get("readyReplicas") != 1
or len(containers) != 1
or containers[0]["image"] != IMAGE
):
raise Refused("unexpected_release")
if document("get", "pvc", "email-connect-data", "-n", NS)["status"]["phase"] != "Bound":
raise Refused("pvc_not_bound")
config = document("get", "configmap", "email-connect-config", "-n", NS)["data"]
if config.get("EMAIL_CONNECT_DATABASE_PATH") != "/data/email-connect.db":
raise Refused("unexpected_database_path")
name = "email-ledger-capture-" + secrets.token_hex(8)
pod = helper(name)
policies = document("get", "networkpolicies", "-n", NS)["items"]
require_no_helper_egress(policies, pod["metadata"]["labels"])
body = json.dumps(pod).encode()
k("create", "--dry-run=server", "-f", "-", body=body)
projected = document(
"patch",
"deployment",
"email-connect",
"-n",
NS,
"--type=json",
"--patch-file=/dev/stdin",
"--dry-run=server",
body=replica_patch(spec["template"], 1, 0),
)
expected = copy.deepcopy(spec)
expected["replicas"] = 0
if projected["spec"] != expected:
raise Refused("unexpected_pause_spec")
return before, body
def capture_database(name, destination):
# No app startup, mounted secrets, SMTP import or provider request in helper.
code = """import os,sqlite3,sys
source=sqlite3.connect('file:/data/email-connect.db?mode=ro',uri=True)
pages=source.execute('PRAGMA page_count').fetchone()[0]
size=source.execute('PRAGMA page_size').fetchone()[0]
assert pages*size<=LIMIT
os.umask(0o077)
output=sqlite3.connect('/tmp/ledger.sqlite3')
source.backup(output,pages=256)
assert output.execute('PRAGMA integrity_check').fetchall()==[('ok',)]
output.close();source.close()
with open('/tmp/ledger.sqlite3','rb') as stream:sys.stdout.buffer.write(stream.read())
""".replace("LIMIT", str(MAX_BYTES))
data = k("exec", "-n", NS, name, "--", "python", "-c", code)
if not data.startswith(b"SQLite format 3\x00") or len(data) > MAX_BYTES:
raise Refused("invalid_ledger_backup")
path = destination / "database.sqlite3"
write_private(path, data)
return {
"database_sha256": digest(path),
"database_bytes": len(data),
"ledger_fingerprint": fingerprint(path),
"sqlite_integrity": "ok",
"writers_quiesced": True,
"source_pvc_read_only": True,
}
def capture(before, body, destination):
if not destination.is_absolute() or destination.exists() or destination.is_symlink():
raise Refused("fresh_absolute_destination_required")
destination.mkdir(mode=0o700)
private_directory(destination)
receipt = {
"status": "incomplete",
"application": "email-connect",
"image": IMAGE,
"captured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
started, paused, created, captured = time.monotonic(), False, False, False
name = json.loads(body)["metadata"]["name"]
cleanup_error = None
try:
paused = True # A failed response can still have changed replica count.
k(
"patch",
"deployment",
"email-connect",
"-n",
NS,
"--type=json",
"--patch-file=/dev/stdin",
body=replica_patch(before["spec"]["template"], 1, 0),
)
k("wait", "--for=delete", "pod", "-n", NS, "-l", SELECTOR, "--timeout=120s")
current = document("get", "deployment", "email-connect", "-n", NS)
expected = copy.deepcopy(before["spec"])
expected["replicas"] = 0
if current["spec"] != expected:
raise Refused("writers_not_quiesced")
# Require no other pod mounting the ledger PVC, including a prior helper.
pods = document("get", "pods", "-n", NS)["items"]
if any(
any(
v.get("persistentVolumeClaim", {}).get("claimName") == "email-connect-data"
for v in pod["spec"].get("volumes", [])
)
for pod in pods
):
raise Refused("other_ledger_pvc_consumer")
created = True
k("create", "-f", "-", body=body)
k("wait", "--for=condition=Ready", "pod/" + name, "-n", NS, "--timeout=120s")
receipt.update(capture_database(name, destination))
captured = True
except BaseException as error:
receipt.update(failure_type=type(error).__name__)
raise
finally:
if created:
try:
k(
"delete",
"pod",
name,
"-n",
NS,
"--ignore-not-found=true",
"--wait=true",
"--timeout=120s",
)
receipt["helper_removed"] = True
except BaseException as error:
receipt["helper_cleanup_failed"] = True
cleanup_error = error
if paused:
try:
current = document("get", "deployment", "email-connect", "-n", NS)
expected = copy.deepcopy(before["spec"])
expected["replicas"] = current["spec"]["replicas"]
if current["spec"] != expected or current["spec"]["replicas"] not in {0, 1}:
raise Refused("foreign_release_requires_receiver_check")
if current["spec"]["replicas"] == 0:
k(
"patch",
"deployment",
"email-connect",
"-n",
NS,
"--type=json",
"--patch-file=/dev/stdin",
body=replica_patch(before["spec"]["template"], 0, 1),
)
k(
"rollout",
"status",
"deployment/email-connect",
"-n",
NS,
"--timeout=180s",
timeout=240,
)
current = document("get", "deployment", "email-connect", "-n", NS)
if current["spec"] != before["spec"] or current["status"].get("readyReplicas") != 1:
raise Refused("receiver_resume_not_verified")
receipt["receiver_resumed"] = True
receipt["unchanged_receiver_spec"] = True
except BaseException as error:
receipt["receiver_state_requires_check"] = True
cleanup_error = cleanup_error or error
receipt["status"] = (
"captured"
if captured and receipt.get("receiver_resumed") and not cleanup_error
else "failed"
)
receipt["elapsed_seconds"] = round(time.monotonic() - started, 2)
write_private(destination / "receipt.json", json.dumps(receipt, indent=2).encode())
if cleanup_error:
raise Refused("capture_cleanup_requires_check") from None
return receipt
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--execute", action="store_true")
parser.add_argument("--destination", type=Path, required=True)
args = parser.parse_args()
os.environ["KUBECONFIG"] = "/etc/rancher/k3s/k3s.yaml"
before, body = preflight()
if not args.execute:
print(
json.dumps(
{
"status": "reviewed",
"live_changes": False,
"server_dry_run": True,
"image_pinned": True,
"pvc_read_only": True,
"receiver_spec_changed": False,
}
)
)
return 0
LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
private_directory(LOCK_PATH.parent)
fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise Refused("capture_already_running") from None
# Recheck after locking; do not use a stale preflight across another run.
before, body = preflight()
print(json.dumps(capture(before, body, args.destination)))
return 0
if __name__ == "__main__":
def terminate(signum, frame):
raise Refused("capture_interrupted")
signal.signal(signal.SIGTERM, terminate)
try:
raise SystemExit(main())
except Exception as error:
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
raise SystemExit(1)

284
scripts/ledger_archive.py Normal file
View file

@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Private transactional-ledger archive primitives; no mail/provider code."""
from __future__ import annotations
import hashlib
import json
import os
import re
import sqlite3
import stat
import subprocess
import tarfile
import tempfile
from contextlib import closing
from pathlib import Path
MAX_BYTES = 128 * 1024**2
TABLES = {"deliveries", "verifications", "suppressions", "send_attempts", "verification_attempts"}
PREFIX = "s3://railiance-platform-pg-backup/platform-pg/application-archives/email-connect/"
class Refused(RuntimeError):
pass
def digest(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def private_directory(path):
info = path.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise Refused("private_owner_directory_required")
def private_file(path):
info = path.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise Refused("private_owner_file_required")
def write_private(path, value):
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(value)
stream.flush()
os.fsync(stream.fileno())
def checkpoint(path, value):
fd, name = tempfile.mkstemp(prefix="receipt-", dir=path.parent)
with os.fdopen(fd, "w") as stream:
json.dump(value, stream, indent=2)
stream.flush()
os.fsync(stream.fileno())
Path(name).replace(path)
def fingerprint(path):
private_file(path)
if not 0 < path.stat().st_size <= MAX_BYTES:
raise Refused("ledger_size_outside_contract")
with closing(sqlite3.connect(path.absolute().as_uri() + "?mode=ro", uri=True)) as database:
database.execute("PRAGMA trusted_schema=OFF")
if database.execute("PRAGMA integrity_check").fetchall() != [("ok",)]:
raise Refused("ledger_integrity_failed")
tables = {
row[0]
for row in database.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
)
}
if tables != TABLES:
raise Refused("unexpected_ledger_schema")
schema = list(
database.execute("SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name")
)
result = {
"schema_sha256": hashlib.sha256(json.dumps(schema).encode()).hexdigest(),
"user_version": database.execute("PRAGMA user_version").fetchone()[0],
"tables": {},
}
for name in sorted(TABLES):
rows = [
json.dumps(
row, separators=(",", ":"), default=lambda value: {"bytes_hex": value.hex()}
).encode()
for row in database.execute('SELECT * FROM "' + name + '"')
]
result["tables"][name] = {
"rows": len(rows),
"sha256": hashlib.sha256(b"\n".join(sorted(rows))).hexdigest(),
}
return result
def sqlite_backup(source, output):
"""Use SQLite backup rather than copying a file that may still have WAL."""
private_directory(output.parent)
if source.is_symlink() or not source.is_file() or output.exists() or output.is_symlink():
raise Refused("invalid_sqlite_backup_paths")
# No source write or checkpoint; a read-only connection includes committed WAL.
with closing(sqlite3.connect(source.absolute().as_uri() + "?mode=ro", uri=True)) as src:
if (
src.execute("PRAGMA page_count").fetchone()[0]
* src.execute("PRAGMA page_size").fetchone()[0]
> MAX_BYTES
):
raise Refused("ledger_size_outside_contract")
write_private(output, b"")
with closing(sqlite3.connect(output)) as dst:
src.backup(dst, pages=256)
return fingerprint(output)
def validate_capture(directory):
private_directory(directory)
private_file(directory / "receipt.json")
receipt = json.loads((directory / "receipt.json").read_text())
if (
receipt.get("status") != "captured"
or receipt.get("application") != "email-connect"
or receipt.get("writers_quiesced") is not True
or receipt.get("receiver_resumed") is not True
or receipt.get("source_pvc_read_only") is not True
):
raise Refused("capture_incomplete")
path = directory / "database.sqlite3"
actual = fingerprint(path)
if (
digest(path) != receipt.get("database_sha256")
or path.stat().st_size != receipt.get("database_bytes")
or actual != receipt.get("ledger_fingerprint")
):
raise Refused("capture_binding_mismatch")
return receipt
def command(argv, timeout=900):
# Keep private child diagnostics out of both memory pipes and the journal.
with tempfile.TemporaryFile() as output:
child = subprocess.Popen(argv, stdout=output, stderr=output)
try:
result = child.wait(timeout=timeout)
except BaseException:
child.terminate()
try:
child.wait(timeout=800)
except subprocess.TimeoutExpired:
child.kill()
child.wait()
raise
if result:
raise Refused("archive_command_failed")
def seal(directory, stage, recipient):
captured = validate_capture(directory)
encrypted, sealed_path = stage / "packet.tar.age", stage / "sealed.json"
binding = {
key: captured[key]
for key in ["database_sha256", "captured_at", "image", "ledger_fingerprint"]
}
if sealed_path.exists():
private_file(sealed_path)
private_file(encrypted)
receipt = json.loads(sealed_path.read_text())
if receipt.get("binding") != binding or receipt.get("ciphertext_sha256") != digest(
encrypted
):
raise Refused("sealed_packet_binding_mismatch")
return encrypted, captured
if encrypted.exists() or encrypted.is_symlink():
raise Refused("partial_encryption_requires_review")
archive = stage / "packet.tar"
with archive.open("xb") as stream:
os.chmod(archive, 0o600)
with tarfile.open(fileobj=stream, mode="w") as packet:
for name in ["database.sqlite3", "receipt.json"]:
packet.add(directory / name, arcname=name, recursive=False)
command(["age", "-r", recipient, "-o", str(encrypted), str(archive)])
encrypted.chmod(0o600)
private_file(encrypted)
if not 0 < encrypted.stat().st_size <= MAX_BYTES + 1024**2:
raise Refused("encrypted_packet_size_outside_contract")
checkpoint(sealed_path, {"binding": binding, "ciphertext_sha256": digest(encrypted)})
return encrypted, captured
def unpack(packet, destination):
if packet.stat().st_size > MAX_BYTES + 1024**2:
raise Refused("packet_too_large")
with tarfile.open(packet, "r:") as archive:
entries = archive.getmembers()
if (
len(entries) != 2
or {entry.name for entry in entries} != {"database.sqlite3", "receipt.json"}
or any(
not entry.isfile() or entry.sparse is not None or entry.size <= 0
for entry in entries
)
or sum(entry.size for entry in entries) > MAX_BYTES + 1024**2
or any(entry.size > 1024**2 for entry in entries if entry.name == "receipt.json")
):
raise Refused("invalid_packet_members")
for entry in entries:
with (
archive.extractfile(entry) as source,
(destination / entry.name).open("xb") as target,
):
os.chmod(target.name, 0o600)
remaining = entry.size
while remaining:
block = source.read(min(remaining, 8 * 1024**2))
if not block:
raise Refused("truncated_packet_member")
target.write(block)
remaining -= len(block)
return validate_capture(destination)
def recover(encrypted, transfer_path, identity, destination):
for path in [encrypted, transfer_path, identity]:
private_file(path)
transfer = json.loads(transfer_path.read_text())
key = transfer.get("object_key", "")
suffix = key.removeprefix("platform-pg/application-archives/email-connect/")
version = transfer.get("version_id")
if (
not isinstance(version, str)
or version in {"", "null"}
or any(char in version for char in "\r\n")
or not key.startswith("platform-pg/application-archives/email-connect/")
or not re.fullmatch(r"\d{4}/\d{2}/\d{2}/\d{8}T\d{6}Z-[a-f0-9]{24}\.tar\.age", suffix)
or transfer.get("destination") != "s3://railiance-platform-pg-backup/" + key
):
raise Refused("verified_transfer_required")
if (
transfer.get("schema") != "platform.application-primary-archive.v1"
or transfer.get("application") != "email-connect"
or transfer.get("status") != "offsite_fetched_verified"
or transfer.get("remote_retrieval_verified") is not True
or not transfer.get("version_id")
or transfer["version_id"] == "null"
or not transfer.get("destination", "").startswith(PREFIX)
or transfer.get("ciphertext_sha256") != digest(encrypted)
or transfer.get("ciphertext_bytes") != encrypted.stat().st_size
):
raise Refused("verified_transfer_required")
if not destination.is_absolute() or destination.exists() or destination.is_symlink():
raise Refused("fresh_absolute_destination_required")
destination.mkdir(mode=0o700)
packet = destination / "packet.tar"
# age must create plaintext with private modes even in an interactive shell.
result = subprocess.run(
["age", "-d", "-i", str(identity), "-o", str(packet), str(encrypted)],
capture_output=True,
timeout=900,
umask=0o077,
)
if result.returncode:
raise Refused("decryption_failed")
private_file(packet)
captured = unpack(packet, destination)
for key in ["captured_at", "image", "database_sha256", "ledger_fingerprint"]:
if transfer.get(key) != captured.get(key):
raise Refused("recovery_capture_binding_mismatch")
packet.unlink()
result = {
"status": "decrypted_ledger_verified",
"application": "email-connect",
"object_key": transfer["object_key"],
"version_id": transfer["version_id"],
"ciphertext_sha256": transfer["ciphertext_sha256"],
"database_sha256": captured["database_sha256"],
"ledger_fingerprint": captured["ledger_fingerprint"],
"mail_sent": False,
"production_restored": False,
"application_restore_verified": False,
}
checkpoint(destination / "unpack-receipt.json", result)
return result

199
scripts/rehearse-ledger.py Normal file
View file

@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Attended recovered-ledger rehearsal in the cached receiver image, no network."""
from __future__ import annotations
import argparse
import json
import secrets
import subprocess
import time
from pathlib import Path
from ledger_archive import (
Refused,
checkpoint,
private_directory,
private_file,
validate_capture,
)
IMAGE = (
"forgejo.coulomb.social/coulomb/email-connect@sha256:"
"1e613d43734c44c5cd32c6083fda8b4da5d01ef43d10c6ca9c29a33c788cf75c"
)
CODE = """import io,json,os,sys
from pathlib import Path
sys.path.insert(0,'/opt/ledger-rehearsal')
from ledger_archive import fingerprint
from email_connect.transactional import SQLiteDeliveryStore,TransactionalApplication,ProviderError
assert os.getuid()==10001
path=Path('/tmp/recovered.sqlite3')
os.umask(0o077)
path.write_bytes(sys.stdin.buffer.read())
path.chmod(0o600)
before=fingerprint(path)
store=SQLiteDeliveryStore(str(path))
assert fingerprint(path)==before
class ForbiddenProvider:
def send(self,*args):raise AssertionError('recovery_provider_call_forbidden')
app=TransactionalApplication(store,ForbiddenProvider(),'synthetic-recovery-token','https://issuer.example.test')
checks={'accepted_verifications':0,'unknown_verifications':0,'invalidated_handles':0,
'suppressions':0,'accepted_deliveries':0,'unknown_sends':0}
query=("SELECT v.registration_id,v.email,v.username,v.client_id,v.tenant,v.display_name,a.state "
"FROM verifications v JOIN verification_attempts a ON a.request_id=v.request_id "
"WHERE a.state IN ('accepted','in_flight')")
for row in store.db.execute(query).fetchall():
payload=dict(zip(['registration_id','normalized_email','preferred_username','client_id','tenant','display_name'],row[:6]))
payload['correlation_id']='synthetic-recovery-probe'
raw=json.dumps(payload).encode();response={}
body=json.loads(b''.join(app({'PATH_INFO':'/v1/registration-verifications','REQUEST_METHOD':'POST',
'CONTENT_LENGTH':str(len(raw)),'wsgi.input':io.BytesIO(raw),
'HTTP_AUTHORIZATION':'Bearer synthetic-recovery-token'},
lambda status,headers:response.update(status=status))))
if row[6]=='accepted':
assert response['status'].startswith('202') and body['accepted']
checks['accepted_verifications']+=1
else:
assert response['status'].startswith('422') and body['error']=='delivery_outcome_unknown'
checks['unknown_verifications']+=1
query=("SELECT handle_hash FROM verifications "
"WHERE consumed_at IS NOT NULL OR canceled_at IS NOT NULL")
for row in store.db.execute(query).fetchall():
try:store.consume_verification(row[0]);raise AssertionError('invalid_handle_reactivated')
except ValueError as error:assert str(error)=='verification_invalid'
checks['invalidated_handles']+=1
for row in store.db.execute('SELECT email FROM suppressions').fetchall():
assert store.is_suppressed(row[0]);checks['suppressions']+=1
query=("SELECT d.event_id,a.fingerprint FROM deliveries d "
"LEFT JOIN send_attempts a ON a.event_id=d.event_id")
for row in store.db.execute(query).fetchall():
assert store.delivery_status(row[0])['state']=='provider_accepted'
assert store.reserve_send(row[0],row[1] or 'synthetic-legacy-probe') is False
checks['accepted_deliveries']+=1
query=("SELECT event_id,fingerprint FROM send_attempts WHERE state='in_flight' "
"AND event_id NOT IN (SELECT event_id FROM deliveries)")
for row in store.db.execute(query).fetchall():
assert store.delivery_status(row[0])['state']=='unknown'
try:store.reserve_send(row[0],row[1]);raise AssertionError('unknown_send_retried')
except ProviderError as error:assert error.code=='delivery_outcome_unknown'
checks['unknown_sends']+=1
store.db.close()
assert fingerprint(path)==before
print(json.dumps({'status':'isolated_ledger_verified','ledger_fingerprint':before,
'checks':checks,'mail_sent':False,'uid':os.getuid()}))
"""
def rehearse(recovered, receipt_path):
private_directory(recovered)
private_directory(receipt_path.parent)
if receipt_path.exists() or receipt_path.is_symlink():
raise Refused("fresh_receipt_required")
capture = validate_capture(recovered)
private_file(recovered / "unpack-receipt.json")
unpacked = json.loads((recovered / "unpack-receipt.json").read_text())
if (
capture["image"] != IMAGE
or unpacked.get("status") != "decrypted_ledger_verified"
or unpacked.get("ledger_fingerprint") != capture["ledger_fingerprint"]
):
raise Refused("pinned_recovered_ledger_required")
# Never pull a recovery image from the production forge during this rehearsal.
inspected = subprocess.run(
["docker", "image", "inspect", IMAGE], capture_output=True, timeout=30
)
if inspected.returncode:
raise Refused("cached_pinned_recovery_image_required")
started = time.monotonic()
module = Path(__file__).with_name("ledger_archive.py").resolve()
name = "email-ledger-rehearsal-" + secrets.token_hex(8)
try:
with (recovered / "database.sqlite3").open("rb") as source:
result = subprocess.run(
[
"docker",
"run",
"--rm",
"--name",
name,
"-i",
"--network",
"none",
"--read-only",
"--user",
"10001:10001",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--memory",
"512m",
"--cpus",
"0.5",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=160m,uid=10001,gid=10001,mode=0700",
"--mount",
"type=bind,source="
+ str(module)
+ ",target=/opt/ledger-rehearsal/ledger_archive.py,readonly",
"-e",
"PYTHONDONTWRITEBYTECODE=1",
"--entrypoint",
"python",
IMAGE,
"-c",
CODE,
],
stdin=source,
capture_output=True,
timeout=120,
)
finally:
subprocess.run(["docker", "rm", "-f", name], capture_output=True, timeout=30)
if result.returncode:
raise Refused("isolated_rehearsal_failed")
verification = json.loads(result.stdout)
if verification.get("ledger_fingerprint") != capture["ledger_fingerprint"]:
raise Refused("isolated_fingerprint_mismatch")
receipt = dict(
unpacked,
status="application_restored_verified",
application_restore_verified=True,
recovery_image=IMAGE,
network="disabled",
production_restored=False,
checks=verification["checks"],
uid=verification["uid"],
mail_sent=False,
evidence_ceiling="isolated_ledger_rehearsal",
elapsed_seconds=round(time.monotonic() - started, 2),
)
checkpoint(receipt_path, receipt)
return receipt
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--recovered", required=True, type=Path)
parser.add_argument("--receipt", required=True, type=Path)
parser.add_argument("--execute", action="store_true")
args = parser.parse_args()
if not args.execute:
print(
json.dumps(
{"status": "reviewed", "container_started": False, "production_restored": False}
)
)
return 0
print(json.dumps(rehearse(args.recovered, args.receipt)))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
raise SystemExit(1)

435
tests/test_ledger_backup.py Normal file
View file

@ -0,0 +1,435 @@
"""Only synthetic ledger records; no production URLs, custody or SMTP sends."""
import copy
import hashlib
import importlib.util
import io
import json
import sqlite3
import subprocess
import sys
import tarfile
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from email_connect.transactional import SQLiteDeliveryStore, TransactionalApplication
@pytest.fixture
def modules(monkeypatch):
scripts = Path(__file__).resolve().parents[1] / "scripts"
monkeypatch.syspath_prepend(str(scripts))
result = []
for name in ["ledger_archive", "capture-ledger", "backup-ledger"]:
spec = importlib.util.spec_from_file_location(
name.replace("-", "_"), scripts / (name + ".py")
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, name.replace("-", "_"), module)
spec.loader.exec_module(module)
result.append(module)
return result
def synthetic_store(path):
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
store = SQLiteDeliveryStore(str(path))
path.chmod(0o600)
accepted = {
"registration_id": "accepted",
"normalized_email": "accepted@example.test",
"preferred_username": "synthetic",
"client_id": "synthetic-client",
"tenant": "synthetic-tenant",
"correlation_id": "trace-1",
}
expires = (datetime.now(UTC) + timedelta(days=1)).isoformat()
def reserve(payload, handle):
return store.reserve_verification(
payload, hashlib.sha256(handle.encode()).hexdigest(), expires
)[0]
request = reserve(accepted, "accepted-handle")
store.verification_accepted(request, "synthetic-provider-reference")
reserve(dict(accepted, registration_id="unknown"), "unknown-handle")
consumed = reserve(dict(accepted, registration_id="consumed"), "consumed-handle")
store.verification_accepted(consumed, "synthetic-consumed-reference")
store.consume_verification(hashlib.sha256(b"consumed-handle").hexdigest())
canceled = reserve(dict(accepted, registration_id="canceled"), "canceled-handle")
store.verification_accepted(canceled, "synthetic-canceled-reference")
store.cancel_verification(hashlib.sha256(b"canceled-handle").hexdigest())
store.reserve_send("accepted-invitation", "synthetic-accepted-fingerprint")
store.record("accepted-invitation", "synthetic-invitation-reference")
store.reserve_send("unknown-invitation", "synthetic-unknown-fingerprint")
store.suppress("suppressed@example.test", "synthetic complaint")
store.db.close()
return accepted
def captured(directory, archive, image="synthetic@sha256:fixture"):
directory.mkdir(mode=0o700)
synthetic_store(directory / "database.sqlite3")
receipt = {
"status": "captured",
"application": "email-connect",
"image": image,
"captured_at": "2026-10-07T20:00:00Z",
"elapsed_seconds": 30,
"writers_quiesced": True,
"receiver_resumed": True,
"source_pvc_read_only": True,
"database_sha256": archive.digest(directory / "database.sqlite3"),
"database_bytes": (directory / "database.sqlite3").stat().st_size,
"ledger_fingerprint": archive.fingerprint(directory / "database.sqlite3"),
}
archive.write_private(directory / "receipt.json", json.dumps(receipt).encode())
return receipt
def invoke(app, payload, path):
result = {}
raw = json.dumps(payload).encode()
environ = {
"PATH_INFO": path,
"REQUEST_METHOD": "POST",
"CONTENT_LENGTH": str(len(raw)),
"wsgi.input": io.BytesIO(raw),
"HTTP_AUTHORIZATION": "Bearer synthetic-token",
}
body = json.loads(b"".join(app(environ, lambda status, headers: result.update(status=status))))
return result["status"], body
def test_sqlite_backup_includes_committed_wal_without_source_checkpoint(modules, tmp_path):
archive, _, _ = modules
source = tmp_path / "source.sqlite3"
synthetic_store(source)
with sqlite3.connect(source) as writer:
writer.execute("PRAGMA journal_mode=WAL")
writer.execute("INSERT INTO deliveries VALUES ('wal-event','wal-reference')")
writer.commit()
original_bytes = source.read_bytes()
before = archive.fingerprint(source)
output = tmp_path / "private" / "database.sqlite3"
output.parent.mkdir(mode=0o700)
assert archive.sqlite_backup(source, output) == before
assert source.read_bytes() == original_bytes
with sqlite3.connect(output) as restored:
assert restored.execute(
"SELECT provider_ref FROM deliveries WHERE event_id='wal-event'"
).fetchone() == ("wal-reference",)
writer.execute("INSERT INTO deliveries VALUES ('uncommitted','private')")
output2 = output.parent / "another.sqlite3"
archive.sqlite_backup(source, output2)
with sqlite3.connect(output2) as restored:
assert (
restored.execute("SELECT 1 FROM deliveries WHERE event_id='uncommitted'").fetchone()
is None
)
writer.rollback()
def test_real_encryption_recovery_preserves_challenge_and_delivery_behavior(modules, tmp_path):
archive, _, _ = modules
stage = tmp_path / "stage"
stage.mkdir(mode=0o700)
receipt = captured(stage / "capture", archive)
identity = tmp_path / "synthetic-identity"
subprocess.run(["age-keygen", "-o", str(identity)], capture_output=True, check=True)
public = (
subprocess.run(["age-keygen", "-y", str(identity)], capture_output=True, check=True)
.stdout.decode()
.strip()
)
encrypted, _ = archive.seal(stage / "capture", stage, public)
first_ciphertext = encrypted.read_bytes()
assert (
archive.seal(stage / "capture", stage, "unused-retry-recipient")[0].read_bytes()
== first_ciphertext
)
transfer = dict(
receipt,
schema="platform.application-primary-archive.v1",
status="offsite_fetched_verified",
remote_retrieval_verified=True,
version_id="synthetic-version",
object_key="platform-pg/application-archives/email-connect/2026/10/07/20261007T200000Z-"
+ "a" * 24
+ ".tar.age",
destination=archive.PREFIX + "2026/10/07/20261007T200000Z-" + "a" * 24 + ".tar.age",
ciphertext_sha256=archive.digest(encrypted),
ciphertext_bytes=encrypted.stat().st_size,
)
transfer_path = tmp_path / "transfer.json"
archive.write_private(transfer_path, json.dumps(transfer).encode())
destination = tmp_path / "recovered"
result = archive.recover(encrypted, transfer_path, identity, destination)
assert result["status"] == "decrypted_ledger_verified" and not result["production_restored"]
assert not (destination / "packet.tar").exists()
assert all(p.stat().st_mode & 0o777 == 0o600 for p in destination.iterdir())
path = destination / "database.sqlite3"
before = archive.fingerprint(path)
store = SQLiteDeliveryStore(str(path))
assert archive.fingerprint(path) == before
class ForbiddenProvider:
def send(self, *args):
raise AssertionError("recovery must not resend")
app = TransactionalApplication(
store, ForbiddenProvider(), "synthetic-token", "https://issuer.example.test"
)
payload = {
"registration_id": "accepted",
"normalized_email": "accepted@example.test",
"preferred_username": "synthetic",
"client_id": "synthetic-client",
"tenant": "synthetic-tenant",
"correlation_id": "another-trace",
}
status, response = invoke(app, payload, "/v1/registration-verifications")
assert status.startswith("202") and response["accepted"]
status, response = invoke(
app, dict(payload, registration_id="unknown"), "/v1/registration-verifications"
)
assert status.startswith("422") and response["error"] == "delivery_outcome_unknown"
for handle in ["consumed-handle", "canceled-handle"]:
_, response = invoke(app, {"handle": handle}, "/v1/registration-verifications/consume")
assert response["error"] == "verification_invalid"
assert store.reference("accepted-invitation") == "synthetic-invitation-reference"
assert store.is_suppressed("suppressed@example.test")
store.db.close()
assert archive.fingerprint(path) == before
@pytest.mark.parametrize("bad", ["duplicate", "traversal", "link", "extra"])
def test_packet_refuses_all_unsafe_members_before_extract(modules, tmp_path, bad):
archive, _, _ = modules
packet = tmp_path / "packet.tar"
with tarfile.open(packet, "w") as output:
for name in ["database.sqlite3", "receipt.json"]:
entry = tarfile.TarInfo(name)
entry.size = 1
if bad == "link" and name == "database.sqlite3":
entry.type, entry.linkname, entry.size = tarfile.SYMTYPE, "/tmp/private", 0
output.addfile(entry, io.BytesIO(b"x"))
if bad != "link":
entry = tarfile.TarInfo(
{"duplicate": "database.sqlite3", "traversal": "../escape", "extra": "smtp.env"}[
bad
]
)
entry.size = 1
output.addfile(entry, io.BytesIO(b"x"))
destination = tmp_path / "out"
destination.mkdir(mode=0o700)
with pytest.raises(archive.Refused, match="invalid_packet_members"):
archive.unpack(packet, destination)
assert not list(destination.iterdir())
def deployment(capture):
return {
"spec": {"replicas": 1, "template": {"spec": {"containers": [{"image": capture.IMAGE}]}}},
"status": {"readyReplicas": 1},
}
@pytest.mark.parametrize(
"failure", [None, "capture", "interrupt", "helper-cleanup", "foreign-image"]
)
def test_capture_resumes_unchanged_receiver_after_failures(modules, monkeypatch, tmp_path, failure):
archive, capture, _ = modules
before = deployment(capture)
current = copy.deepcopy(before)
calls = []
def k(*args, **kwargs):
calls.append(args)
if args[0] == "patch":
patches = json.loads(kwargs["body"])
assert patches[0]["value"] == before["spec"]["template"]
assert patches[1]["value"] == current["spec"]["replicas"]
current["spec"]["replicas"] = patches[2]["value"]
if args[0] == "delete" and failure == "helper-cleanup":
raise capture.Refused("cleanup failed")
return b""
def document(*args, **kwargs):
return {"items": []} if args[1] == "pods" else copy.deepcopy(current)
def capture_database(name, destination):
if failure == "capture":
raise capture.Refused("capture failed")
if failure == "interrupt":
raise KeyboardInterrupt()
if failure == "foreign-image":
current["spec"]["template"]["spec"]["containers"][0]["image"] = "foreign-image"
synthetic_store(destination / "database.sqlite3")
path = destination / "database.sqlite3"
return {
"database_sha256": archive.digest(path),
"database_bytes": path.stat().st_size,
"ledger_fingerprint": archive.fingerprint(path),
"writers_quiesced": True,
"source_pvc_read_only": True,
}
monkeypatch.setattr(capture, "k", k)
monkeypatch.setattr(capture, "document", document)
monkeypatch.setattr(capture, "capture_database", capture_database)
destination = tmp_path / "capture"
body = json.dumps(capture.helper("synthetic-capture")).encode()
if failure:
with pytest.raises((archive.Refused, KeyboardInterrupt)):
capture.capture(before, body, destination)
else:
assert capture.capture(before, body, destination)["status"] == "captured"
receipt = json.loads((destination / "receipt.json").read_text())
assert any(args[0] == "delete" for args in calls)
if failure == "foreign-image":
assert current["spec"]["replicas"] == 0 and receipt["receiver_state_requires_check"]
assert not receipt.get("receiver_resumed")
else:
assert current == before and receipt["receiver_resumed"]
assert receipt["status"] == ("captured" if failure is None else "failed")
@pytest.mark.parametrize("failure", [None, "publish", "encrypt"])
def test_backup_success_requires_readback_and_preserves_one_pending_capture(
modules, monkeypatch, tmp_path, failure
):
archive, _, runner = modules
captures, publications = [], []
original_seal = runner.seal
state = tmp_path / "state"
def command(argv, **kwargs):
captures.append(argv)
captured(Path(argv[-1]), archive)
def seal(directory, stage, recipient):
if failure == "encrypt":
raise archive.Refused("synthetic encryption failure")
return original_seal(directory, stage, recipient)
def publish(encrypted, fetched, receipt_path, capture, tool, python):
publications.append(encrypted.read_bytes())
if failure == "publish":
raise archive.Refused("synthetic readback failure")
archive.write_private(fetched, encrypted.read_bytes())
key = (
"platform-pg/application-archives/email-connect/2026/10/07/20261007T200000Z-"
+ "b" * 24
+ ".tar.age"
)
return {
"schema": "platform.application-primary-archive.v1",
"application": "email-connect",
"status": "offsite_fetched_verified",
"remote_retrieval_verified": True,
"version_id": "synthetic-version",
"object_key": key,
"destination": archive.PREFIX + key.rsplit("/", 1)[-1],
"captured_at": capture["captured_at"],
"ciphertext_sha256": archive.digest(encrypted),
"ciphertext_bytes": encrypted.stat().st_size,
"immutable_create": True,
}
identity = tmp_path / "synthetic-identity"
subprocess.run(["age-keygen", "-o", str(identity)], capture_output=True, check=True)
public = (
subprocess.run(["age-keygen", "-y", str(identity)], capture_output=True, check=True)
.stdout.decode()
.strip()
)
monkeypatch.setattr(runner, "command", command)
monkeypatch.setattr(runner, "seal", seal)
monkeypatch.setattr(runner, "publish", publish)
if failure:
with pytest.raises(archive.Refused):
runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public)
assert not (state / "last-success.json").exists()
assert len(list(state.glob("pending-*"))) == 1
with pytest.raises(archive.Refused, match="pending_backup_requires_review"):
runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public)
if failure == "publish":
ciphertext = publications[0]
failure = None
result = runner.backup(
state, Path("synthetic-tool"), Path("synthetic-python"), public, resume=True
)
assert len(captures) == 1 and publications == [ciphertext, ciphertext]
assert result["application_restore_verified"] is False
else:
result = runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public)
assert not list(state.glob("pending-*"))
assert not list(state.rglob("database.sqlite3"))
assert json.loads((state / "inventory.json").read_text()) == [result]
assert runner.freshness(state, datetime(2026, 10, 8, 20, tzinfo=UTC))["status"] == "fresh"
with pytest.raises(archive.Refused, match="backup_stale"):
runner.freshness(state, datetime(2026, 10, 9, 20, tzinfo=UTC))
(state / result["local_archive"]).write_bytes(b"corruption")
with pytest.raises(archive.Refused, match="local_ciphertext_mismatch"):
runner.freshness(state, datetime(2026, 10, 8, 20, tzinfo=UTC))
@pytest.mark.parametrize(
"field,value",
[
("receiver_resumed", False),
("database_sha256", "bad"),
("ledger_fingerprint", {}),
("source_pvc_read_only", False),
],
)
def test_invalid_capture_cannot_be_sealed(modules, tmp_path, field, value):
archive, _, _ = modules
stage = tmp_path / "stage"
stage.mkdir(mode=0o700)
receipt = captured(stage / "capture", archive)
receipt[field] = value
(stage / "capture" / "receipt.json").write_text(json.dumps(receipt))
with pytest.raises(archive.Refused):
archive.seal(stage / "capture", stage, "unused public recipient")
assert not (stage / "packet.tar").exists()
def test_helper_has_no_secret_or_token_mount_and_requires_no_egress(modules):
_, capture, _ = modules
pod = capture.helper("synthetic-helper")
container = pod["spec"]["containers"][0]
assert not pod["spec"]["automountServiceAccountToken"]
assert "env" not in container and "envFrom" not in container
assert container["volumeMounts"][0]["readOnly"]
labels = pod["metadata"]["labels"]
deny = {"spec": {"policyTypes": ["Ingress", "Egress"], "podSelector": {}}}
scoped = {
"spec": {
"policyTypes": ["Egress"],
"podSelector": {"matchLabels": {"app.kubernetes.io/name": "email-connect"}},
"egress": [{}],
}
}
capture.require_no_helper_egress([deny, scoped], labels)
with pytest.raises(capture.Refused, match="helper_egress_not_denied"):
capture.require_no_helper_egress([], labels)
broad = copy.deepcopy(scoped)
broad["spec"]["podSelector"] = {}
with pytest.raises(capture.Refused, match="helper_egress_not_denied"):
capture.require_no_helper_egress([deny, broad], labels)
assert capture.selected(
{"matchExpressions": [{"key": "email-ledger-capture", "operator": "Exists"}]}, labels
)
assert not capture.selected(
{
"matchExpressions": [
{"key": "email-ledger-capture", "operator": "In", "values": ["other"]}
]
},
labels,
)

View file

@ -0,0 +1,109 @@
---
id: EMAIL-WP-0007
type: workplan
title: "Back up and recover the transactional delivery ledger"
domain: infotech
repo: email-connect
status: active
owner: codex
topic_slug: email-connect
created: "2026-10-07"
updated: "2026-10-08"
origin: residual
origin_ref: EMAIL-WP-0006
---
CSOC-WP-0010-T04/T05 request durable recovery of the SQLite delivery/challenge
ledger independently of Coulomb's metadata/content pair. EMAIL-WP-0006 completed
the verification retry release; its temporary private pre-release copies are not
a recurring backup. This work does not change SMTP/caller custody or send mail.
## T01 — Implement coherent ledger capture and verified publication
```task
id: EMAIL-WP-0007-T01
status: done
priority: high
```
Prepare a default-dry-run node capture, pinned to the current cluster/image/PVC.
Quiesce the single receiver, use SQLite's backup API against the read-only PVC,
verify integrity/all five transactional tables and resume the unchanged receiver
in finally. Lock overlap and preserve private failure evidence. Package SQLite
and its capture receipt only, age-encrypt once, publish through platform's
immutable email-connect archive profile, and require version/hash readback before
advancing success. Reviewed retries reuse ciphertext without another pause.
No SMTP password, ingest token, scanner mailbox or provider message body belongs
in this archive. Tests use only synthetic records and no production endpoints.
## T02 — Prove isolated recovery preserves delivery and challenge state
```task
id: EMAIL-WP-0007-T02
status: done
priority: high
```
Add attended guarded decrypt/unpack and all-table/schema/row fingerprint checks.
Exercise current packaged service under UID10001/network-disabled conditions
with synthetic recovered SQLite. Accepted sends/verifications replay without a
provider call; unknown outcomes remain blocked; suppression and consumed/cancelled
links remain effective. A historical snapshot can lose newer reservations or
revive a handle consumed after capture: production recovery must freeze delivery,
reconcile post-capture operations/issuer state and explicitly invalidate/reconcile
challenges before releasing traffic. Never auto-restore into a running service.
## T03 — Admit the independent primary caller and retention custody
```task
id: EMAIL-WP-0007-T03
status: wait
priority: high
needs_human: true
intervention_note: "Platform must admit the separate email-connect archive prefix, caller delivery, retention/capacity, independently retained receipts and recovery escrow; shared uploader support alone is not custody admission."
```
Platform profile: platform-pg/application-archives/email-connect/ in the current
Scaleway primary bucket. Request a separately scoped caller; do not copy another
workload's Secret or inherit its write/delete authority. Worker has only public
age recipient, no escrow identity or retained-object deletion rights. Bind the
platform hash-locked SDK runtime and separate recovery/retention owner. RPF-WP-0053-T05
tracks admission; founder continuation authorizes source preparation, not an
invented owner review or credential grant.
## T04 — Complete the admitted first run, recovery and monitoring
```task
id: EMAIL-WP-0007-T04
status: wait
priority: high
needs_human: true
intervention_note: "Needs T03 admission and the exact first receiver pause/upload/schedule decision; real offsite retrieval, attended recovery and failure reception remain unproved."
```
Present pinned source hashes, dry-run evidence, coherent ledger scope, bounded
pause and rollback/resume protocol before exact activation. First capture must
resume current service before encryption/upload. Independently retrieve/decrypt
with escrow and validate all rows/schema and no-send recovery behavior. Record
RPO/RTO and retained recovery point, then admit schedule and monitored failure
response. Temporary private release copies retain their existing custody until
the owner explicitly disposes of them; no expiry is implied here.
## Implementation evidence — 2026-10-08
Standalone scripts/capture-ledger.py, ledger_archive.py, backup-ledger.py and
rehearse-ledger.py plus four disabled unit candidates implement T01/T02. Full
email suite 120 passed with the local GreenMail harness, zero skips, including 19
new synthetic ledger cases. Actual age roundtrip and pinned receiver-image
rehearsal under UID10001/network none preserve all five tables/schema/rows and
verify accepted/unknown reservation and consumed/cancelled/suppression behavior
without provider calls. Isolated rehearsal is not live recovery acceptance.
Node server dry-run verifies current cluster/image/PVC, no-egress/no-secret helper
and replica-only guarded pause projection; receiver remains unchanged, no helper
created. All four units verify. Metadata-only check: email primary.env absent;
no credentials fetched/copied, live pause/capture/provider upload, deploy or timer
activation. Complete review: docs/ledger-backup-review-2026-10-08.md. Platform
admission handoff RPF-WP-0053-T05 is a live record, not an owner approval. T03/T04
remain wait for custody, retention/capacity, actual offsite/recovery and monitoring
plus the incident post-capture reconciliation gate. The workplan remains active.