Files
railiance-infra/Makefile
Bernd Worsch 6bb953090c feat: datetime reports, auto-commit on verify, register pruning EP
- Include time in TAP report filename (ISO 8601: date + HHmmssZ)
- Add changed_when: false to report write task — verify play now shows
  changed=0 on a clean run (all green recap)
- make verify auto-commits new reports to repo after a passing run;
  exits non-zero before committing if assertions fail
- Register EP-RAIL-001: report pruning extension point for future
  implementation when reports/ accumulates beyond a threshold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 16:44:06 +00:00

227 lines
11 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -------- RailianceHosts Make Utilities --------
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
# Set this to your Gitea host if you want 'remote-set' helper
GITEA ?= gitea.example.com
OWNER ?= coulomb
REPO ?= railiance-hosts
# New-host defaults (can be overridden: make new-host NAME=... TYPE=...)
TYPE ?= cpx11
REGION ?= nbg1
ROLE ?= core
IMG ?= ubuntu-24.04
USER ?= admin
# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally)
HCLOUD_TOKEN := $(shell sops -d --extract '["hetzner"]["token"]' secrets/hetzner-token.yaml 2>/dev/null)
# ---- Help ----
help: ## Show this help
@echo "RailianceHosts Commands"; \
grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | sort | sed 's/:.*##/: /'
# ---- Git hooks ----
hooks: ## Configure git to use repo-local hooks (.githooks) and ensure executables
@mkdir -p .githooks
git config core.hooksPath .githooks
@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"
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
@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
@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
sops secrets/hetzner-token.yaml
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)"
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)
sops-rotate: ## Rotate recipients on a SOPS file (after updating .sops.yaml)
@[ -n "$(FILE)" ] || (echo "Usage: make sops-rotate FILE=secrets/xxx.sops.yaml" && exit 1)
sops --rotate --in-place $(FILE)
check-secrets: ## Fail if any file in secrets/ is not SOPS-encrypted
@! (git ls-files secrets | xargs -r grep -L -E '(^sops:$$|\"sops\"[[:space:]]*:)' | tee /dev/stderr | read) \
|| (echo "❌ Unencrypted secrets detected above. Encrypt with: sops --encrypt --in-place <file>"; exit 1)
@echo "✔ All files in secrets/ appear SOPS-encrypted"
# ---- Terraform (Hetzner) ----
tf-fmt: ## Terraform fmt
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1)
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); @terraform -chdir=terraform/hetzner fmt -recursive || true
tf-init: ## Terraform init
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1)
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner init
tf-plan: tf-init ## Terraform plan (requires decrypted HCLOUD_TOKEN)
@echo "🔍 Running terraform plan..."
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1)
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner plan -var="hcloud_token=$(HCLOUD_TOKEN)"
tf-apply: tf-init ## Terraform apply (provision)
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1)
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner apply -auto-approve -var="hcloud_token=$(HCLOUD_TOKEN)"
tf-destroy: tf-init ## Terraform destroy (tear down)
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1)
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner destroy -auto-approve -var="hcloud_token=$(HCLOUD_TOKEN)"
# --- 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)")
# ---- Ansible ----
ansible-bootstrap: ## Run base bootstrap play (users, ssh, ufw, sops-agent)
cd ansible && ansible-playbook playbooks/bootstrap.yaml -u admin
# ---- Orchestration ----
apply: tf-fmt tf-apply ansible-bootstrap ## Provision via Terraform then converge via Ansible
# ---- Utilities ----
doctor: ## Check tools and basic repo setup
@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)"; \
command -v sops >/dev/null && ok "sops: $$(sops --version --check-for-updates)"; \
command -v age >/dev/null && ok "age: $$(age --version)"; \
command -v terraform >/dev/null && ok "terraform: $$(terraform -version | head -1)"; \
test -f keys/admin_ssh.pub && ok "keys/admin_ssh.pub present" || echo " add your SSH pubkey to keys/admin_ssh.pub"; \
test -f inventory/group_vars/secrets.sops.yaml && ok "secrets.sops.yaml present" || echo " create inventory/group_vars/secrets.sops.yaml"; \
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 ----
new-host: ## Add a new host quickly: make new-host NAME=core1 TYPE=cpx11 REGION=nbg1 ROLE=core
@[ -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"
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"
# ==== Convergence (Ansible) ====
ANS_DIR := ansible
INV_SCRIPT := $(ANS_DIR)/inventory_from_yaml.py
PLAY := $(ANS_DIR)/playbooks/bootstrap.yaml
SSH_USER ?= admin
# 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"
@echo " make converge-check # dry-run (check mode)"
@echo " make converge-diff # show config diffs"
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)
cd $(ANS_DIR) && ansible all -u $(SSH_USER) -m ping
status: ## Show live security state of all hosts (UFW, fail2ban, SSH hardening)
@echo "=== Connectivity ==="
cd $(ANS_DIR) && ansible all -u $(SSH_USER) -m ping
@echo "=== UFW ==="
cd $(ANS_DIR) && ansible all -u $(SSH_USER) -m shell -a "ufw status" --become
@echo "=== fail2ban ==="
cd $(ANS_DIR) && ansible all -u $(SSH_USER) -m shell -a "systemctl is-active fail2ban"
@echo "=== SSH hardening ==="
cd $(ANS_DIR) && ansible all -u $(SSH_USER) -m shell -a "grep -iE '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config" --become
@echo ""
@echo "--- Hint: run 'make verify' for a structured pass/fail report ---"
verify: ## Run Goss test suite against all hosts, commit TAP reports — exits non-zero on failure
@echo "Running Goss baseline assertions..."
@cd $(ANS_DIR) && ansible-playbook playbooks/verify.yaml -u $(SSH_USER) || \
(echo "One or more assertions FAILED — see reports/ for TAP output." && exit 1)
@echo "All assertions passed."
@git add reports/ && \
git diff --cached --quiet && echo "No new reports to commit." || \
git commit -m "chore: Goss verification reports $$(date -u +%Y-%m-%dT%H%M%SZ)"
converge: ## Converge all hosts to the baseline (idempotent)
cd $(ANS_DIR) && ansible-playbook $(PLAY) -u $(SSH_USER)
converge-host: ## Converge a single host: make converge-host HOST=core-01
@test -n "$(HOST)" || (echo "Usage: make converge-host HOST=<name>"; exit 1)
cd $(ANS_DIR) && ansible-playbook $(PLAY) -u $(SSH_USER) -l $(HOST)
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)
cd $(ANS_DIR) && ansible-playbook $(PLAY) -u $(SSH_USER) --tags "$(TAGS)"
converge-check: ## Dry-run (no changes), great for previews
cd $(ANS_DIR) && ansible-playbook $(PLAY) -u $(SSH_USER) --check
converge-diff: ## Show file/templating diffs while applying changes
cd $(ANS_DIR) && ansible-playbook $(PLAY) -u $(SSH_USER) --diff