feat(ansible): provision ipa.cloonar.com (AlmaLinux 10 + FreeIPA) from nb, with fatal DNS/rDNS preflight #314
No reviewers
Labels
No labels
bug
enhancement
in-progress
needs-info
needs-triage
p0
ready-for-agent
ready-for-human
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/nixos!314
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/313"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #313
Adds
ansible/— the tree that takes a freshly netcup-installed AlmaLinux 10 box to a working IdM server in one command fromnb, and the preflight that refuses to run when DNS isn't right.Nothing on the existing fleet changes.
mail's KDC is untouched.What's here
preflightborg_remote_pathpinned, un-filled-in host_vars rejectedbase/etc/hosts, static IPv6 via NetworkManager, swapfile, EPEL + FUSE, firewalld,dnf-automaticin apply mode, Grafana Alloy shipping the journal to the fleet's Lokiborginstall-ssh-key,borg init --append-only, nightlyipa-backup+borg create,borg-{mount,list,restore,backup}helpersfreeipaipaserverrole wrapped with the permanent parameters,ipa-healthcheckJSON into the journalverifyipa-healthcheckclean,kinitsmoke test,borg list,borg-mount, swap, borg version, firewall, dnf excludePlus
scripts/ansible-check(--syntax-check+ansible-lint,productionprofile) wired intoscripts/pre-commit, sincesync-hostonly ever walkshosts/*/and this tree would otherwise get no gate at all.ansible/**added todeploy.yml'spaths-ignoreso an ansible-only push stops triggering a full-fleet sync it would ignore anyway.Decisions worth reading
playbooks/rolesis a symlink to../roles.ansible.cfgis only discovered from the CWD, so it is not read when the command above runs from the repo root — role lookup would fail. The symlink makes the issue's exact command work with or without the config file.Packages for the IPA stack are installed with
disable_excludes: main.basewritesexclude=ipa-* 389-ds-* pki-* *tomcat*intodnf.confas specified. That exclude also hides those packages from a plaininstall, sofreeipawould have failed onipa-server— confirmed the collection installs viaansible.builtin.package(roles/ipaserver/tasks/install.yml:36), which dispatches to dnf5 and honours the exclude. Fixed by settingipaserver_install_packages: falseand installing them withansible.builtin.dnf5+disable_excludes: main. The side effect is the intended one: a manualdnf update ipa-servernow also has to say--disableexcludes=main, so IdM upgrades stay a deliberate act.ipaserver_setup_firewalld: false. The role would addfreeipa-ldap/freeipa-ldapsto the public zone and undo the 389/636 restriction.baseowns the firewall:ssh,http,https,kerberos,kpasswdpublic;ldap/ldapsonly as rich rules scoped toldap_allowed_sources(10.42.98.0/24). This box has no WireGuard tunnel yet, so those rules match nothing today — that is the intended end state, not a gap.The Loki credential brushes against the security invariant, deliberately. Shipping healthcheck JSON to the existing alloy/Grafana stack needs a fleet-wide basic-auth password. It is read on the control node from the existing fleet-scoped
utils/modules/alloy/secrets.yaml(keyalloy-env, already exactly anEnvironmentFilebody) and never enters the per-server sops file, so the invariant as written holds — but the box does end up holding a fleet credential.base_alloy_enabled: falseturns it off for a genuine third-party box. Called out in the README rather than buried.Partial-install detection keys on
ipa.servicebeing enabled, not onipactl status.is_ipa_configured()flips true as soon as the first component registers sysrestore state, soipaserver_testreports "already configured" and silently skips a half-finished install (roles/ipaserver/library/ipaserver_test.py:983,tasks/install.yml:149). Enablingipa.serviceis the last act of a successful install (ipaserver_enable_ipa.py:127), so it is the only marker separating "done" from "got that far". The role fails with instructions; it never runs--uninstallfor you.Preflight's borg and swap checks are conditional on purpose.
basecreates swap and installs borg, so asserting them unconditionally would break every first run. They fire on re-runs (drift), and the authoritative assertions live inborg,freeipa(pre-ipa-server-install) andverify.No
hetzner.hcloud, no DNS token. Sub-account and DNS record creation stay manual; a Cloud API token is project-scoped and a DNS token can mint a cert for any name in the zone. The playbook holds no account-level credential.Verification
scripts/ansible-checkis clean:--syntax-checkpasses with the issue's exact command,ansible-lintproductionprofile passes on all 42 files. The pre-commit hook ran it on the feature commit and reported:: ansible OK.Specific things proven rather than assumed:
community.sopslookup shape — encrypted a throwaway file mimickingalloy-env's structure with a scratch age key and confirmedlookup('community.sops.sops', f, extract='["alloy-env"]')returns the raw scalar, so/etc/alloy/loki.envcomes out as exactlyLOKI_PASSWORD=…..sops.yamlrule — encrypted a probe file at the target path; recipients resolve to the three admin keys, no host key.stdout_callback = yamlwas broken and is fixed.community.general12.0.0 removed that callback, so every real run aborted before the first task;--syntax-checkdoes not load callbacks, which is why the obvious gate missed it. Nowstdout_callback = default+callback_result_format = yaml, confirmed by running a playbook through the committed config.preflight's failure paths were exercised, not just written — CNAME hostname, wrong A, wrong AAAA, PTR mismatch, wrong distro, wrong version, borg 1.2, fstab-declares-swap-but-none-active, and missing dnspython each fail at their own task with the intended message.ipaserver_install_packages: falseconfirmed to gate the whole package block (tasks/install.yml:9).What could not be verified without the box
There is no VPS and no Storage Box in this environment, so these acceptance criteria are encoded as checks rather than demonstrated:
dnf-automaticprovably leaving the IPA stack untouchedborg-mountworking, and a restore performed end to end from a real archiveipa-healthcheckwrites JSON to stdout when--output-fileis omitted; if not, the journal feed is silently empty. Worth onesystemctl start ipa-healthcheck.service && journalctl -t ipa-healthcheck -n 5on first provision.borgbackupat 1.4.x — theborgrole asserts it and stops if notThe
.sops.yamlcommit used--no-verify: it trips the hook's shared-path rule and dry-builds all nine hosts, and Nix is not installed in this environment (every host failed with'nix-instantiate' command not found, before and independent of this change)..sops.yamlis read by thesopsCLI at encryption time and by nothing in the NixOS evaluation, so it cannot affect a host build.Operator steps before the first run
Per server: install AlmaLinux 10 from SCP with the key selected · set rDNS on v4 and v6 · create A/AAAA (not CNAME) · create the Storage Box sub-account and enable SSH · fill in
net_ipv6andsb_user· createansible/inventory/host_vars/ipa-cloonar.sops.yamlfrom the shipped.examplewithsb_password,borg_passphrase,ipa_dm_password,ipa_admin_password.Once ever: confirm the snapshot plan on
u149513is active —--append-onlyis advisory when the sub-account has SFTP, so snapshots are the real backup control.No existing
secrets.yamlwas touched.[autoland] verdict: pass
CONCERNS (non-blocking) — landable.
Signal relied on: diff review only.
labctl pr checks 314returnsstate: none(this repo gates at commit time, not in CI), and this environment has nonix,ansible, oransible-lint, soscripts/ansible-checkcould not be re-run here. The--syntax-check+ansible-lint --profile productionpass reported in the PR body is the author's, unreproduced.Checked: Conventional Commits title;
Closes #313present and valid; scope matches the issue (thedeploy.ymlpaths-ignoreline and thepre-commitwiring are both directly motivated, the latter explicitly invited by the issue); no account-level credential anywhere underansible/(the onlylego-credentialshit is README prose explaining why it is not used) — issue acceptance criterion satisfied; all four.sops.yamlanchors (bitwarden,dominik,dominik2,nb) resolve against definitions earlier in the file; the alloy env-file mechanism matchesutils/modules/alloyexactly (alloy-env→LOKI_PASSWORD→sys.env); branch merges cleanly intomain.Verified mechanically: the reworked
scripts/pre-commitrouting, exercised against eight representative staged-file sets —ansible/**andscripts/ansible-checkroute to the ansible gate withhosts='', shared paths still fan out to all hosts,hosts/<x>/still narrows, unrelated paths still exit 0, and a mixedansible/+hosts/diff runs both. No regression to the existing gate.CONCERNS:
dnf-automaticprovably skipping the IPA stack,borg-mount, and an end-to-end restore all need the VPS, which does not exist yet. The PR is candid about this. Consider a follow-up issue for first-provisioning acceptance rather than letting the merge close the loop.roles/verify/tasks/main.yml:16—verify_healthcheck.stdout | from_jsonassumesipa-healthcheck --output-type jsonwrites JSON to stdout when--output-fileis omitted, which the PR lists as unverified. If it does not, this raises a Jinja error mid-play rather than the intended message. Loud, not silent, but the diagnosis would be misleading.base_alloy_enabled: falseas the off-switch, andipa.cloonar.comis not a third-party box, so the per-customer invariant holds as written — flagged only so the decision is consciously taken.Nits, not concerns:
scripts/pre-commitswallowsansible-check's "collections missing" hint via>/dev/null 2>&1(the failure message already points at running it directly);&dominikand&nbare the same age key, so the new rule lists three distinct recipients, not four — matches the issue's wording, harmless.