platform/platform-infra/ansible
Bart Van Geyt a90d07875d chore(ansible): set admin ed25519 key for platform hosts
Provide the admin's ed25519 public key in admin_authorized_keys so the
ssh_hardening role installs it, enabling key-based login (and allowing
ssh_disable_password_auth to be flipped on later).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 03:54:01 +02:00
..
group_vars chore(ansible): set admin ed25519 key for platform hosts 2026-08-09 03:54:01 +02:00
inventory Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
roles feat(ansible/zfs): support file-backed pool (+ single/mirror modes) 2026-08-09 03:09:38 +02:00
ansible.cfg Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
README.md feat(ansible/zfs): support file-backed pool (+ single/mirror modes) 2026-08-09 03:09:38 +02:00
requirements.yml Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
site.yml Phase 5: backup/DR automation (backup Ansible role) 2026-07-07 18:04:41 +02:00

Phase 1 — Host baseline (Ansible)

Idempotent host configuration for the heleos platform, targeting Ubuntu 24.04 LTS. Roles: base packages → ZFS pool/datasets → Docker (data-root on ZFS) → nftables firewall + container egress filter → SSH hardening → backup/DR (sanoid snapshots, per-DB dumps, offsite).

Prerequisites

  1. An Ubuntu 24.04 VM you can SSH into as a sudo-capable user.
  2. Storage for the ZFS pool — pick one via zfs_pool_mode:
    • file (default): a loopback disk image, no spare disk needed — ideal for a cost-optimized VM. Real ZFS, backed by a file on the OS disk.
    • single / mirror: a dedicated spare disk (or two, mirrored) for a real host.
  3. Ansible on your workstation, installed one way only — prefer pipx install --include-deps ansible (isolated). Mixing apt's ansible with a pip ansible causes version-skew errors such as No module named 'ansible.module_utils.six.moves'; if you hit that, remove the duplicates and reinstall via pipx.

Configure

cd platform-infra/ansible
ansible-galaxy collection install -r requirements.yml

cp inventory/hosts.yml.example inventory/hosts.yml   # edit host/user (git-ignored)
$EDITOR group_vars/all.yml                            # set zfs_pool_mode, keys, etc.

Key variables in group_vars/all.yml:

Variable Meaning
zfs_pool_mode file (loopback image, default), single, or mirror.
zfs_pool_file_path / zfs_pool_file_size File-mode image location + size (sparse).
zfs_pool_disks Disk(s) for single/mirror mode. Contents destroyed.
zfs_pool_force Must be true to create a pool on a non-empty disk (safety gate).
admin_authorized_keys Public keys for the admin — required before disabling passwords.
ssh_disable_password_auth Leave false until key login is verified, then flip to true.
smtp_relay_host Optional; if set, containers may reach SMTP only on this host.

Run

ansible-playbook site.yml --syntax-check   # no-host pre-flight
ansible-playbook site.yml -K               # apply (-K prompts for the sudo/become
                                           # password; omit only if the user has
                                           # passwordless sudo on the VM)

Everything runs via become (root), so -K is required unless the target user has passwordless sudo (/etc/sudoers.d/… NOPASSWD:ALL).

Run a single layer with tags: --tags zfs, --tags docker, --tags firewall, --tags ssh, --tags base.

Running from Windows / WSL

Files on the /mnt/c drive mount are world-writable (mode 0777), so Ansible ignores ansible.cfg there (a security measure) — which then loses the inventory path and you get "no hosts matched". Two ways around it:

  • Preferred: copy the repo into your WSL home and run from there, where permissions are normal (also much faster):
    cp -r /mnt/c/claude/heleosv2 ~/heleosv2 && cd ~/heleosv2/platform-infra/ansible
    
  • Or force the config explicitly (bypasses the world-writable check):
    export ANSIBLE_CONFIG=$(pwd)/ansible.cfg
    

Either way, create the inventory first (cp inventory/hosts.yml.example inventory/hosts.yml and edit it). WSL itself is not a valid target host (no spare disk for the ZFS pool); point the inventory at your Ubuntu VM.

Pre-flight without a host: ansible-playbook --syntax-check site.yml (--check is not meaningful on the first run — see Safety notes).

Safety notes

  • ZFS disk modes are destructive: in single/mirror mode the play refuses to create a pool on a disk that already has a filesystem/partition unless zfs_pool_force: true. Double-check zfs_pool_disks point at empty spare disks, not the OS disk; prefer stable /dev/disk/by-id/... paths in production. file mode (default) wipes nothing — it creates a loopback image at zfs_pool_file_path.
  • SSH lock-out: the play asserts that admin_authorized_keys is non-empty before it will disable password authentication. Verify you can log in with your key before setting ssh_disable_password_auth: true.
  • Firewall coexistence: host inbound rules live in a dedicated inet heleos nftables table and never flush the global ruleset, so Docker's own iptables/nft rules are left intact. Container SMTP egress is blocked via a DOCKER-USER jump applied by the heleos-docker-egress service.

What this sets up

  • ZFS pool tank with platform datasets and the customers parent (see ../../docs/03-naming-conventions.md).
  • Docker Engine + Compose plugin, data-root on tank/platform/docker using the native zfs storage driver; per-site network address pool preconfigured.
  • nftables default-deny inbound (allow SSH/80/443 + established + loopback + ICMP); outbound SMTP blocked from containers.
  • Hardened SSH (key-first, root prohibit-password) and the sftponly group that per-customer SFTP accounts will join in Phase 4.
  • Backup/DR (Phase 5): sanoid snapshot policy + timer, nightly per-database dumps with rotation, and optional offsite zfs send (syncoid) / rsync — enable offsite by setting zfs_offsite_target / db_offsite_target.

Verify after running

zpool status && zfs list
docker info | grep -E 'Storage Driver|Docker Root Dir'
sudo nft list table inet heleos
sudo iptables -S DOCKER-USER