Some sanoid packages (e.g. on Ubuntu 26.04) don't ship /etc/sanoid, so the config template failed with 'Destination directory does not exist'. Create the directory explicitly, and copy the packaged sanoid.defaults.conf into it (sanoid requires it beside sanoid.conf) so the first timer run succeeds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| group_vars | ||
| inventory | ||
| roles | ||
| ansible.cfg | ||
| README.md | ||
| requirements.yml | ||
| site.yml | ||
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
- An Ubuntu 24.04 VM you can SSH into as a sudo-capable user.
- A dedicated second virtual disk attached to the VM for the ZFS pool
(e.g.
/dev/sdbor/dev/vdb) — separate from the OS disk. - Ansible on your workstation, installed one way only — prefer
pipx install --include-deps ansible(isolated). Mixing apt'sansiblewith a pipansiblecauses version-skew errors such asNo 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_disk, keys, etc.
Key variables in group_vars/all.yml:
| Variable | Meaning |
|---|---|
zfs_pool_disk |
The dedicated disk for the pool. Its contents will be 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 is destructive: the play refuses to create a pool on a disk that
already has a filesystem/partition unless
zfs_pool_force: true. Double-checkzfs_pool_diskpoints at the empty spare disk, not the OS disk. For production prefer a stable/dev/disk/by-id/...path over/dev/sdb. - SSH lock-out: the play asserts that
admin_authorized_keysis non-empty before it will disable password authentication. Verify you can log in with your key before settingssh_disable_password_auth: true. - Firewall coexistence: host inbound rules live in a dedicated
inet heleosnftables table and never flush the global ruleset, so Docker's own iptables/nft rules are left intact. Container SMTP egress is blocked via aDOCKER-USERjump applied by theheleos-docker-egressservice.
What this sets up
- ZFS pool
tankwith platform datasets and thecustomersparent (see ../../docs/03-naming-conventions.md). - Docker Engine + Compose plugin, data-root on
tank/platform/dockerusing the nativezfsstorage 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
sftponlygroup 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 settingzfs_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