Implements the operator intent: "CI/CD pipeline is the end-all-be-all for approval. If the CI pipeline is not good enough to be trusted for this, then it is not good enough to do its intended job and needs to be improved. Manual approvers should only be for architecturally or security relevant changes." Adds ci/approval-policy.sh as a gate stage. A green gate becomes sufficient for an ordinary change; the one exception is structural rather than a matter of taste: A CHANGE TO THE GATE CANNOT BE GATED BY THE GATE. A PR that weakens a check would be approved by the weakened check. The same holds for the admission policies, the Flux wiring that decides what reconciles, the SOPS recipients and the RBAC. Those paths -- and only those -- still require an APPROVED review from someone other than the author. The list is data with a reason per entry (infra/gitea/approval-policy.json) and is itself a sensitive path, so the policy cannot be widened or narrowed without a human. FAIL-CLOSED, and deliberately the opposite of gitea-settings. There, a missing credential must not block every PR -- that is a lock-out. Here, a missing credential would let an unreviewed change to the gate merge, so "cannot evaluate" is a BLOCK. Verified against real PRs rather than fixtures: PR #16 (sensitive paths, approved by jakman85, author gitea-admin) -> PASS PR #6 (.gitea/workflows/gate.yml, ZERO reviews) -> BLOCK, naming the file push event (not a PR) -> N/A, exit 0 bogus token -> BLOCK, not skip Also caught before shipping: the first draft passed the PR file list with a second stdin redirect after a heredoc, which silently wins and leaves the script body unread. Running it found that; reading it would not have. required_approvals is NOT changed here. Flipping it live must happen together with the declaration in expected-branch-protection.json, or gitea-settings correctly fails on the drift -- that is a separate, deliberate change. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
infra/ — node provisioning and the toolchain
OpenTofu (Proxmox VE) + cloud-init + Ansible bring up the 3-node k3s/Cilium dev cluster.
Spec: ../home-network-k3s/docs/02-environments-and-provisioning.md.
| Path | What |
|---|---|
tofu/ |
The "Create" stage: 3 cloud-init'd VMs on the Proxmox box (bpg/proxmox provider) |
cloud-init/ |
First-boot baseline shipped as a Proxmox user-data snippet |
ansible/ |
The "Baseline"/"Configure" stages: k3s HA, Cilium, kube-vip, the VPN egress gateway |
devcontainer/ |
The pinned toolchain (tofu, ansible, sops, age, flux, kubectl, helm and the whole CI gate toolchain) |
cluster.sh |
RETIRED — drove the old libvirt VMs; does not work against Proxmox |
PHASE1-STATUS.md |
Current state of the cluster and how to reconnect to it |
PHASE1-SCOPE.md |
Historical Phase-1 scoping doc (libvirt era — superseded) |
Where the cluster actually lives
The dev cluster is not on this workstation. It runs on a Proxmox VE host; the nodes sit on the Proxmox LAN and are reached across a pfSense NAT:
| Nodes | k3s-dev-1/2/3 at 192.168.1.50/.51/.52 — 6 vCPU, 24 GiB, Debian 13, k3s v1.36.4 |
| kube-API | kube-vip VIP 192.168.1.49:6443, reached from the workstation via NAT 192.168.2.50:6443 |
| Node SSH | pfSense NAT 192.168.2.102:2250/2251/2252 → node :22 (Ansible only) |
| Ingress VIP | 192.168.1.53 (Cilium L2, shared Gateway); .53–.60 reserved for LB VIPs |
The earlier libvirt/KVM cluster on 10.73.0.0/24 is retired. Any doc or command referencing
virsh, qemu:///system, or 10.73.0.x for the cluster is historical.