Pin apps-pg targets to railiance01 by cluster identity; seed RPF-WP-0020
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Two reachable clusters each carry a CNPG Cluster named apps-pg in a
namespace named databases. KUBECONFIG is an environment variable, so the
Makefile ?= default never applied, and RAILIANCE01_KUBECONFIG pointed at
config-hosteurope - a different cluster. Had the environment pointed at the
other reachable cluster instead of an unauthorized one, make apps-pg-deploy
would have applied RPF-WP-0019 connection limits, role timeouts and backup
config to the wrong cluster and reported success. The Unauthorized error was
the only thing that prevented it.

Filename selection cannot protect against this: both kubeconfigs resolve to
a 127.0.0.1 tunnel port and the environment wins either way. railiance01-guard
pins identity instead, comparing the live kube-system namespace UID against
RAILIANCE01_CLUSTER_UID, and fails closed on mismatch or unreachability. It
gates apps-pg deploy, backup-deploy, overflow-dry-run, status and shell.
Verified refusing on the wrong cluster, refusing when unreachable, and
passing on railiance01. Not global: db-status legitimately targets the other
cluster for gitea-db.

RPF-WP-0019 blocker note corrected - the cluster was never unreachable, our
wiring was wrong.

RPF-WP-0020 seeded for the pre-existing CCR test failure, which is two
unrelated problems: CCR-2026-0010 is an active lane missing its whole
openbao.auth block, and CCR-2026-0011 is an honest in-flight draft the suite
has no way to express.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-08-18 15:18:37 +02:00
parent 1cbde550b5
commit b1f973c2d5
4 changed files with 227 additions and 13 deletions

View file

@ -1,9 +1,25 @@
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config) $(HOME)/.kube/config-hosteurope)
KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config-railiance01) $(wildcard $(HOME)/.kube/config))
KUBECTL_BIN ?= $(firstword $(shell command -v kubectl 2>/dev/null) $(wildcard $(HOME)/.local/bin/kubectl) kubectl)
KUBECTL := $(KUBECTL_BIN) --kubeconfig=$(KUBECONFIG)
# Cluster identity pin (RPF-WP-0019).
#
# Two reachable clusters each carry a CNPG Cluster named `apps-pg` in a
# namespace named `databases`. Selecting by kubeconfig FILENAME does not
# protect against applying to the wrong one: KUBECONFIG is an environment
# variable, `?=` does not override it, and both files resolve to a
# 127.0.0.1 tunnel port. So the guard pins cluster IDENTITY instead —
# the kube-system namespace UID, which is unique per cluster and stable
# for its lifetime.
#
# railiance01 (k3s v1.35.1) carries platform-pg, forgejo-db and both
# apps-pg consumers. The other cluster carries gitea-db and only one
# apps-pg consumer; `db-status` legitimately targets it, which is why the
# guard is applied per-target rather than globally.
RAILIANCE01_CLUSTER_UID ?= a553c742-0115-43d4-99a4-a5ca56fe0786
HELM := helm --kubeconfig=$(KUBECONFIG)
NAMESPACE := platform
@ -82,10 +98,31 @@ consumption-preflight: ## Refuse a new order that exceeds a restricted entity's
##@ Shared apps-pg (S5 application databases)
cluster-id: ## Print the kube-system UID of the cluster KUBECONFIG currently selects
@echo "kubeconfig: $(KUBECONFIG)"
@echo "cluster-uid: $$($(KUBECTL) get ns kube-system -o jsonpath='{.metadata.uid}' 2>/dev/null || echo UNREACHABLE)"
railiance01-guard: ## Fail closed unless KUBECONFIG selects railiance01
@uid=$$($(KUBECTL) get ns kube-system -o jsonpath='{.metadata.uid}' 2>/dev/null); \
if [ -z "$$uid" ]; then \
echo "railiance01-guard: cluster unreachable via $(KUBECONFIG)" >&2; \
echo " hint: unset KUBECONFIG, or set it to ~/.kube/config-railiance01" >&2; \
exit 2; \
fi; \
if [ "$$uid" != "$(RAILIANCE01_CLUSTER_UID)" ]; then \
echo "railiance01-guard: REFUSING - wrong cluster" >&2; \
echo " kubeconfig: $(KUBECONFIG)" >&2; \
echo " expected: $(RAILIANCE01_CLUSTER_UID)" >&2; \
echo " found: $$uid" >&2; \
echo " both clusters carry apps-pg in namespace databases; applying here would hit the wrong one." >&2; \
exit 2; \
fi; \
echo "railiance01-guard: ok ($$uid)"
apps-pg-verify-capacity: ## Verify cell ceiling, role limits and distinct backup prefixes
python3 tools/verify_apps_pg_capacity.py helm/apps-pg-cluster.yaml helm/apps-pg-2-cluster.yaml
apps-pg-deploy: apps-pg-verify-capacity ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
apps-pg-deploy: railiance01-guard apps-pg-verify-capacity ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
@if [ -n "$(CONSUMING_ENTITY)" ]; then \
$(MAKE) consumption-preflight CONSUMING_ENTITY='$(CONSUMING_ENTITY)' \
ESTIMATE_EUR='$(ESTIMATE_EUR)' CONSUMPTION_CLASS=new-order; \
@ -93,11 +130,11 @@ apps-pg-deploy: apps-pg-verify-capacity ## Apply shared apps-pg cnpg Cluster + N
$(KUBECTL) apply -f helm/apps-pg-cluster.yaml
$(KUBECTL) apply -f helm/apps-pg-networkpolicies.yaml
apps-pg-backup-deploy: ## Apply the daily backup after the governed S3 Secret exists
apps-pg-backup-deploy: railiance01-guard ## Apply the daily backup after the governed S3 Secret exists
@$(KUBECTL) get secret platform-pg-backup-s3 -n databases >/dev/null || { echo 'missing governed platform-pg-backup-s3 secret' >&2; exit 2; }
$(KUBECTL) apply -f helm/apps-pg-backup.yaml
apps-pg-overflow-dry-run: apps-pg-verify-capacity ## Server-validate the unapplied overflow cell
apps-pg-overflow-dry-run: railiance01-guard apps-pg-verify-capacity ## Server-validate the unapplied overflow cell
@$(KUBECTL) get secret apps-pg-2-credentials -n databases >/dev/null || { echo 'missing distinct apps-pg-2-credentials secret' >&2; exit 2; }
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-cluster.yaml
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-networkpolicies.yaml
@ -108,11 +145,11 @@ apps-pg-overflow-deploy: apps-pg-overflow-dry-run ## Provision only for an appro
$(KUBECTL) apply -f helm/apps-pg-2-networkpolicies.yaml
$(KUBECTL) apply -f helm/apps-pg-2-backup.yaml
apps-pg-status: ## Show apps-pg cnpg cluster health
apps-pg-status: railiance01-guard ## Show apps-pg cnpg cluster health
$(KUBECTL) cnpg status apps-pg -n databases 2>/dev/null || \
$(KUBECTL) get cluster apps-pg -n databases -o wide
apps-pg-shell: ## Open psql shell on apps-pg primary as apps_admin / apps_meta
apps-pg-shell: railiance01-guard ## Open psql shell on apps-pg primary as apps_admin / apps_meta
$(KUBECTL) cnpg psql apps-pg -n databases -- -U apps_admin apps_meta 2>/dev/null || \
$(KUBECTL) exec -it -n databases apps-pg-1 -- psql -U apps_admin apps_meta
@ -383,7 +420,7 @@ argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos
##@ Backup
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
@test -x tools/cmd/railiance-backup || { echo "tools/cmd/railiance-backup not installed; use forgejo-backup for Forgejo." >&2; exit 1; }