Commit Graph

1250 Commits

Author SHA1 Message Date
unkin-agent 5345216b12 Allow ssh from the 172.18.30-31.0/24 dmz vpcs
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-14 22:21:33 +11:00
unkin-agent 0f2814ccb5 Merge remote-tracking branch 'origin/develop' into benvin/mail-edge-dnsmasq
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
# Conflicts:
#	hieradata/roles/infra/mail/gateway.yaml
#	site/roles/manifests/infra/mail/gateway.pp
2026-10-13 21:58:27 +11:00
unkin-agent 343d645d24 Adapt base classes for the mail edge gateway (#555)
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
The edge gateways sit in BinaryLane VPCs with no consul datacentre and a public interface, so the consul agent cannot join and sshd must not serve the internet.

- add an enable toggle to profiles::consul::client, off for the gateway role
- disable node_exporter on the gateway role, as it registers via consul
- restrict sshd AllowUsers to the edge VPCs (198.18.30.0/24, 198.18.31.0/24) and the lab (198.18.0.0/16, 10.10.12.200/32)

Stacked on #554.

Reviewed-on: #555
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-13 21:48:03 +11:00
unkin-agent b1f2fff045 Resolve subnet facts from secondary interfaces (#557)
BinaryLane edge hosts have a public primary IP, so region/environment/country/zone resolve to unknown even though their VPC NIC sits in a known subnet.

- match the primary IP first, unchanged for existing hosts
- otherwise scan interface bindings in name order, skipping lo, dum* and wg*
- add modules/libs/test/subnet_facts_check.rb (`ruby modules/libs/test/subnet_facts_check.rb`)

Reviewed-on: #557
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-13 21:46:53 +11:00
unkin-agent 4313b7db49 Fix edge postfix gateway config for unkin.net (#552)
The mail gateway config was copied from an eFa appliance and predates the edge design: unfiltered/dead DNSBLs, a MailScanner milter, no local filtering, and no edge identity.

- filter postscreen DNSBL codes, add mailspike/psbl, drop sorbs/swl; dnswl clients skip greet tests
- run local rspamd (deterministic checks, redis bayes, edge.cf rules fetched each puppet run) as a tempfail milter
- set a 50MB size limit; drop qmqpd, 3DES and eFa leftovers
- relay unkin.net and main.unkin.net to [mail-in.main.unkin.net]:25 with verified TLS and recipient probing
- set myhostname and vault cert SANs per node (mta1/mta2.edge.unkin.net)

Reviewed-on: #552
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-13 21:42:38 +11:00
unkin-agent 693452b4f8 Map the binarylane DMZ VPCs in subnet facts (#556)
The BinaryLane mail edge DMZ VPCs need environment, region, country and zone facts like every other lab subnet.

- map 198.18.30.0/24 to prod/syd2/au/dmz (binarylane syd dmz vpc)
- map 198.18.31.0/24 to prod/mel1/au/dmz (binarylane mel dmz vpc)

Reviewed-on: #556
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-13 21:37:57 +11:00
unkin-agent 01f242657c Merge remote-tracking branch 'origin/benvin/mail-edge-role' into benvin/mail-edge-dnsmasq
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-13 21:24:31 +11:00
unkin-agent 89efe4f413 Relay main.unkin.net through the mail edge gateways
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-13 21:24:23 +11:00
unkin-agent 28d02a279f Merge remote-tracking branch 'origin/benvin/mail-edge-role' into benvin/mail-edge-dnsmasq
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was canceled
ci/woodpecker/pr/ruby-check Pipeline was canceled
ci/woodpecker/pr/erb-validate Pipeline was canceled
ci/woodpecker/pr/epp-validate Pipeline was canceled
# Conflicts:
#	hieradata/roles/infra/mail/gateway.yaml
#	site/roles/manifests/infra/mail/gateway.pp
2026-10-13 21:23:52 +11:00
unkin-agent 85a9f47025 Add edge PTR names to the mail gateway vault certs
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-13 21:18:54 +11:00
unkin-agent 9484e8b77b Add local rspamd and redis to the mail edge gateway (#553)
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
Edge gateways need cheap deterministic in-session filtering behind the postfix milter, with no AI and no ClamAV on 1GB VMs.

- add a signature-checked rspamd yum repo via artifactapi, enabled for the gateway role
- add profiles::rspamd::edge managing the package and local.d files from hiera
- fetch rule files without blocking the service: empty placeholder, swap only on a successful changed fetch, then reload
- run a self-scanning milter proxy on 11332; reject at 13, header at 6, no greylisting, subject rewrite or gpt
- add a local 128MB redis for bayes autolearn; load SpamAssassin rules from rspamd-rules
- bypass rspamd for mail relayed out by the smarthost

Stacked on #552; requires terraform-artifactapi #63 applied first.

Reviewed-on: #553
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-13 21:14:16 +11:00
unkin-agent d5741202b1 Merge remote-tracking branch 'origin/benvin/mail-edge-rspamd' into benvin/mail-edge-dnsmasq
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-13 20:37:19 +11:00
unkin-agent 7c1c5967e9 Decouple rspamd rule fetches from the service and verify repo signatures
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-13 20:37:14 +11:00
unkin-agent ba94dc2183 Merge remote-tracking branch 'origin/benvin/mail-edge-role' into benvin/mail-edge-rspamd 2026-10-13 20:36:02 +11:00
unkin-agent a7c2b40b72 Clarify postscreen dnswl bypass threshold comment
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-13 20:35:59 +11:00
unkin-agent 05c1176cdc Run dnsmasq as the mail edge local resolver
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-12 22:38:09 +11:00
unkin-agent 7878d35587 Add local rspamd and redis to the mail edge gateway
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-12 22:37:36 +11:00
unkin-agent bca6dce407 Fix edge postfix gateway config for unkin.net
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-12 22:36:08 +11:00
unkin-agent 6c880d9794 Add consul server federation SANs to consul server certs (#547)
WAN federation through mesh gateways makes Consul verify server certs against `server.<dc>.consul` and `<node_name>.server.<dc>.consul`. Node names are FQDNs, so each server needs its exact SAN. This adds the SANs ahead of Consul server TLS, which a stacked follow-up enables once the reissued certs are verified.

- add `server.<country>-<region>.consul` to consul server alt_names
- add `<fqdn>.server.<country>-<region>.consul` to consul server alt_names

Reviewed-on: #547
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-09 21:51:43 +11:00
unkin-agent 2e95cd00d7 Migrate k8s roles to puppet-on-k8s (#545)
Move the k8s roles to the puppet-on-k8s compilers. Their role code (rke2, frrouting, ceph) has no exported resources or PuppetDB queries; the base consul lookup is covered by the static syd1 server list.

- enable profiles::puppet::migrate in the shared roles::infra::k8s hieradata (compute, control, node)

Reviewed-on: #545
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 22:10:50 +11:00
unkin-agent 171cf23a93 Migrate incus nodes to puppet-on-k8s (#546)
Incus nodes still run against the VM puppetservers; move them to the k8s compilers as part of the puppet-on-k8s migration.

- enable profiles::puppet::migrate for roles::infra::incus::node

Reviewed-on: #546
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 22:03:30 +11:00
unkin-agent 83cb039b97 consul: use static server list for clients (#544)
Hosts compiled on the k8s puppetservers can't see the consul servers in PuppetDB, so the members lookup returns nothing and syd1 clients get no retry_join targets.

- set a static consul server list for syd1 and disable the members lookup there
- keep the PuppetDB members lookup for all other regions

Reviewed-on: #544
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 21:33:03 +11:00
unkin-agent fc26d3cc4a nzbget: fix params lookup copied from sonarr (#543)
nzbget defaulted bind_address and port from sonarr::params, which is not loaded on nzbget-only nodes, so catalog compilation failed with "Unknown variable".

- default bind_address/port from nzbget::params

Reviewed-on: #543
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 13:36:52 +11:00
unkin-agent 5fe9f7a9da Migrate media roles to puppet-on-k8s (#540)
The ausyd1nxvm2120 canary is healthy on the puppet-on-k8s servers, so move the next role wave across: the media nodes (ausyd1nxvm2045-2051: nzbget, sonarr, radarr, lidarr, readarr, prowlarr, jellyfin).

- enable `profiles::puppet::migrate` in `roles/apps/media.yaml`

Reviewed-on: #540
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 12:10:31 +11:00
unkin-agent 18fd966f92 halb: stop publishing haproxy2 site CNAMEs (#542)
The haproxy2 VMs are being decommissioned and the site names they front move to bind-internal DNSRecord CRs in argocd-apps. dns-updater on these hosts deletes records that leave its records file, so the CNAMEs drop once this applies.

- empty profiles::haproxy::dns::vrrp_cnames for haproxy2
- empty profiles::haproxy::dns::cnames for haproxy2

Reviewed-on: #542
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 01:31:54 +11:00
unkin-agent b74bced771 Persist rp_filter=0 on every router interface (#539)
Effective rp_filter is max(all, <iface>). Setting `all`/`default` to 0 is not enough after a reboot: `/usr/lib/sysctl.d/50-redhat.conf` sets `net.ipv4.conf.*.rp_filter = 1`, which udev applies to every interface as it is created, so routed asymmetric traffic gets dropped once shorewall goes.

- add `net.ipv4.conf.*.rp_filter: 0` to the router role, overriding the vendor glob
- disable `enforce` for it, since `sysctl -n` cannot read glob keys

Reviewed-on: #539
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 16:02:00 +11:00
unkin-agent 0cfe598f90 Install certmanager and sshsignhost from RPM (#524)
certmanager and sshsignhost are now Go binaries released as RPMs, and their packaged paths collide with the venv install these helper classes manage.

- Drop the pyvenv, pip, rendered script and /usr/local/bin symlink resources
- Delete the now-unused Python script templates
- Keep rendering /opt/<tool>/config.yaml, unchanged ownership and mode
- Nest certmanager's output_path under vault:, where the binary reads it
- Drop output_path from sshsignhost's config; the binary has no such key
- Pin certmanager to 0.2.0 and sshsignhost to 0.1.0 on the puppet master role
- Point sshsignhost at the sshca mount and signhost role, documented in doc/vault

Reviewed-on: #524
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 15:07:54 +11:00
unkin-agent 0272104504 Add wireguard module (#538)
WireGuard on the router is configured by hand, so its tunnels are not reproducible from code. This adds a module to manage it from hieradata.

- add `wireguard` class to install wireguard-tools and manage interfaces from a hash
- add `wireguard::interface` to render `/etc/wireguard/<iface>.conf` (0600) and enable `wg-quick@<iface>`
- keep private and preshared keys `Sensitive` end to end (`wireguard::interfaces` lookup_options `convert_to: Sensitive`, typed peer Struct)
- without `private_key`, generate `/etc/wireguard/<iface>.key` (0600) only if absent and load it via PostUp, so the key never rotates
- apply config changes with `wg syncconf` instead of restarting the tunnel

Reviewed-on: #538
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 14:17:07 +11:00
unkin-agent 1ed268acda Add dnsmasq module (#537)
The router runs dnsmasq as a DNS forwarder and DHCP relay from hand-edited config, so its state is not reproducible from code.

- add `dnsmasq` module installing the package, rendering `/etc/dnsmasq.conf` from class params and running the service
- cover listen addresses/interfaces, bind mode, upstream servers, no-resolv, cache-size, domain-needed, bogus-priv, per-domain forwards and dhcp-relay
- add raw `options` lines for anything else
- add `purge_config_dir` (default off) to remove unmanaged `/etc/dnsmasq.d` files

Reviewed-on: #537
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 22:40:46 +10:00
unkin-agent 47e3bdc8f5 Add router role for prodnxsr0020 (#536)
prodnxsr0020 runs FRR/OSPF hand-configured; bring its routing config under puppet without touching interfaces, firewall or dnsmasq.

- add roles::infra::network::router (base + frrouting + frr_exporter)
- enable ip_forward and disable rp_filter via sysctl::base
- add prodnxsr0020 OSPF config (dum0, dum1, bond0.201; src 198.18.21.160)
- pin dns, consul and router-id to dum0 instead of the WAN-facing primary IP
- listen sshd on 127.0.0.1 and dum0 only, knocking out the common WAN primary IP
- keep resolv.conf on the local dnsmasq (127.0.0.1)

Reviewed-on: #536
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 22:40:01 +10:00
unkin-agent 410a1f13d0 Add 198.18.2.0/24 router loopback subnet (#535)
Router loopbacks in 198.18.2.0/24 (e.g. prodnxsr0020, 198.18.2.160) match no subnet entry, so they get unknown environment/region/country facts and are not autosigned.

- add 198.18.2.0/24 to subnet_facts as prod/syd1/au/common
- add 198.18.2.0/24 to puppet master autosign subnet_ranges

Reviewed-on: #535
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 20:46:44 +10:00
unkin-agent ec74484d89 Pin the journald ingest URL to port 443 (#533)
Estate journald ingestion has been down since 00:33Z. `systemd-journal-upload` appends `:19532/upload` to the configured URL whenever that URL carries no explicit port, so the portless k8s endpoint became `/insert/journald:19532/upload`, which vlinsert rejects as an unsupported path. The previous consul URL only worked because `:9428` was explicit.

- pin `victorialogs::client::journald::inserturl` to port 443

Requests then land on `/insert/journald/upload`, which vlinsert accepts.

Reviewed-on: #533
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 11:22:07 +10:00
unkin-agent c0e65fade3 Ship estate journald logs to the k8s log ingest endpoint (#531)
The estate ships journald to the VM VictoriaLogs cluster, which is being left to age out rather than grow. New log capacity lands in k8s, so clients need to point there while the VM cluster keeps serving historical queries until its retention lapses.

- repoint victorialogs::client::journald::inserturl at https://logs-ingest.k8s.syd1.au.unkin.net/insert/journald

VMs already trust the issuing CA via the system bundle, so journal-upload needs no TLS change.

Requires the k8s VLCluster deployed and serving /insert/journald first.

Reviewed-on: #531
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 10:01:49 +10:00
unkin-agent 1c01b7e6ca Allow k8s edge health checks on arrstack hosts (#532)
The Kubernetes-hosted haproxy edge probes the arr/nzbget backends on `/consul/health`, but its traffic arrives SNATed from the node ranges rather than the DMZ edge, so nginx returns 403 and every backend health-checks down.

- Allow `198.18.21.0/24` (2.5gbe physical), `198.18.15.0/24` and `198.18.19.0/24` (node loopbacks) on the `arrstack_web_healthcheck` location.
- Keep `198.18.24.0/24` so the existing DMZ edge stays healthy through cutover.

---------

Co-authored-by: unkin-agent <agent@unkin.net>
Reviewed-on: #532
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 09:55:58 +10:00
unkin-agent cb9f8870bf Trust the sshca host CA alongside the legacy signer (#530)
Catalog compilation moved to the k8s puppetserver compilers, which sign host certificates against the terraform-managed `sshca` mount. Clients only trust the legacy `ssh-host-signer` CA, so every re-signed node (ausyd1nxvm2120 already) presents a certificate nothing accepts, and knownhosts emits no plain host-key fallback.

- Add a second `@cert-authority *` entry for the `sshca` public key to `profiles::ssh::knownhosts::lines`.
- Keep the legacy entry untouched so legacy-signed hosts still verify.

Reviewed-on: #530
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-24 22:46:30 +10:00
unkin-agent 734fcb8cf4 Trust the estate CA when fetching ENC facts (#529)
Facter runs under Puppet's vendored Ruby, which reads its own bundled CA file and never the system trust store. The ENC fact now fetches over HTTPS, so every node fails certificate verification and falls back to its cached value.

- set ca_file on the request to the vaultca anchor bundle
- keep VERIFY_PEER on, and fall through to the existing cache path when the anchor is absent

Reviewed-on: #529
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 23:32:16 +10:00
unkin-agent f933660d3b Fetch agent ENC facts from encapi (#528)
The enc_role and enc_env facts still resolve against Cobbler on every agent, the last Cobbler dependency in the classification path now that the master-side ENC runs encapic-enc.

- Point the fact at https://encapi.k8s.syd1.au.unkin.net
- Rename the module and its messages from Cobbler to encapi

Cache file, TTL and fallback-to-cache failure behaviour are unchanged.

Reviewed-on: #528
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 23:08:20 +10:00
unkin-agent 3370aff38f Classify puppet masters through encapi (#527)
The VM puppet masters are the last part of the classification path still calling Cobbler; the k8s compilers already classify through encapi and the encapic RPM is installed on all six masters.

- Point `profiles::puppet::server::external_nodes` at `/usr/bin/encapic-enc` for `roles::infra::puppet::master`.
- Drop the stale comment about external_nodes still using cobbler-enc.

`profiles::puppet::cobbler_enc` stays in place so the revert is one hiera line.
Depends on the encapic 0.2.0 install (#525).

Reviewed-on: #527
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 22:42:43 +10:00
unkin-agent 1a907467e9 Shorten metadata_expire on internal RPM repos (#526)
A package pinned in hieradata right after its RPM lands in artifactapi is invisible to dnf until the host's 1h cached metadata expires, so the first Puppet run after a release cannot find the version.

- Set `metadata_expire` 60s on `rpm-internal`/`rpm-vendor` and their per-release variants for AlmaLinux and Fedora
- Leave upstream mirrors on the 1h default
- Drop the stale expiry note in `profiles::dns::updater`

Reviewed-on: #526
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 00:55:19 +10:00
unkin-agent cf25a20a92 Install encapic ENC client on puppet masters (#525)
The VM masters classify through the cobbler ENC while the k8s compilers
already use encapi. Install the client ahead of that cutover;
external_nodes still points at cobbler-enc, so classification is unchanged.

- pin the encapic package to 0.2.0 via profiles::packages::include
- add profiles::puppet::encapic managing /etc/encapic/encapic.conf from a
  hiera-driven ENCAPI_URL, ordered after Package['encapic'] so the config is
  written once the RPM that owns the path is installed
- include the class from profiles::puppet::puppetmaster

Requires encapic 0.2.0 in the rpm-internal repo.

Reviewed-on: #525
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 00:32:40 +10:00
unkin-agent 979c188c34 Bump openbao-plugin-secrets-arrstack to 0.2.0 (#522)
## Why

Plugin 0.2.0 adds per-role method scoping; the binary must land on the vault nodes before the catalog sha256 bump in terraform-vault.

## How

- Bump the `openbao-plugin-secrets-arrstack` pin in `hieradata/roles/infra/storage/vault.yaml` from `0.1.0` to `0.2.0`.

Reviewed-on: #522
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 15:28:47 +10:00
unkin-agent 7e1f2c336c Install openbao-plugin-secrets-arrstack v0.1.0 (#521)
## Why

The new `openbao-plugin-secrets-arrstack` OpenBao secrets engine mints dynamic per-service arrproxy machine tokens for the arrstack (Sonarr/Radarr/Prowlarr) fleet. Its v0.1.0 RPM is now published in artifactapi (rpm-internal), so the OpenBao VM nodes can install the plugin binary into `/opt/openbao-plugins`.

## Changes

- Add `openbao-plugin-secrets-arrstack` pinned to `0.1.0` to `profiles::packages::include` in `hieradata/roles/infra/storage/vault.yaml`, matching the existing pinned sibling plugins (litellm, gpg, rancher, gitea, apptoken, netbox, ghp).

This PR only installs the RPM binary on the nodes. Catalog registration is handled separately by terraform-vault (`config/plugins/*.yaml`).

## Reference

Plugin binary sha256 (used by the terraform-vault catalog registration, separate PR):

```
f8ee60ca7ba14819976acb7dc4cfb6799e3e8da8f871d0bb2bd18d1d9e537972
```

Reviewed-on: #521
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-18 23:21:44 +10:00
unkin-agent 2a329058c0 vault: install openbao-plugin-secrets-ghp on OpenBao nodes (#520)
Why: step 1 of adding the ghp Vault secrets engine — the plugin binary must be on the OpenBao nodes before terraform-vault registers it in the catalog (the catalog sha256 pins this exact build).

How: pin `openbao-plugin-secrets-ghp` ensure 0.1.0 in hieradata/roles/infra/storage/vault.yaml profiles::packages::include, alongside the other secrets plugins.

Ordering: this merges + puppet-applies first; then the config-write policy; then the terraform-vault engine resources (terraform-vault#121).
Reviewed-on: #520
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-16 14:47:58 +10:00
unkinben 7102ef2b34 vault: move openbao plugin sockets off /tmp onto /run (#509)
## Why

The litellm secrets engine on the OpenBao cluster died with `rpc Unavailable / dial unix /tmp/pluginNNN: no such file` (terraform-vault#112), fixed only by a manual `sys/plugins/reload/backend`.

Root cause (post-incident log audit): go-plugin puts each plugin's control socket under the process TMPDIR — `/tmp/pluginNNN` — and `vault.service` runs without PrivateTmp. The daily `systemd-tmpfiles-clean` reaps aged `/tmp` files; bao ran a single systemd invocation for 3+ weeks, so the socket long outlived the `/tmp` cleanup age and got deleted out from under the still-running plugin process (no panic/OOM/signal/exit in the bao journal — the process was healthy, just unreachable). The risk is shared by every OpenBao plugin (gpg, rancher, gitea, ...), not just litellm.

Supersedes the earlier tmpfiles-exclude approach (#508, closed) with the permanent fix: move the sockets off `/tmp` entirely.

## Change

- Add a `vault.service` drop-in (`systemd::manage_dropin`) that sets `Environment=TMPDIR=/run/vault-plugins` and `RuntimeDirectory=vault-plugins` (mode 0700).
- Point plugin sockets at `/run` (tmpfs, no age-based cleanup); `RuntimeDirectory` creates/owns the dir per service start.
- Notify a vault service restart (module handles daemon-reload) so the new TMPDIR takes effect and plugins respawn with sockets under `/run`.

## Heads-up

Puppet rolls a **bao restart per node** when this lands (the drop-in notifies `Service['vault']`). With auto-unseal (this cluster runs `profiles::vault::unseal`) it is a rolling non-event; if any node relies on manual unseal it will come back **sealed** and need unsealing. Merge consciously / stagger if needed.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #509
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 12:14:02 +10:00
unkinben d859daead4 vault: install netbox and apptoken OpenBao plugin RPMs on bao nodes (#519)
## Why

terraform-vault #115 registers `vault-plugin-secrets-netbox` in the OpenBao plugin catalog pinned to the released v0.1.0 binary's sha256. OpenBao only registers/mounts a plugin whose binary is physically present in the plugin_directory (`/opt/openbao-plugins`); the `openbao-plugin-secrets-netbox` RPM is published on rpm-internal but not yet installed on the bao nodes, so #115 fails to register until Puppet lays the binary down. The `openbao-plugin-secrets-apptoken` binary is likewise published but missing from the bao package list, deferred from the apptoken engine's earlier deploy follow-up.

## Changes

- Add `openbao-plugin-secrets-netbox` (pinned `0.1.0`) to the bao node `profiles::packages::include` list
- Add `openbao-plugin-secrets-apptoken` (pinned `0.1.0`) to the same list, alongside the existing gpg/litellm/rancher/gitea plugin RPMs
- Both install to `/opt/openbao-plugins`, matching the plugin_directory OpenBao reads and the sha256 registered in terraform-vault

## Ordering

This must apply (Puppet converge on the `roles::infra::storage::vault` nodes) BEFORE terraform-vault #115 is applied, otherwise #115's catalog registration fails on a missing binary.

Reviewed-on: #519
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 01:11:02 +10:00
unkinben 0fd01ac1a7 ceph: manage /etc/ceph/ceph.conf on osd and mon/mgr/mds hosts (#515)
## Why

`/etc/ceph/ceph.conf` on the prodnxsr ceph cluster (fsid `de96a98f`) is currently unmanaged and hand-maintained via the runbook (`profiles::ceph::client::manage_ceph_conf` is `false` on the k8s and incus roles). This brings it under Puppet, drives it from a single source of truth in hiera, and normalizes `public_network` to every ceph host's `/32`.

## How hosts are identified (osd-only vs mon/mgr/mds)

The authoritative, declarative signal is the role's `hiera_include` list (ENC `role` -> role hiera):

- **osd-only hosts** = `roles::infra::k8s::*` (prodnxsr0001-0008 and prodnxsr0014-0019): include `profiles::ceph::osd` + `::client`, NOT `::mds` -> `[global]` only.
- **mon/mgr/mds hosts** = `roles::infra::incus::node` (prodnxsr0009-0013): also include `profiles::ceph::mds` -> `[global]` + `[mds]` + `[mds.*]`.

The template keys on `profiles::ceph::client::render_mds_config`, set `true` only in the incus node role hiera (the single role that includes `profiles::ceph::mds`). The `is_ceph_mds` fact is a runtime fallback, not used here.

## Changes

- Add cluster topology as a single source of truth in `hieradata/common.yaml`: `cluster_public_ips` (all 19 ceph host `/32`s, 198.18.23.1-19), `mon_initial_members` (the 5 mons), and the `mds_instances` map (two mds daemons per mon/mgr/mds host).
- Rework `profiles/ceph/client.conf.erb` to render `[global]` always and the `[mds]`/`[mds.*]` sections only when `render_mds_config` is true, preserving the live two-space indent, exact key order, and blank-line separators between mds sections.
- Extend `profiles::ceph::client` with topology + tunable params (sample values as defaults) and gate `[mds]` on `render_mds_config`.
- Drop the hard `Package[ceph-common]` dependency when the class does not manage the package (cephadm / `profiles::packages` deliver it on the k8s and incus hosts; k8s hosts do not declare `ceph-common`, so an unconditional require would fail to compile).
- Enable `manage_ceph_conf: true` on `roles/infra/k8s.yaml`, `roles/infra/k8s/node.yaml`, and `roles/infra/incus/node.yaml`; add `render_mds_config: true` to the incus node role. prodnxsr0014-0019 run `roles::infra::k8s::compute` (confirmed live via PuppetDB; `is_ceph_osd` true, `is_ceph_mds` false), so they are already covered by `k8s.yaml` and get the `[global]`-only variant with no mds sections.

## Live-cluster safety review (please read before merge)

This touches a running storage cluster's config. The rendered content is byte-equivalent to the live file **except** for the one deliberate `public_network` normalization below.

- **Intended change:** `public_network` is normalized to all 19 ceph host `/32`s on every host (198.18.23.1-19). Adoption is now a **single `public_network`-line change on every cluster host**, no longer a no-op on the osd hosts:
  - osd hosts prodnxsr0001-0008 currently list `.1-.13` live -> first convergence adds `.14-.19`.
  - mon/mgr/mds hosts prodnxsr0009-0013 currently list only `.9-.13` live -> first convergence adds `.1-.8` and `.14-.19`.
  - prodnxsr0014-0019 (newly joined OSD hosts) get the file created with the full `[global]` variant.
- **Verified byte diff against the live files** (`/etc/ceph/ceph.conf` pulled from prodnxsr0008 and prodnxsr0009): each differs by **exactly one line** (the expanded `public_network`); on prodnxsr0009 the `[mds]` + `[mds.*]` sections and their blank-line separators are byte-identical to live. Rendering prodnxsr0009 with its current live `.9-.13` value reproduces the live file byte-for-byte, confirming the format match.
- RGW hosts (`roles::infra::ceph::rgw`, ausyd1nxvm2115-2119) include only `profiles::ceph::rgw` -> `profiles::ceph::conf`; they do not include `profiles::ceph::client`, so their variant is untouched and there is no duplicate `File[/etc/ceph/ceph.conf]`.

Rendered samples:

- osd-only (prodnxsr0008): `[global]` with all 19 `/32`s in `public_network`, nothing else.
- mon/mgr/mds (prodnxsr0009): the same `[global]` plus `[mds]` and `[mds.<host>-1|2]` for prodnxsr0009-0013, blank-line separated.

Reviewed-on: #515
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 00:00:20 +10:00
unkinben 62dff97c98 rke2: taint control-plane nodes NoSchedule (#517) 2026-08-08 23:19:12 +10:00
unkinben ff1a30823c fix: lock rke2-common and pin rke2 to 1.33.13~rke2r2 to unblock puppet (#516)
## Why

Every puppet run on k8s nodes (e.g. prodnxsr0002) fails on `Package[rke2-server]` and stops applying the rest of the catalog, so the nodes stop receiving all further package/config updates:

```
change from '1.33.4~rke2r1-1.el9' to '1.33.11~rke2r1' failed: Could not update:
dnf upgrade rke2-server-1.33.11~rke2r1 returned 1:
 Problem: problem with installed package rke2-common-1.33.13~rke2r2-0.el9.x86_64
  - package rke2-server-1.33.11~rke2r1 requires rke2-common = 1.33.11~rke2r1, but none of the providers can be installed
  - cannot install the best update candidate for package rke2-server
```

`rke2::install` versionlocks only `rke2-server`/`rke2-agent`, never their strict (`= version`) `rke2-common` dependency. `rke2-common` is served from the rolling `rancher-rke2-1.33-latest` channel, whose head is now `1.33.13~rke2r2`, so `rke2-common` drifted up to `1.33.13~rke2r2` while the pin (`#512`) sat at `1.33.11~rke2r1`. `dnf upgrade` cannot downgrade the newer `rke2-common` to satisfy the older server, so the transaction fails. This is the rolling-channel drift `#512` flagged as needing follow-up.

## How

- Versionlock `rke2-common` to the same `${rke2_version}~${rke2_release}` as the server/agent, so the rolling channel can no longer drift the dependency ahead of the pin.
- Bump `rke2_version` `1.33.11` -> `1.33.13` and `rke2_release` `rke2r1` -> `rke2r2` to match the current channel head and the already-drifted installed `rke2-common`, so the pinned server/agent, both versionlocks, and the preloaded airgap bundle resolve in one transaction.

Verified against the live artifactapi rke2 remote: `rke2-server-1.33.13~rke2r2-0.el9.x86_64.rpm` and the `v1.33.13+rke2r2` `rke2-images.linux-amd64.tar.zst` airgap bundle both serve HTTP 200.

Reviewed-on: #516
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 23:13:40 +10:00
unkinben 804ea06499 ceph: set OSD disk I/O scheduler to none via udev rule (#505)
## Why

Ceph OSDs manage their own I/O ordering, so the kernel scheduler on the backing disks just adds overhead. The original intent was to set those disks to the `noop` scheduler. The whole OSD fleet (k8s + incus nodes) runs AlmaLinux 9 on blk-mq kernels (5.14), where the equivalent of `noop` is `none`.

## Changes

- Add `profiles::ceph::osd_scheduler`, rendering a udev rule from the `ceph_osd_devices` fact (PR #504) that pins `queue/scheduler` to `none` on each OSD disk.
- Reload udev and trigger the matched block devices so the setting applies immediately; the udev rule keeps it set across reboots and device re-add.
- No-op when the fact is absent/empty, so VMs and non-OSD hosts are untouched.
- Include the class from `profiles::ceph::osd` so it lands only on OSD hosts.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #505
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 22:33:29 +10:00
unkinben 0fdc7c97d8 rke2,puppet: enforce versionlock before package install (#514)
## Why

A package versionlock is only effective if it is applied **before** the package resource is evaluated. When the package is upgraded/installed first and locked afterwards, dnf can pull an unpinned version and the lock then pins whatever landed. This is the exact drift that let new RKE2 nodes boot 1.33.11 instead of the pinned version, and it also matters because a global `dnf update -y` autoupdate timer (`profiles::yum::autoupdater`) relies on the versionlock plugin already having the lock written.

## Changes

- `rke2::install` orders the versionlock ahead of the package: adds `before => Package["rke2-${node_type}"]` so the lock exists before install/upgrade.
- `profiles::puppet::agent` (RedHat branch) adds `before => Package[$use_package]` on the versionlock for the same reason.
- Leaves `profiles::gitea::runner` and `profiles::vault::server` unchanged: both already order the versionlock ahead of the package (`before => Package` / `before => Class['vault']`).
- Changes ordering only; locked versions (including the RKE2 pin) are untouched. No dependency cycle is introduced (packages hold no back-reference to the versionlock).

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #514
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 20:00:42 +10:00