diff --git a/Makefile b/Makefile index 21d2b9f..28732f2 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,14 @@ REUSE_VALUES ?= helm/reuse-surface-values.yaml REUSE_URL ?= https://reuse.coulomb.social REUSE_CERTIFICATE ?= reuse-surface-tls +POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus +POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json +POLICY_NEXUS_TARGET ?= railiance01 +POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])') +POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")') +POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")') +POLICY_NEXUS_REVISION ?= + COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG) @@ -577,6 +585,46 @@ reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runt reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04) bash tools/reuse-webhook-smoke.sh +##@ policy-nexus (policy.coulomb.social) + +policy-nexus-check-binding: ## Require a source-controlled, immutable production binding + @test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; } + python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)" + +policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo + tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)" + +policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \ + IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \ + IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \ + PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" + +policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01 + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \ + IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \ + IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \ + PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" + +policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \ + IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \ + IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \ + PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" + +policy-nexus-status: ## Show the policy-nexus Helm release state + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)" + +policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \ + IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \ + IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \ + PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" + +policy-nexus-rollback: ## Roll back to a named previous Helm revision + @test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; } + $(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)" + ##@ Help help: ## Show this help @@ -584,4 +632,4 @@ help: ## Show this help /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) -.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke help +.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 610c468..4f7f5d1 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -10,6 +10,7 @@ | --- | --- | --- | --- | --- | | workplan | RAILIANCE-WP-0015 | finished | — | workplans/RAILIANCE-WP-0015-cnpg-backup-scheduledbackup-coverage.md | | workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-railiance01-activity-core-backup-automation.md | +| workplan | RAILIANCE-WP-0018 | active | — | workplans/RAILIANCE-WP-0018-policy-nexus-production-binding.md | | task | RAILIANCE-WP-0015-T01 | done | — | workplans/RAILIANCE-WP-0015-cnpg-backup-scheduledbackup-coverage.md | | task | RAILIANCE-WP-0015-T02 | done | — | workplans/RAILIANCE-WP-0015-cnpg-backup-scheduledbackup-coverage.md | | task | RAILIANCE-WP-0015-T03 | done | — | workplans/RAILIANCE-WP-0015-cnpg-backup-scheduledbackup-coverage.md | @@ -22,3 +23,7 @@ | task | RAILIANCE-WP-0016-T05 | done | — | workplans/RAILIANCE-WP-0016-railiance01-activity-core-backup-automation.md | | task | RAILIANCE-WP-0016-T06 | done | — | workplans/RAILIANCE-WP-0016-railiance01-activity-core-backup-automation.md | | task | RAILIANCE-WP-0016-T07 | done | — | workplans/RAILIANCE-WP-0016-railiance01-activity-core-backup-automation.md | +| task | RAILIANCE-WP-0018-T01 | done | — | workplans/RAILIANCE-WP-0018-policy-nexus-production-binding.md | +| task | RAILIANCE-WP-0018-T02 | done | — | workplans/RAILIANCE-WP-0018-policy-nexus-production-binding.md | +| task | RAILIANCE-WP-0018-T03 | done | — | workplans/RAILIANCE-WP-0018-policy-nexus-production-binding.md | +| task | RAILIANCE-WP-0018-T04 | progress | — | workplans/RAILIANCE-WP-0018-policy-nexus-production-binding.md | diff --git a/bindings/policy-nexus-production.json b/bindings/policy-nexus-production.json new file mode 100644 index 0000000..85ec028 --- /dev/null +++ b/bindings/policy-nexus-production.json @@ -0,0 +1,15 @@ +{ + "schema_version": "policy-nexus-production-binding/v1", + "status": "release-approved", + "approved_on": "2026-08-18", + "rapp_repo": "rapp-policy-nexus", + "reef": "reef-railiance", + "rail": "rail-kubernetes", + "namespace": "policy-nexus", + "release": "policy-nexus", + "hostname": "policy.coulomb.social", + "image_repository": "forgejo.coulomb.social/coulomb/policy-nexus", + "image_digest": "sha256:23318442d1b3a5bdced54f0b407fa5b32834020b80c60d72dea04c03bfbf51e7", + "publication_manifest_digest": "1d8ae5cd683e385d82d29a78729948f7be95d902401a5fbdcfb5e8230cdbfee6", + "residual_risk_owner": "the-custodian" +} diff --git a/docs/policy-nexus-production.md b/docs/policy-nexus-production.md new file mode 100644 index 0000000..ab435a3 --- /dev/null +++ b/docs/policy-nexus-production.md @@ -0,0 +1,56 @@ +# policy-nexus production release + +`railiance-apps` owns only the approved production binding for +`policy.coulomb.social`. Policy sources and the OCI artifact belong to +`policy-nexus`; the Helm chart, hardening, verification, and rollback belong to +`rapp-policy-nexus`. This repository must not copy either the generated site or +the chart. + +## Binding a release + +The source-controlled binding is +`bindings/policy-nexus-production.json`. Before deployment, replace both null +values and set `status` to `release-approved`: + +- `image_digest`: the registry-resolved `sha256:...` digest of the image + published by `policy-nexus`; +- `publication_manifest_digest`: the 64-hex SHA-256 printed by the release + verifier for the exact `publication-manifest.json` in that image. + +Tags are not accepted as production identity. The image digest and publication +digest must be reviewed in the same change; this binds runtime bytes to policy +source provenance without making this repository a second policy authority. + +## Preflight and deployment + +With sibling checkouts of `railiance-apps` and `rapp-policy-nexus`: + +```sh +make policy-nexus-check-binding +make policy-nexus-check-image +make policy-nexus-render +make policy-nexus-server-dry-run +make policy-nexus-deploy +make policy-nexus-status +make policy-nexus-smoke +``` + +Production deployment is an operator action. The current Forgejo build runner +has registry-publication authority but intentionally has no cluster-deploy +authority, kubeconfig, or SSH lane. Do not add those credentials to make this +release automatic. The guarded Make targets refuse the current +`approved-awaiting-artifact` binding and provide the reviewed execution path +from an authorized operator workstation. + +## Rollback + +Find the last known-good Helm revision with `make policy-nexus-status`, then: + +```sh +make policy-nexus-rollback POLICY_NEXUS_REVISION= +make policy-nexus-smoke +``` + +After an emergency rollback, restore the binding file to the two immutable +identities actually serving and capture the smoke output. The package promises +restart recovery on the single-node reef, not multi-node high availability. diff --git a/tools/check-policy-nexus-binding.py b/tools/check-policy-nexus-binding.py new file mode 100644 index 0000000..98af389 --- /dev/null +++ b/tools/check-policy-nexus-binding.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Validate the source-controlled policy-nexus production release identity.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("binding", type=Path) + args = parser.parse_args() + binding = json.loads(args.binding.read_text(encoding="utf-8")) + + if binding.get("schema_version") != "policy-nexus-production-binding/v1": + parser.error("unsupported policy-nexus binding schema") + if binding.get("status") != "release-approved": + parser.error("binding status must be release-approved") + if not re.fullmatch(r"sha256:[a-f0-9]{64}", binding.get("image_digest") or ""): + parser.error("binding image_digest must be sha256:<64 lowercase hex>") + if not re.fullmatch( + r"[a-f0-9]{64}", binding.get("publication_manifest_digest") or "" + ): + parser.error("binding publication_manifest_digest must be 64 lowercase hex") + if binding.get("hostname") != "policy.coulomb.social": + parser.error("binding hostname must be policy.coulomb.social") + + print("policy-nexus production binding is complete") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())