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> |
||
|---|---|---|
| .. | ||
| 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.
- 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.
- 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_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/mirrormode the play refuses to create a pool on a disk that already has a filesystem/partition unlesszfs_pool_force: true. Double-checkzfs_pool_diskspoint at empty spare disks, not the OS disk; prefer stable/dev/disk/by-id/...paths in production.filemode (default) wipes nothing — it creates a loopback image atzfs_pool_file_path. - 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