2025-09-13 23:34:27 +02:00
# -------- RailianceHosts Make Utilities --------
2025-09-13 20:26:11 +02:00
SHELL := /usr/bin/env bash
2025-09-13 23:34:27 +02:00
.DEFAULT_GOAL := help
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
# Set this to your Gitea host if you want 'remote-set' helper
GITEA ?= gitea.example.com
OWNER ?= coulomb
2026-03-10 00:34:18 +01:00
REPO ?= railiance-infra
2026-08-23 13:13:13 +02:00
SOURCE_REVISION ?= $( shell git rev-parse HEAD 2>/dev/null)
2025-09-13 23:34:27 +02:00
2025-09-13 23:46:48 +02:00
# New-host defaults (can be overridden: make new-host NAME=... TYPE=...)
2025-09-14 01:20:54 +00:00
TYPE ?= cpx11
2025-09-13 23:46:48 +02:00
REGION ?= nbg1
2025-09-14 01:20:54 +00:00
ROLE ?= core
2025-09-13 23:46:48 +02:00
IMG ?= ubuntu-24.04
USER ?= admin
2025-09-13 23:34:27 +02:00
# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally)
2025-09-14 01:20:54 +00:00
HCLOUD_TOKEN := $( shell sops -d --extract '["hetzner"]["token"]' secrets/hetzner-token.yaml 2>/dev/null)
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
# ---- Help ----
help : ## Show this help
2025-09-13 23:46:48 +02:00
@echo "RailianceHosts Commands" ; \
grep -E '^[a-zA-Z0-9_-]+:.*?## ' $( MAKEFILE_LIST) | sort | sed 's/:.*##/: /'
2025-09-13 23:34:27 +02:00
# ---- Git hooks ----
2025-09-13 21:58:19 +00:00
hooks : ## Configure git to use repo-local hooks (.githooks) and ensure executables
@mkdir -p .githooks
2025-09-13 23:34:27 +02:00
git config core.hooksPath .githooks
2025-09-13 21:58:19 +00:00
@test -f .githooks/pre-commit || ( echo "❌ Missing .githooks/pre-commit" ; exit 1)
chmod +x .githooks/pre-commit
@echo "✔ hooks enabled and pre-commit is executable"
2025-09-13 23:34:27 +02:00
hooks-test : ## Test secrets hook blocks plaintext in secrets/
@mkdir -p secrets && echo 'PLAINTEXT_TEST=true' > secrets/_hook_test.yaml
@git add secrets/_hook_test.yaml || true
2025-09-13 23:46:48 +02:00
@if git commit -m "TEST: should be blocked" 2>/dev/null; then \
echo "❌ Hook did NOT block plaintext (check .githooks/pre-commit)" ; \
git reset --soft HEAD~1; \
else \
echo "✔ Hook blocked plaintext as expected" ; \
fi
2025-09-13 23:34:27 +02:00
@git restore --staged secrets/_hook_test.yaml || true
@rm -f secrets/_hook_test.yaml
# ---- SOPS / Age helpers ----
sops-setup : ## Copy age key to SOPS default path (~/.config/sops/age/keys.txt)
mkdir -p ~/.config/sops/age
cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true
chmod 600 ~/.config/sops/age/keys.txt
@echo "✔ SOPS key path set (~/.config/sops/age/keys.txt). Alternatively export SOPS_AGE_KEY."
sops-edit : ## Edit the global secrets with SOPS
2025-09-14 00:11:27 +00:00
sops secrets/hetzner-token.yaml
2025-09-13 23:34:27 +02:00
sops-encrypt : ## Encrypt a file in place: make sops-encrypt FILE=secrets/foo.yaml
@[ -n " $( FILE) " ] || ( echo "Usage: make sops-encrypt FILE=secrets/xxx.yaml" && exit 1)
sops --encrypt --in-place $( FILE)
@echo " ✔ Encrypted $( FILE) "
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
sops-decrypt : ## Print decrypted file to stdout (for inspection) FILE=secrets/foo.sops.yaml
@[ -n " $( FILE) " ] || ( echo "Usage: make sops-decrypt FILE=secrets/xxx.sops.yaml" && exit 1)
sops -d $( FILE)
2025-09-13 20:26:11 +02:00
2026-08-23 12:02:23 +02:00
sops-rotate : ## Check SOPS recipient drift; use the bounded tool for approved changes
python3 scripts/sops_rotation.py --check
2025-09-13 20:26:11 +02:00
2026-08-23 12:02:23 +02:00
check-secrets : ## Fail if any declared secret-bearing path is not encrypted
python3 scripts/check_secret_paths.py --tracked
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
# ---- Terraform (Hetzner) ----
2026-08-23 12:02:23 +02:00
validate-inventory : ## Validate adopted/provider-managed host declarations without provider access
python3 scripts/inventory_contract.py inventory/servers.yaml
validate-baseline : ## Validate the executable baseline and its Ansible/Goss consumers
python3 scripts/baseline_contract.py --check-repo
2026-08-23 12:41:23 +02:00
validate-handoff-readonly : ## Prove the live S1 handoff playbook has no remote mutation surface
python3 scripts/handoff_contract.py
2026-08-23 12:02:23 +02:00
validate-receipts : ## Validate committed metadata-only S1 receipt examples
python3 scripts/s1_receipt.py docs/evidence/s1-receipts/*.json
2026-08-23 12:41:23 +02:00
s1-handoff : ## Run the read-only live S1 verification gate and emit a receipt
2026-08-23 12:02:23 +02:00
python3 scripts/s1_handoff.py
s1-handoff-dry-run : ## Validate handoff inputs without host access; receipt is not-run
python3 scripts/s1_handoff.py --dry-run
2025-09-13 23:34:27 +02:00
tf-fmt : ## Terraform fmt
2026-08-23 12:02:23 +02:00
terraform -chdir= terraform/hetzner fmt -recursive
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
tf-init : ## Terraform init
2026-08-23 12:02:23 +02:00
terraform -chdir= terraform/hetzner init
2025-09-13 23:34:27 +02:00
tf-plan : tf -init ## Terraform plan (requires decrypted HCLOUD_TOKEN)
2025-09-14 01:20:54 +00:00
@echo "🔍 Running terraform plan..."
2026-08-23 12:02:23 +02:00
@[ -n " $( HCLOUD_TOKEN) " ] || ( echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
2025-09-14 01:20:54 +00:00
@export HCLOUD_TOKEN = $( HCLOUD_TOKEN) ; terraform -chdir= terraform/hetzner plan -var= " hcloud_token= $( HCLOUD_TOKEN) "
2025-09-13 23:34:27 +02:00
2026-08-23 12:02:23 +02:00
tf-apply : ## Terraform apply (provision; exact approval required before init)
@test " $( APPROVE_TF_APPLY) " = "YES" || ( echo "Refusing apply: review make tf-plan, then set APPROVE_TF_APPLY=YES" && exit 1)
@[ -n " $( HCLOUD_TOKEN) " ] || ( echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
@$( MAKE) tf-init
2025-09-14 01:20:54 +00:00
@export HCLOUD_TOKEN = $( HCLOUD_TOKEN) ; terraform -chdir= terraform/hetzner apply -auto-approve -var= " hcloud_token= $( HCLOUD_TOKEN) "
2025-09-13 23:34:27 +02:00
2026-08-23 12:02:23 +02:00
tf-destroy : ## Terraform destroy (exact approval required before init)
@test " $( APPROVE_TF_DESTROY) " = "DESTROY-MANAGED-HETZNER" || ( echo "Refusing destroy: set APPROVE_TF_DESTROY=DESTROY-MANAGED-HETZNER after exact plan review" && exit 1)
@[ -n " $( HCLOUD_TOKEN) " ] || ( echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
@$( MAKE) tf-init
2025-09-14 01:20:54 +00:00
@export HCLOUD_TOKEN = $( HCLOUD_TOKEN) ; terraform -chdir= terraform/hetzner destroy -auto-approve -var= " hcloud_token= $( HCLOUD_TOKEN) "
2025-09-13 23:34:27 +02:00
2025-09-14 01:31:03 +00:00
# --- Terraform provider/lockfile helpers ---
TF_DIR := terraform/hetzner
TF_TOKEN := $( HCLOUD_TOKEN)
LOCKFILE := $( TF_DIR) /.terraform.lock.hcl
tf-lock-commit : ## Commit the current provider lockfile
@test -f $( LOCKFILE) || ( echo " ❌ $( LOCKFILE) not found. Run 'make tf-init' first. " ; exit 1)
@git add $( LOCKFILE)
@git commit -m "chore(terraform): lock providers" || echo "ℹ No lockfile changes to commit."
tf-providers-check : ## Check if newer provider versions are available (non-destructive)
@echo "🔎 Checking for provider upgrades (lockfile readonly)…"
@if terraform -chdir= $( TF_DIR) init -upgrade -lockfile= readonly >/dev/null 2>& 1; then \
echo "✔ Providers up to date (no upgrades available)." ; \
else \
echo "↗ Provider upgrades likely available (readonly lockfile blocked changes)." ; \
echo " Run: make tf-providers-upgrade" ; \
fi
tf-providers-upgrade : ## Upgrade providers (updates .terraform.lock.hcl)
@echo "⬆️ Upgrading providers…"
@terraform -chdir= $( TF_DIR) init -upgrade
@echo " — Diff for $( LOCKFILE) : "
@git --no-pager diff -- $( LOCKFILE) || true
@echo "💡 If changes look good: make tf-lock-commit"
tf-providers-upgrade-commit : tf -providers -upgrade tf -lock -commit ## Upgrade providers and commit the lockfile
tf-providers-plan : ## Plan after an upgrade (uses HCLOUD_TOKEN if set)
@echo "🧪 Planning with upgraded providers…"
@terraform -chdir= $( TF_DIR) plan $( if $( TF_TOKEN) ,-var= " hcloud_token= $( TF_TOKEN) " )
2026-03-10 21:18:05 +01:00
# ---- Backup (Q3 Operability & Resilience — D4) ----
backup : ## Backup S1 OS config to /opt/backup/railiance/infra/ (age-encrypted, root required)
sudo tools/cmd/railiance-backup-s1
2026-08-23 13:13:13 +02:00
validate-s1-backup : ## Validate S1 backup declaration and recovery implementation without host changes
python3 scripts/s1_backup.py check
python3 -m unittest tests.test_s1_backup_recovery -v
s1-backup-status : ## Check newest local S1 backup integrity, freshness, count, and disk budget
python3 scripts/s1_backup.py status
s1-backup-prune-plan : ## Print retained bundles and their exact one-use deletion approval
python3 scripts/s1_backup.py prune-plan
s1-backup-prune : ## Apply reviewed prune plan: APPROVE_S1_BACKUP_PRUNE=PRUNE-S1-BACKUPS-...
@test -n " $( APPROVE_S1_BACKUP_PRUNE) " || ( echo "Run make s1-backup-prune-plan, then pass its exact approval" ; exit 1)
python3 scripts/s1_backup.py prune --approval " $( APPROVE_S1_BACKUP_PRUNE) "
s1-restore-inspect : ## Verify encrypted bundle metadata without a private key: BUNDLE=/absolute/path
@test -n " $( BUNDLE) " || ( echo "Usage: make s1-restore-inspect BUNDLE=/absolute/path/to/s1-backup-*" ; exit 1)
python3 scripts/s1_restore.py " $( BUNDLE) "
s1-restore-isolated : ## Decrypt only into explicit empty staging: BUNDLE=... DEST=/tmp/... IDENTITY=...
@test -n " $( BUNDLE) " && test -n " $( DEST) " && test -n " $( IDENTITY) " || ( echo "Usage: make s1-restore-isolated BUNDLE=... DEST=/tmp/... IDENTITY=/path/to/age-identity" ; exit 1)
python3 scripts/s1_restore.py " $( BUNDLE) " --extract-to " $( DEST) " --identity " $( IDENTITY) "
2026-08-23 14:11:00 +02:00
s1-offsite-review : ## Render the exact railiance-platform upload-contract approval
python3 scripts/s1_offsite.py review
s1-offsite-plan : ## Build a credential-free exact upload plan: BUNDLE=/absolute/path
@test -n " $( BUNDLE) " || ( echo "Usage: make s1-offsite-plan BUNDLE=/absolute/path/to/s1-backup-*" ; exit 1)
python3 scripts/s1_offsite.py plan " $( BUNDLE) "
s1-offsite-upload : ## Contained owner-routed upload: BUNDLE=... APPROVE_S1_OFFSITE_UPLOAD=UPLOAD-S1-OFFSITE-...
@test -n " $( BUNDLE) " && test -n " $( APPROVE_S1_OFFSITE_UPLOAD) " || ( echo "Run make s1-offsite-plan, then pass BUNDLE and its exact approval" ; exit 1)
warden access railiance-backup-offsite-lane --field RAILIANCE_BACKUP_NC_TOKEN --exec -- \
python3 scripts/s1_offsite.py upload " $( BUNDLE) " --approval " $( APPROVE_S1_OFFSITE_UPLOAD) "
2026-08-23 13:13:13 +02:00
s1-backup-deploy : ## Deploy and enable timer: HOST=... APPROVE_S1_BACKUP_DEPLOY=DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER
@test -n " $( HOST) " || ( echo "Usage: make s1-backup-deploy HOST=Railiance01 APPROVE_S1_BACKUP_DEPLOY=DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER" ; exit 1)
@test " $( APPROVE_S1_BACKUP_DEPLOY) " = "DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER" || ( echo "Refusing deployment: exact approval is absent" ; exit 1)
@git diff --quiet && git diff --cached --quiet || ( echo "Refusing deployment: commit the exact source first" ; exit 1)
cd ansible && ansible-playbook playbooks/s1-backup.yaml --limit " $( HOST) " \
-e railiance_backup_deploy_approval = " $( APPROVE_S1_BACKUP_DEPLOY) " \
-e railiance_backup_source_revision = " $( SOURCE_REVISION) "
2025-09-13 23:34:27 +02:00
# ---- Ansible ----
2026-03-27 01:21:57 +01:00
ansible-bootstrap : ## Run base bootstrap play (users, ssh, ufw, sops-agent, custodian-agent)
2026-08-15 15:41:59 +02:00
cd ansible && ansible-playbook playbooks/bootstrap.yaml $( ANSIBLE_USER_FLAG)
2025-09-13 20:26:11 +02:00
2026-03-27 01:21:57 +01:00
provision-custodian-agent : ## Deploy custodian agent SSH key to all managed hosts
@python3 -c "import yaml; d=yaml.safe_load(open('ansible/inventory/group_vars/all.yaml')); k=d.get('custodian_agent_pubkey',''); exit(0 if k else 1)" \
|| ( echo "ERROR: custodian_agent_pubkey is empty. Run: cd ~/the-custodian && make custodian-keygen" ; exit 1)
2026-08-15 15:41:59 +02:00
cd ansible && ansible-playbook playbooks/custodian-agent.yaml $( ANSIBLE_USER_FLAG)
2026-03-27 01:21:57 +01:00
2026-03-27 02:20:33 +01:00
provision-custodian-agent-host : ## Deploy custodian agent key to one host: make provision-custodian-agent-host HOST=Railiance01
@test -n " $( HOST) " || ( echo "Usage: make provision-custodian-agent-host HOST=Railiance01" ; exit 1)
2026-08-15 15:41:59 +02:00
cd ansible && ansible-playbook playbooks/custodian-agent.yaml $( ANSIBLE_USER_FLAG) \
2026-03-27 02:20:33 +01:00
--limit " $( HOST) "
2026-03-27 01:21:57 +01:00
2026-06-18 01:06:43 +02:00
bootstrap-ssh-ca : ## Deploy OpenBao SSH CA trust + auth_principals: make bootstrap-ssh-ca SSH_CA_PUBKEY=/path/to/ca_user.pub
@test -n " $( SSH_CA_PUBKEY) " || ( echo "Usage: make bootstrap-ssh-ca SSH_CA_PUBKEY=/path/to/ca_user.pub [HOST=Railiance01]" ; exit 1)
2026-08-15 15:41:59 +02:00
cd ansible && ansible-playbook playbooks/bootstrap-ssh-ca.yaml $( ANSIBLE_USER_FLAG) \
2026-06-18 01:06:43 +02:00
-e ssh_ca_pubkey_path = " $( SSH_CA_PUBKEY) " \
$( if $( HOST) ,--limit " $( HOST) " ,)
bootstrap-ssh-ca-host : ## Deploy SSH CA trust to one host: make bootstrap-ssh-ca-host HOST=Railiance01 SSH_CA_PUBKEY=...
@test -n " $( HOST) " && test -n " $( SSH_CA_PUBKEY) " || \
( echo "Usage: make bootstrap-ssh-ca-host HOST=Railiance01 SSH_CA_PUBKEY=/path/to/ca_user.pub" ; exit 1)
$( MAKE) bootstrap-ssh-ca SSH_CA_PUBKEY = " $( SSH_CA_PUBKEY) " HOST = " $( HOST) "
2025-09-13 23:34:27 +02:00
# ---- Orchestration ----
apply : tf -fmt tf -apply ansible -bootstrap ## Provision via Terraform then converge via Ansible
2025-09-13 20:26:11 +02:00
2026-03-27 02:20:33 +01:00
deploy-stack : ## Print the full S1→S5 ordered deploy sequence (operator follows each step)
@echo ""
@echo "╔══════════════════════════════════════════════════════════════╗"
@echo "║ Railiance Stack — Full Deploy Sequence ║"
@echo "║ See docs/deploy-stack.md for full runbook ║"
@echo "╚══════════════════════════════════════════════════════════════╝"
@echo ""
@echo "PRE-CONDITIONS"
@echo " [ ] SSH key: ~/.ssh/id_ops"
@echo " [ ] SOPS key: ~/.config/sops/age/keys.txt (or SOPS_AGE_KEY)"
2026-08-22 12:34:25 +02:00
@echo " [ ] ops-bridge: bridge up state-hub-railiance01 k3s-api-railiance01"
2026-03-27 02:20:33 +01:00
@echo ""
@echo "S1 — Infrastructure Substrate (this repo)"
@echo " make tf-plan && make tf-apply # provision server (skip if exists)"
2026-08-22 12:34:25 +02:00
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-infra/ansible && ansible-playbook playbooks/bootstrap.yaml -c local --become -l Railiance01'"
2026-03-27 02:20:33 +01:00
@echo " make verify"
@echo ""
@echo "S2 — Cluster Runtime (railiance-cluster)"
2026-08-22 12:34:25 +02:00
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-cluster && make converge && make smoke'"
2026-03-27 02:20:33 +01:00
@echo ""
@echo "S3 — Platform Services (railiance-platform)"
2026-08-22 12:34:25 +02:00
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-platform && make forgejo-db-status'"
2026-03-27 02:20:33 +01:00
@echo ""
2026-08-22 12:34:25 +02:00
@echo "S4 — Developer Enablement (railiance-enablement)"
@echo " make -C ~/railiance-enablement check test"
2026-03-27 02:20:33 +01:00
@echo ""
2026-08-22 12:34:25 +02:00
@echo "S5 — Workloads and forge consumer checks"
@echo " make -C ~/railiance-forge gitea-status"
@echo " deploy each application through its owning rapp/release runbook"
2026-03-27 02:20:33 +01:00
@echo ""
@echo " Full runbook: docs/deploy-stack.md"
2025-09-13 23:34:27 +02:00
# ---- Utilities ----
doctor : ## Check tools and basic repo setup
2025-09-13 23:46:48 +02:00
@bash -ceu ' \
ok( ) { printf "✔ %s\n" " $$ 1 " ; } ; fail( ) { printf "❌ %s\n" " $$ 1 " ; exit 1; } ; \
command -v git >/dev/null && ok " git: $$ (git --version) " || fail "git missing" ; \
command -v ansible >/dev/null && ok " ansible: $$ (ansible --version | head -1) " ; \
2025-09-13 23:37:34 +00:00
command -v sops >/dev/null && ok " sops: $$ (sops --version --check-for-updates) " ; \
2025-09-13 23:46:48 +02:00
command -v age >/dev/null && ok " age: $$ (age --version) " ; \
2025-09-13 23:37:34 +00:00
command -v terraform >/dev/null && ok " terraform: $$ (terraform -version | head -1) " ; \
2025-09-13 23:46:48 +02:00
test -f keys/admin_ssh.pub && ok "keys/admin_ssh.pub present" || echo "ℹ add your SSH pubkey to keys/admin_ssh.pub" ; \
2026-08-23 12:02:23 +02:00
python3 scripts/check_secret_paths.py --tracked >/dev/null && ok "declared secret paths encrypted" || fail "secret path check failed" ; \
2025-09-13 23:46:48 +02:00
grep -q "age1" .sops.yaml && ok ".sops.yaml has an age recipient" || echo "ℹ add your age public key to .sops.yaml" ; \
git config --get core.hooksPath >/dev/null && ok " git hooksPath: $$ (git config --get core.hooksPath) " || echo "ℹ run: make hooks" ; \
'
# ---- Inventory convenience ----
2025-09-14 01:20:54 +00:00
new-host : ## Add a new host quickly: make new-host NAME=core1 TYPE=cpx11 REGION=nbg1 ROLE=core
2025-09-13 23:46:48 +02:00
@[ -n " $( NAME) " ] || ( echo "Usage: make new-host NAME=... [TYPE=...] [REGION=...] [ROLE=...] [IMG=...] [USER=...]" && exit 1)
@python3 scripts/new_host.py --name " $( NAME) " --type " $( TYPE) " --region " $( REGION) " --role " $( ROLE) " --image " $( IMG) " --user " $( USER) "
@echo " ✔ Added host $( NAME) to inventory/servers.yaml "
2025-09-13 20:26:11 +02:00
2025-09-13 23:34:27 +02:00
remote-set : ## Set origin to your Gitea repo (GITEA/OWNER/REPO vars)
git remote remove origin 2>/dev/null || true
git remote add origin https://$( GITEA) /$( OWNER) /$( REPO) .git
git branch -M main
git push -u origin main
@echo " ✔ Remote set to https:// $( GITEA) / $( OWNER) / $( REPO) .git "
2025-09-14 02:23:03 +00:00
# ==== Convergence (Ansible) ====
ANS_DIR := ansible
INV_SCRIPT := $( ANS_DIR) /inventory_from_yaml.py
PLAY := $( ANS_DIR) /playbooks/bootstrap.yaml
2026-08-15 15:41:59 +02:00
# Inventory servers.yaml sets ansible_user. Override only when needed:
# make converge SSH_USER=tegwick
SSH_USER ?=
ANSIBLE_USER_FLAG := $( if $( SSH_USER) ,-u $( SSH_USER) ,)
2025-09-14 02:23:03 +00:00
# Load your SOPS key for decryption when running playbooks (optional if you use keys.txt)
export SOPS_AGE_KEY := $( shell cat ~/.config/sops/age/keys.txt 2>/dev/null)
ansible-help : ## Show common Ansible commands
@echo "Convergence targets:"
@echo " make ansible-inventory # show resolved inventory"
@echo " make ansible-ping # ping all hosts"
@echo " make converge # run baseline convergence on all hosts"
@echo " make converge-host HOST=web-01# run on a single host"
@echo " make converge-tags TAGS=base # run only tagged tasks"
2026-08-15 15:41:59 +02:00
@echo " make converge-firewall HOST=Railiance01 # UFW only (RAIL-HO-WP-0009)"
2025-09-14 02:23:03 +00:00
@echo " make converge-check # dry-run (check mode)"
@echo " make converge-diff # show config diffs"
2026-08-23 12:41:23 +02:00
@echo " make verify-host HOST=Railiance01 # read-only host verification"
@echo " make verify-refresh-host HOST=Railiance01 APPROVE_VERIFY_REFRESH=REFRESH-GOSS-Railiance01"
2026-08-15 15:41:59 +02:00
@echo " make goss-status # last on-host timer result"
2025-09-14 02:23:03 +00:00
ansible-inventory : ## Print the dynamic inventory Ansible will use
cd $( ANS_DIR) && ansible-inventory --list | head -200
ansible-ping : ## Quick connectivity check (SSH + Python availability)
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible all $( ANSIBLE_USER_FLAG) -m ping
2025-09-14 02:23:03 +00:00
2026-03-09 11:08:16 +00:00
status : ## Show live security state of all hosts (UFW, fail2ban, SSH hardening)
@echo "=== Connectivity ==="
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible all $( ANSIBLE_USER_FLAG) -m ping
2026-03-09 11:08:16 +00:00
@echo "=== UFW ==="
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible all $( ANSIBLE_USER_FLAG) -m shell -a "ufw status" --become
2026-03-09 11:08:16 +00:00
@echo "=== fail2ban ==="
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible all $( ANSIBLE_USER_FLAG) -m shell -a "systemctl is-active fail2ban"
2026-03-09 11:08:16 +00:00
@echo "=== SSH hardening ==="
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible all $( ANSIBLE_USER_FLAG) -m shell -a "grep -iE '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config" --become
feat: implement WP-0002 — Goss test suite, verify playbook, and ADR-002
- goss/baseline.yaml: assertions for all spec/server-baseline.yaml items
(packages, services, SSH config, UFW rules, admin user, fail2ban, HISTCONTROL)
- goss/vars/baseline-vars.yaml: parameterised ports and paths
- ansible/roles/goss/: installs Goss binary (v0.4.9), deploys tests,
runs assertions in TAP format, fetches report to reports/
- ansible/playbooks/verify.yaml: playbook wrapping the goss role
- Makefile: add 'make verify' target; update 'make status' with hint
- docs/adr/ADR-002: formal repo boundary — railiance-hosts vs railiance-bootstrap
- workplans/RAIL-HO-WP-0002: registered workstream 8fed53c2, T03–T06 done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 12:38:48 +01:00
@echo ""
@echo "--- Hint: run 'make verify' for a structured pass/fail report ---"
2026-08-23 12:41:23 +02:00
verify : validate -handoff -readonly ## Read-only Goss verification of all hosts; writes TAP only on controller
@echo "Running read-only Goss baseline assertions..."
2026-08-15 15:41:59 +02:00
@cd $( ANS_DIR) && ansible-playbook playbooks/verify.yaml $( ANSIBLE_USER_FLAG) || \
feat: implement WP-0002 — Goss test suite, verify playbook, and ADR-002
- goss/baseline.yaml: assertions for all spec/server-baseline.yaml items
(packages, services, SSH config, UFW rules, admin user, fail2ban, HISTCONTROL)
- goss/vars/baseline-vars.yaml: parameterised ports and paths
- ansible/roles/goss/: installs Goss binary (v0.4.9), deploys tests,
runs assertions in TAP format, fetches report to reports/
- ansible/playbooks/verify.yaml: playbook wrapping the goss role
- Makefile: add 'make verify' target; update 'make status' with hint
- docs/adr/ADR-002: formal repo boundary — railiance-hosts vs railiance-bootstrap
- workplans/RAIL-HO-WP-0002: registered workstream 8fed53c2, T03–T06 done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 12:38:48 +01:00
( echo "One or more assertions FAILED — see reports/ for TAP output." && exit 1)
2026-03-09 16:44:06 +00:00
@echo "All assertions passed."
2026-03-09 11:08:16 +00:00
2026-08-15 19:04:40 +02:00
observe-railiance01 : ## Timestamped host capacity observation for resource-control
@mkdir -p docs/evidence/resource-hosteurope-railiance01/observations
@out= $$ ( mktemp) ; \
python3 scripts/observe-host-capacity.py Railiance01 > $$ out; \
stamp = $$ ( python3 -c " import json; print(json.load(open(' $$ out'))['observed_at'].replace(':','')) " ) ; \
dest = docs/evidence/resource-hosteurope-railiance01/observations/$$ stamp.json; \
mv $$ out $$ dest; \
ln -sfn $$ stamp.json docs/evidence/resource-hosteurope-railiance01/observations/latest.json; \
echo " wrote $$ dest "
2026-08-23 12:41:23 +02:00
verify-host : validate -handoff -readonly ## Read-only Goss verification: make verify-host HOST=Railiance01
2026-08-15 15:41:59 +02:00
@test -n " $( HOST) " || ( echo "Usage: make verify-host HOST=Railiance01" ; exit 1)
2026-08-23 12:41:23 +02:00
@echo " Running read-only Goss baseline assertions on $( HOST) ... "
2026-08-15 15:41:59 +02:00
@cd $( ANS_DIR) && ansible-playbook playbooks/verify.yaml $( ANSIBLE_USER_FLAG) -l $( HOST) || \
( echo "One or more assertions FAILED — see reports/ for TAP output." && exit 1)
2026-08-23 12:41:23 +02:00
verify-refresh : ## Refresh Goss on all hosts after review (exact approval required)
@test " $( APPROVE_VERIFY_REFRESH) " = "REFRESH-GOSS-ALL" || \
( echo "Refusing host mutation: set APPROVE_VERIFY_REFRESH=REFRESH-GOSS-ALL after review" ; exit 1)
cd $( ANS_DIR) && ansible-playbook playbooks/verify-refresh.yaml $( ANSIBLE_USER_FLAG)
verify-refresh-host : ## Refresh one host: HOST=... APPROVE_VERIFY_REFRESH=REFRESH-GOSS-<HOST>
@test -n " $( HOST) " || ( echo "Usage: make verify-refresh-host HOST=Railiance01 APPROVE_VERIFY_REFRESH=REFRESH-GOSS-Railiance01" ; exit 1)
@test " $( APPROVE_VERIFY_REFRESH) " = " REFRESH-GOSS- $( HOST) " || \
( echo " Refusing host mutation: set APPROVE_VERIFY_REFRESH=REFRESH-GOSS- $( HOST) after review " ; exit 1)
cd $( ANS_DIR) && ansible-playbook playbooks/verify-refresh.yaml $( ANSIBLE_USER_FLAG) -l $( HOST)
2026-08-15 15:41:59 +02:00
goss-status : ## Fetch last on-host Goss timer result (fails if FAILED flag present)
cd $( ANS_DIR) && ansible-playbook playbooks/goss-status.yaml $( ANSIBLE_USER_FLAG)
2025-09-14 02:23:03 +00:00
converge : ## Converge all hosts to the baseline (idempotent)
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible-playbook $( PLAY) $( ANSIBLE_USER_FLAG)
2025-09-14 02:23:03 +00:00
converge-host : ## Converge a single host: make converge-host HOST=core-01
@test -n " $( HOST) " || ( echo "Usage: make converge-host HOST=<name>" ; exit 1)
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible-playbook $( PLAY) $( ANSIBLE_USER_FLAG) -l $( HOST)
2025-09-14 02:23:03 +00:00
converge-tags : ## Run only certain tags: make converge-tags TAGS="base,ufw"
@test -n " $( TAGS) " || ( echo "Usage: make converge-tags TAGS=tag1,tag2" ; exit 1)
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible-playbook $( PLAY) $( ANSIBLE_USER_FLAG) --tags " $( TAGS) "
converge-firewall : ## Apply only UFW tasks: make converge-firewall HOST=Railiance01
@test -n " $( HOST) " || ( echo "Usage: make converge-firewall HOST=Railiance01" ; exit 1)
2026-08-22 12:34:25 +02:00
cd $( ANS_DIR) && ansible-playbook playbooks/firewall.yaml $( ANSIBLE_USER_FLAG) -l $( HOST) --tags firewall \
$( if $( REEF_DECLARATION) ,-e reef_declaration_path = " $( abspath $( REEF_DECLARATION) ) " ,)
validate-reef-exposure : ## Validate PORTS against REEF_DECLARATION without changing a host
@test -n " $( REEF_DECLARATION) " || ( echo "Usage: make validate-reef-exposure REEF_DECLARATION=... PORTS=80,443" ; exit 1)
python3 scripts/validate-reef-exposure.py --reef-declaration " $( REEF_DECLARATION) " --ports " $( or $( PORTS) ,80,443) "
2025-09-14 02:23:03 +00:00
converge-check : ## Dry-run (no changes), great for previews
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible-playbook $( PLAY) $( ANSIBLE_USER_FLAG) --check
2025-09-14 02:23:03 +00:00
converge-diff : ## Show file/templating diffs while applying changes
2026-08-15 15:41:59 +02:00
cd $( ANS_DIR) && ansible-playbook $( PLAY) $( ANSIBLE_USER_FLAG) --diff