Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ Production images are published separately by the manual
validated `platform_hosted_managed_image_lock`; workflow commit tags are build
discovery aids and are not deployment inputs. The same lock also pins the
third-party PostgreSQL runtime image used by the production Compose profile.
Render the non-secret production environment from that lock with
`scripts/render_hosted_managed_production_env.py`; this avoids manually
transcribing image digests and keeps the initial allowlist fixed to the
read-only canary operation.

## Local Compose Entry Point

Expand Down
23 changes: 22 additions & 1 deletion docs/hosted-managed-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ Platform service, which continues to require its bearer token.
All runtime images must be immutable digest refs:

```bash
export PLATFORM_MANAGED_OPERATION_IMAGE='ghcr.io/0al-spec/platform@sha256:<digest>'
export PLATFORM_MANAGED_OPERATION_IMAGE='ghcr.io/0al-spec/platform-hosted-managed@sha256:<digest>'
export PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE='postgres@sha256:<digest>'
export PLATFORM_MANAGED_OPERATION_INGRESS_IMAGE='ghcr.io/0al-spec/platform-hosted-managed-ingress@sha256:<digest>'
export PLATFORM_MANAGED_OPERATION_ALLOWLIST=review_status_execute
Expand All @@ -329,6 +329,27 @@ Set `PLATFORM_MANAGED_OPERATION_IMAGE`,
values, not from mutable tags. The lock is public-safe evidence;
it does not deploy, change the allowlist, or grant managed-operation authority.

Prefer rendering the non-secret deployment environment directly from that
validated lock instead of transcribing image refs by hand:

```bash
sudo .venv/bin/python scripts/render_hosted_managed_production_env.py \
--image-lock hosted-managed-image-lock.json \
--output /etc/0al/hosted-managed-production.env \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load the rendered env file before deployment

When operators follow this new renderer path, the file is written to /etc/0al/hosted-managed-production.env, but the later Start and probe commands still invoke docker compose and preflight from the process environment, and I found no --env-file or source step for this output. Docker Compose documents non-default environment files as being selected with --env-file, so following the runbook as written leaves the required PLATFORM_MANAGED_OPERATION_* variables unset unless the operator manually infers an extra load step.

Useful? React with 👍 / 👎.

--artifact-root /srv/0al/specgraph \
--state-dir /srv/0al/specspace-state \
--backup-root /srv/0al/backups \
--secret-root /srv/0al/secrets \
--ingress-bind-ip 0.0.0.0 \
--ingress-port 443
```

The renderer validates the complete image lock, fixes the initial allowlist to
`review_status_execute`, rejects overlapping runtime/secret roots, writes the
environment atomically with mode `0440`, and never reads secret values. Run it
as root so the resulting file has the ownership expected by production
operations. Existing output is preserved unless `--overwrite` is explicit.

`deploy/hosted-managed/production.env.example` contains the complete non-secret
environment inventory. It may be copied to a root-readable deployment env file,
but the referenced secret values remain separate files and must never be added
Expand Down
201 changes: 201 additions & 0 deletions scripts/render_hosted_managed_production_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""Render a non-secret hosted managed production environment from an image lock."""

from __future__ import annotations

import argparse
from datetime import datetime, timezone
import hashlib
import ipaddress
import json
import os
from pathlib import Path
import re
import tempfile
from typing import Any

try:
from scripts.validate_hosted_managed_image_lock import validate_image_lock
except ModuleNotFoundError: # Direct execution adds scripts/ rather than repo root.
from validate_hosted_managed_image_lock import validate_image_lock


SAFE_ABSOLUTE_PATH = re.compile(r"^/[A-Za-z0-9._/-]+$")
READ_ONLY_CANARY_OPERATION = "review_status_execute"
SECRET_FILENAMES = {
"PLATFORM_MANAGED_OPERATION_TOKEN_FILE": "service-token",
"PLATFORM_MANAGED_OPERATION_DB_PASSWORD_FILE": "database-password",
"PLATFORM_MANAGED_OPERATION_DATABASE_URL_FILE": "database-url",
"PLATFORM_MANAGED_OPERATION_GITHUB_TOKEN_FILE": "github-token",
"PLATFORM_MANAGED_OPERATION_TLS_CERTIFICATE_FILE": "tls-certificate.pem",
"PLATFORM_MANAGED_OPERATION_TLS_PRIVATE_KEY_FILE": "tls-private-key.pem",
}


class ProductionEnvRenderError(RuntimeError):
"""The requested production environment is unsafe or incomplete."""


def _load_image_lock(path: Path) -> dict[str, Any]:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ProductionEnvRenderError("image lock is unavailable or invalid") from exc
if not isinstance(payload, dict):
raise ProductionEnvRenderError("image lock must be an object")
diagnostics = validate_image_lock(payload)
if diagnostics:
raise ProductionEnvRenderError(
"image lock failed validation: " + ", ".join(diagnostics)
)
return payload


def _safe_root(value: str, *, label: str) -> Path:
if not SAFE_ABSOLUTE_PATH.fullmatch(value) or "//" in value:
raise ProductionEnvRenderError(f"{label} must be a normalized absolute path")
path = Path(value)
if path == Path("/") or any(part in {".", ".."} for part in path.parts):
raise ProductionEnvRenderError(f"{label} must be a bounded absolute path")
return path


def _roots_overlap(left: Path, right: Path) -> bool:
return left == right or left in right.parents or right in left.parents


def render_environment(
*,
image_lock: dict[str, Any],
artifact_root: str,
state_dir: str,
backup_root: str,
secret_root: str,
ingress_bind_ip: str,
ingress_port: int,
) -> tuple[str, dict[str, Any]]:
roots = {
"artifact_root": _safe_root(artifact_root, label="artifact root"),
"state_dir": _safe_root(state_dir, label="state directory"),
"backup_root": _safe_root(backup_root, label="backup root"),
"secret_root": _safe_root(secret_root, label="secret root"),
}
labels = list(roots)
for index, label in enumerate(labels):
for other_label in labels[index + 1 :]:
if _roots_overlap(roots[label], roots[other_label]):
raise ProductionEnvRenderError(
f"{label} and {other_label} must not overlap"
)
try:
parsed_ip = ipaddress.ip_address(ingress_bind_ip)
except ValueError as exc:
raise ProductionEnvRenderError("ingress bind IP is invalid") from exc
if parsed_ip.version != 4:
raise ProductionEnvRenderError("ingress bind IP must be IPv4")
if not 1 <= ingress_port <= 65535:
raise ProductionEnvRenderError("ingress port is outside the valid range")

images = image_lock["images"]
values = {
"PLATFORM_MANAGED_OPERATION_IMAGE": images["platform"]["image_ref"],
"PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE": images["postgresql"][
"image_ref"
],
"PLATFORM_MANAGED_OPERATION_INGRESS_IMAGE": images["ingress"]["image_ref"],
"PLATFORM_MANAGED_OPERATION_ALLOWLIST": READ_ONLY_CANARY_OPERATION,
"PLATFORM_MANAGED_OPERATION_ARTIFACT_ROOT": str(roots["artifact_root"]),
"PLATFORM_MANAGED_OPERATION_STATE_DIR": str(roots["state_dir"]),
"PLATFORM_MANAGED_OPERATION_BACKUP_ROOT": str(roots["backup_root"]),
"PLATFORM_MANAGED_OPERATION_INGRESS_BIND_IP": str(parsed_ip),
"PLATFORM_MANAGED_OPERATION_INGRESS_PORT": str(ingress_port),
}
values.update(
{
key: str(roots["secret_root"] / filename)
for key, filename in SECRET_FILENAMES.items()
}
)
rendered = "\n".join(f"{key}={value}" for key, value in values.items()) + "\n"
report = {
"artifact_kind": "platform_hosted_managed_production_env_render_report",
"contract_ref": "platform.hosted-managed.production-env.v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"ok": True,
"summary": {
"status": "production_env_rendered",
"source_commit": image_lock["source_commit"],
"image_count": 3,
"enabled_operation_ids": [READ_ONLY_CANARY_OPERATION],
"secret_ref_count": len(SECRET_FILENAMES),
"environment_sha256": hashlib.sha256(rendered.encode("utf-8")).hexdigest(),
},
"privacy_boundary": {
"public_safe": True,
"includes_secret_values": False,
"includes_secret_paths": False,
"includes_local_paths": False,
},
"authority_boundary": {
"may_deploy_production": False,
"may_create_secrets": False,
"may_execute_platform": False,
"may_enqueue_operations": False,
},
}
return rendered, report


def _write_atomic(path: Path, content: str, *, overwrite: bool) -> None:
if path.exists() and not overwrite:
raise ProductionEnvRenderError("output already exists; use --overwrite")
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
temporary.chmod(0o440)
temporary.replace(path)
finally:
temporary.unlink(missing_ok=True)


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--image-lock", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--artifact-root", default="/srv/0al/specgraph")
parser.add_argument("--state-dir", default="/srv/0al/specspace-state")
parser.add_argument("--backup-root", default="/srv/0al/backups")
parser.add_argument("--secret-root", default="/srv/0al/secrets")
parser.add_argument("--ingress-bind-ip", default="0.0.0.0")
parser.add_argument("--ingress-port", type=int, default=443)
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args(argv)
try:
image_lock = _load_image_lock(Path(args.image_lock))
rendered, report = render_environment(
image_lock=image_lock,
artifact_root=args.artifact_root,
state_dir=args.state_dir,
backup_root=args.backup_root,
secret_root=args.secret_root,
ingress_bind_ip=args.ingress_bind_ip,
ingress_port=args.ingress_port,
)
_write_atomic(Path(args.output), rendered, overwrite=args.overwrite)
except ProductionEnvRenderError as exc:
report = {
"artifact_kind": "platform_hosted_managed_production_env_render_report",
"ok": False,
"summary": {"status": "blocked"},
"diagnostics": [str(exc)],
}
print(json.dumps(report, indent=2, sort_keys=True))
return 0 if report.get("ok") is True else 1


if __name__ == "__main__":
raise SystemExit(main())
117 changes: 117 additions & 0 deletions tests/test_hosted_managed_production_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations

from contextlib import redirect_stdout
import io
import json
from pathlib import Path
import tempfile
import unittest

from scripts import render_hosted_managed_production_env as renderer


def image_lock() -> dict:
digest = "a" * 64
return {
"artifact_kind": "platform_hosted_managed_image_lock",
"schema_version": 1,
"generated_at": "2026-07-13T00:00:00+00:00",
"source_commit": "b" * 40,
"platforms": ["linux/amd64", "linux/arm64"],
"images": {
"platform": {
"image_ref": f"ghcr.io/0al-spec/platform-hosted-managed@sha256:{digest}"
},
"postgresql": {"image_ref": f"postgres@sha256:{digest}"},
"ingress": {
"image_ref": f"ghcr.io/0al-spec/platform-hosted-managed-ingress@sha256:{digest}",
"base_image_ref": f"caddy@sha256:{digest}",
"upstream_file_capability_removed": True,
},
},
"supply_chain": {"provenance_attestation": True, "sbom_attestation": True},
"privacy_boundary": {"public_safe": True, "includes_secrets": False},
"authority_boundary": {"may_deploy_production": False},
}


class HostedManagedProductionEnvTests(unittest.TestCase):
def test_runbook_uses_renderer_and_current_platform_image_repository(self) -> None:
runbook = (
Path(__file__).resolve().parents[1]
/ "docs"
/ "hosted-managed-operations.md"
).read_text(encoding="utf-8")
self.assertIn("render_hosted_managed_production_env.py", runbook)
self.assertIn("ghcr.io/0al-spec/platform-hosted-managed@sha256:", runbook)
self.assertNotIn("ghcr.io/0al-spec/platform@sha256:<digest>", runbook)

def test_renderer_uses_validated_lock_and_read_only_canary_scope(self) -> None:
rendered, report = renderer.render_environment(
image_lock=image_lock(),
artifact_root="/srv/0al/specgraph",
state_dir="/srv/0al/specspace-state",
backup_root="/srv/0al/backups",
secret_root="/srv/0al/secrets",
ingress_bind_ip="203.0.113.10",
ingress_port=443,
)
self.assertIn(
"PLATFORM_MANAGED_OPERATION_ALLOWLIST=review_status_execute\n", rendered
)
self.assertIn("PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE=postgres@sha256:", rendered)
self.assertIn(
"PLATFORM_MANAGED_OPERATION_TOKEN_FILE=/srv/0al/secrets/service-token",
rendered,
)
self.assertNotIn("token-value", rendered)
self.assertEqual(report["summary"]["enabled_operation_ids"], ["review_status_execute"])
self.assertFalse(report["authority_boundary"]["may_deploy_production"])

def test_renderer_rejects_overlapping_roots_and_invalid_bind_address(self) -> None:
arguments = {
"image_lock": image_lock(),
"artifact_root": "/srv/0al/specgraph",
"state_dir": "/srv/0al/specgraph/state",
"backup_root": "/srv/0al/backups",
"secret_root": "/srv/0al/secrets",
"ingress_bind_ip": "managed.example.org",
"ingress_port": 443,
}
with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "must not overlap"):
renderer.render_environment(**arguments)
arguments["state_dir"] = "/srv/0al/specspace-state"
with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "bind IP is invalid"):
renderer.render_environment(**arguments)

def test_invalid_lock_does_not_replace_existing_environment(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
lock_path = root / "image-lock.json"
lock_path.write_text(json.dumps({"artifact_kind": "invalid"}), encoding="utf-8")
output = root / "production.env"
output.write_text("existing=true\n", encoding="utf-8")
with redirect_stdout(io.StringIO()):
result = renderer.main(
[
"--image-lock",
str(lock_path),
"--output",
str(output),
"--overwrite",
]
)
self.assertEqual(result, 1)
self.assertEqual(output.read_text(encoding="utf-8"), "existing=true\n")

def test_atomic_output_is_group_read_only(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output = Path(temp_dir) / "production.env"
renderer._write_atomic(output, "KEY=value\n", overwrite=False)
self.assertEqual(output.stat().st_mode & 0o777, 0o440)
with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "already exists"):
renderer._write_atomic(output, "OTHER=value\n", overwrite=False)


if __name__ == "__main__":
unittest.main()