29 Commits

Author SHA1 Message Date
unkinben 5e8cff921c vault: keep systemd-tmpfiles from reaping openbao plugin sockets
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
go-plugin gives each secrets plugin a control socket at /tmp/pluginNNN.
The daily systemd-tmpfiles-clean deletes /tmp files older than the OS
policy, which severs the socket of a long-lived plugin while its process
keeps running. Every request then fails with rpc Unavailable / dial unix
/tmp/pluginNNN: no such file, until a plugin reload respawns it. This hit
the litellm engine (terraform-vault#112) but the risk is shared by every
OpenBao plugin (gpg, rancher, gitea, ...).

Ship a tmpfiles.d exclude (x /tmp/plugin*) on all vault servers so plugin
sockets survive the cleaner. Takes effect on the next clean, no restart.

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-06 22:31:58 +10:00
benvin 08472ff950 chore: update dns nameserver in dhcp (#507)
- set dhcp to provide 198.18.200.7 as dns in syd1

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #507
2026-08-06 22:23:51 +10:00
unkinben d5cfaed7d4 libs: add ceph_osd_devices fact listing ceph OSD block devices (#504)
A forthcoming profile needs to set the I/O scheduler to noop/none on the disks backing ceph OSDs, so Puppet must know which block devices those are. This fact surfaces them by inspecting LVM PVs whose volume group belongs to ceph.

- add ceph_osd_devices fact returning the sorted array of PV device paths whose VG name starts with ceph-
- confine the fact to physical Linux hosts with pvs installed so it never resolves on VMs

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #504
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-02 17:25:18 +10:00
unkinben dd38651a9f puppetserver: auto-restart JVM when its binaries are replaced by an upgrade (#499)
## Why

An out-of-band OpenJDK upgrade (java-17-openjdk 17.0.18 -> 17.0.19, delivered by the AlmaLinux repo migration in #496) removed the old versioned JAVA_HOME while the puppetserver JVMs kept running against the now-deleted files. The running JVM re-execs `jspawnhelper` from its original (deleted) JAVA_HOME on every posix_spawn, so `ProcessBuilder` fails with `error=2, No such file or directory`. That broke the exec ENC (`/opt/cobbler-enc/cobbler-enc`) and 500'd every catalog compile across all 6 masters, failing 136/143 nodes. The masters could not self-heal because nothing restarts the JVM after a java upgrade. Incident was resolved by manually restarting puppetserver on all 6 masters.

## Change

- Add `/usr/local/bin/puppetserver_jvm_guard.sh`: restarts puppetserver if a running JVM (`puppet-server-release.jar`) is executing from deleted binaries.
- Add a `puppetserver-jvm-guard` systemd timer (every 5 min, mirrors the existing generate-types timer pattern) that runs the guard, so any future JVM/library upgrade recovers automatically.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #499
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-30 20:50:30 +10:00
unkinben 72bcbacddb yum: source AlmaLinux RPM repos from artifactapi, drop legacy unkin repo (#496)
AlmaLinux hosts still pulled in-house RPMs from the legacy `unkin` repo hosted on Gitea (git.query.consul), and a second legacy Gitea repo `unkinben` was enabled fleet-wide. This moves AlmaLinux hosts fully onto the artifactapi local rpm repos so all hosts get the in-house/vendored set for their EL release, matching the artifactapi-is-the-real-registry model already used for baseos/epel/etc., and removes both legacy Gitea yum repos.

Depends on unkin/terraform-artifactapi#15 (creates the general `rpm-vendor` local repo); that should apply before this reaches hosts.

### RPM source migration
- retarget `rpm-vendor` to the distribution-agnostic `local/rpm-vendor` repo
- add `rpm-internal-release`/`rpm-vendor-release`, resolved from `os.release.major` so el8 hosts pull `-el8`, el9 hosts `-el9`, etc.
- remove the legacy `unkin` yumrepo definition (its content now comes from rpm-internal/rpm-vendor)

### unkinben removal
- remove the `unkinben` yumrepo definition from `AlmaLinux/all_releases.yaml`
- remove the `unkinben` `ensure: present` override from the jellyfin media role (its only consumer)
- `purge => true` on the yum profile then cleans `/etc/yum.repos.d/unkin.repo` from hosts

**Heads-up for reviewer:** the jellyfin role installs `jellyfin-web`, `jellyfin-server`, and `jellyfin-ffmpeg-bin` from unkinben. These packages exist **only** in unkinben, so once it is removed they will remain installed but **stop receiving updates** until they are republished into artifactapi (e.g. `rpm-internal-elN`). Jellyfin's other deps (`SDL2`, `ffmpeg`, `ffmpeg-devel`) come from rpmfusion and are unaffected. The `unkinben` *generic* archive downloads used by the *arr modules (sonarr/radarr/prowlarr/lidarr/readarr) and glauth are direct HTTP fetches, not the yum repo, and are untouched.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #496
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-28 21:42:58 +10:00
unkinben 965849594e os/Fedora: add Fedora 44 support for the base role (#497)
## Why

We want to manage Fedora 44 hosts with this Puppet codebase, starting with the `base` role only. Fedora reuses the RedHat osfamily code paths (dnf/yum, crypto-policies, firewalld, openvox), so this adds the Fedora-specific hieradata, repositories, and gates needed for `base` to compile and apply, mirroring how AlmaLinux is wired and consuming the artifactapi `rpm-internal-f<major>` / `rpm-vendor-f<major>` local repos. Everything is keyed off `facts.os.release.major` so a future Fedora release only needs its artifactapi local repos created, not new hieradata.

## How

- Add `hieradata/os/Fedora/all_releases.yaml`:
  - Define dnf repos via `profiles::yum::global::repos`: `fedora` and `updates` proxied through artifactapi's `fedora` remote, plus the artifactapi locals `rpm-internal-f%{major}` and `rpm-vendor-f%{major}`; GPG via the on-disk `fedora-gpg-keys`; metalink/mirrorlist cleared so only the artifactapi baseurl is used.
  - Set `crypto_policies::policy: DEFAULT`, `profiles::puppet::agent::openvox_enable: true`, and `lm-sensors:📦 lm_sensors`.
  - Fix up the base package set for Fedora: exclude `p7zip`/`dstat`/`iotop` (absent on Fedora) and include `7zip` and `iotop-c`, plus the same `crypto-policies-scripts`/`lzo`/`policycoreutils`/`unar`/`xz` additions AlmaLinux carries.
  - Wire `profiles::fedora::base` via `hiera_include`.
- Add `profiles::fedora::base` (ensures NetworkManager enabled) as the Fedora analogue of `profiles::almalinux::base`; deliberately minimal so it can grow into workstation/laptop use later.
- Make `profiles::puppet::agent` select the OpenVox distribution path (`fedora/<major>` on Fedora, `el/<major>` elsewhere); AlmaLinux/Debian behaviour is unchanged.

## Validation

`puppet-lint`, puppet manifest validate, and `yamllint` all pass via the repo's pre-commit hooks on the changed files.

## Note for reviewer

OpenVox does not yet publish a Fedora 44 build (`openvox7/fedora/` currently has only 36/40/41), so `openvox-agent` will 404 until upstream publishes f44 or a build is placed in `rpm-internal-f44`. The Puppet code produces the correct path for when that exists; installing the agent is a prerequisite for a Fedora 44 host to actually run.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #497
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-28 21:42:32 +10:00
unkinben 83f355cef4 vault: install openbao-plugin-secrets-gitea on the storage role (#498)
## Why

The new Gitea token secrets engine (`vault-plugin-secrets-gitea`, released v0.1.0) needs its plugin binary present on the OpenBao servers before terraform-vault can register it in the plugin catalog and mount it. This is the same install step already done for the rancher, gpg, and litellm secrets plugins.

## Change

- Add `openbao-plugin-secrets-gitea` to `profiles::packages::include` on the vault storage role, pinned to `0.1.0` so the on-disk binary stays in lockstep with the sha256 that terraform-vault pins in its plugin catalog entry (`config/plugins/vault-plugin-secrets-gitea.yaml`).

## Order

This is the first step. It must merge (and Puppet must run on the vault nodes to lay down the binary) before the terraform-vault engine PR is applied, or OpenBao cannot launch the plugin.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #498
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 19:07:30 +10:00
unkinben 82a5b2ee1a dns: stop exporting legacy DNS fragments (manage_export false) (#495)
## Why

During the k8s DNS cutover the `dns` module dual-writes every host's records two
ways: (a) LEGACY exported `@@concat::fragment`s collected onto the VM puppet DNS
master into `/var/named/sources/<zone>.conf`, and (b) NEW nsupdate/RFC2136 to the
k8s bind-authoritative cluster (`manage_nsupdate`). Both were gated on so the
legacy path stayed as a safety net.

The legacy path is no longer in any client's resolution path:

- **#490** repointed `profiles::dns::base::nameservers` from the VM resolver
  anycast `198.18.19.16` to the k8s **bind-resolvers** LB `198.18.200.7`. Every
  puppet client now resolves there.
- The k8s bind-resolvers forward `unkin.net`, `main.unkin.net`, and all
  `18.198.in-addr.arpa` reverse zones to the k8s **bind-authoritative** cluster
  (`198.18.200.6`), which is populated purely by the nsupdate path — the same
  `profiles::dns::record` defines, `manage_nsupdate` branch.
- The VM master's assembled `/var/named/sources` files are therefore orphaned:
  nothing resolves against them anymore.

This flips `profiles::dns::updater::manage_export: false` to stop nodes exporting
the legacy fragments. `manage_nsupdate` stays on.

## What changes

- `hieradata/common.yaml`: `profiles::dns::updater::manage_export` `true` -> `false`.

## Verification evidence

Pulled all **498** exported `Concat::Fragment` resources from PuppetDB (239 A,
232 PTR, 27 CNAME; 490 unique fqdn/type/value) and `dig`'d each against the k8s
authoritative read endpoint `198.18.200.7` (the dynamic path's serving side):

| Result | Count |
|---|---|
| MATCH | 447 |
| MISSING-from-dynamic | 38 |
| DIFFERENT-value | 5 |

Both non-match buckets are accounted for and are **not coverage gaps**:

- **38 MISSING** are all records the dynamic path deliberately filters via
  `profiles::dns::updater::deny_ranges`
  (`198.18.200.0/24`, `10.42.0.0/16`, ...) plus reverse zones the authoritative
  cluster isn't even authoritative for: `*-kube-lb0` (198.18.200.x),
  `*-flannel.1` (10.42.x.x pod CIDR), and `*-nomad`/`*-brcom1`
  (172.26.64.x / 10.255.255.x). These are exactly the k8s/LB noise the deny
  ranges exist to keep out of the authoritative zones. Verified every one falls
  in a denied/non-authoritative range.
- **5 DIFFERENT** are stale legacy duplicates that the dynamic path holds *more*
  correctly:
  - 4x `au-syd1-prod-halb.main.unkin.net` A: four ceph nodes (2069-2073) each
    self-register the shared name with their own primary IP (haproxy::dns
    exports `networking.ip` per host, a legacy collision). Dynamic resolves it to
    the single VIP `198.18.24.60`.
  - 1x `dashboard.ceph.unkin.net` CNAME: the ceph node self-publishes this to
    `lb1.unkin.net.` via its own nsupdate (per the k8s zones.yaml note); the
    exported VRRP-name fragment is stale.

Conclusion: the dynamic path fully covers the served record set, and the serving
path no longer depends on the exported files.

## Collector-behavior note (what the flip actually does)

`profiles::dns::zone` realizes the fragments with an exported-resource collector
(`Concat::Fragment <<| target == ... |>>`). Turning off export purges the
fragments from PuppetDB, so on the VM master concat rebuilds each
`/var/named/sources/<zone>.conf` down to just the header (SOA+NS+glue) as nodes
converge. That is **cosmetic**: no client resolves against the VM master anymore
(#490), and the k8s authoritative zones are unaffected (fed by nsupdate, not
these files).

## Rollback

Set `profiles::dns::updater::manage_export: true` again and let puppet converge;
nodes re-export their fragments and the VM-master zone files repopulate.

## Follow-up (after soak)

Deleting the `@@concat::fragment` / `Concat::Fragment <<||>>` collector code in
`profiles::dns::record` / `profiles::dns::zone` (and the VM `profiles::dns::master`
zone assembly) is a separate cleanup PR once this has soaked and the VM master is
confirmed decommissionable.

Reviewed-on: #495
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 12:29:21 +10:00
unkinben 36d8230ac0 Migrate canary ausyd1nxvm2120 to puppet-on-k8s (#494)
First live agent migration to the k8s puppetserver, using the single-boolean mechanism from #493 on the lowest-risk host (roles::base test host).

- Adds hieradata/nodes/ausyd1nxvm2120.main.unkin.net.yaml with profiles::puppet::migrate::enabled: true

On merge: the node's next run (from the old master) rewrites puppet.conf to the k8s server/ca_server with a fresh ssl-k8s ssldir; the run after enrols against the k8s CA (autosigned) and compiles from the k8s compilers (ENC=encapi). Verify: node appears in the k8s PuppetDB with a fresh report. Rollback: revert this file — legacy ssldir/cert remain intact.

Reviewed-on: #494
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 00:16:30 +10:00
unkinben aebe80e73f Add profiles::puppet::migrate: repoint VM agents onto puppet-on-k8s (#493)
## Why

Migrate the VM puppet agents off the legacy VM puppetmasters onto the new puppet-on-kubernetes servers, one wave at a time and reversible without re-enrolment.

Changing `server`/`ca_server` alone is insufficient: each agent's ssldir holds a client certificate signed by the **old** CA, which the new k8s CA neither trusts nor recognises. Migrated nodes switch to a **fresh ssldir**, so the agent generates a new key + CSR on its next run (autosigned immediately by the k8s CA, which runs `autosign = true`), pulls its catalog from the new compilers, and leaves the old CA-signed creds intact on disk for rollback.

The reconfiguration catalog is compiled by the **old** server (the node still points there when it applies the change). `profiles::puppet::client` rewrites `puppet.conf` with the new endpoints + ssldir and notifies `Service['puppet']`, so the very next agent run lands on k8s.

**Single-boolean design (reworked):** cutover is now driven by one hiera key. `profiles::puppet::migrate` ships wired into `profiles::base` with `enabled => false`, so every node evaluates it but nothing changes until the boolean is flipped. `profiles::puppet::client` includes migrate and folds the migrate endpoints into the effective values it renders. Flip `profiles::puppet::migrate::enabled: true` at any hiera layer (node/role/common) to migrate a target -- no other keys required.

## Changes

- `profiles::base` (`site/profiles/manifests/base.pp`): `include profiles::puppet::migrate` alongside the other puppet profiles (ships disabled).
- `profiles::puppet::migrate` (`site/profiles/manifests/puppet/migrate.pp`): unchanged params (`enabled=false`, `new_server`, `new_ca_server`, `new_ssldir`); when enabled and the node is not a puppetmaster it owns the fresh ssldir directory. Class-header runbook rewritten for the single-boolean flow. No longer opt-in via `hiera_include`.
- `profiles::puppet::client` (`site/profiles/manifests/puppet/client.pp`): `include profiles::puppet::migrate` and compute `$effective_server` / `$effective_ca_server` / `$effective_report_server` / `$effective_ssldir`, feeding those to the template. Keeps the existing optional `$ssldir` / `$report_server` params as manual overrides.
- `puppet.conf.erb`: render from the `@effective_*` vars.

## Precedence

- **ssldir / report_server** (`Optional`, default `undef`): explicit `profiles::puppet::client::ssldir` / `::report_server` in hiera wins; else the migrate-derived value when `enabled`; else the legacy default (ssldir omitted -> puppet default; report_server -> effective server).
- **server / ca_server** (`String`, no undef sentinel): the migrate endpoint wins while `enabled => true`; otherwise the client param (legacy default unless set explicitly). To pin a bespoke server, leave `enabled => false` for that target and set `profiles::puppet::client::server` directly.
- No include cycle: migrate.pp never references client.pp; its guard uses `$facts['enc_role']`.

## Runbook

- **Canary (one node)** -> `hieradata/nodes/<certname>.yaml`: `profiles::puppet::migrate::enabled: true`
- **Wave (a role)** -> `hieradata/roles/<t1>/<t2>[/<t3>].yaml`: `profiles::puppet::migrate::enabled: true`
- **Estate flip** -> `hieradata/common.yaml`: `profiles::puppet::migrate::enabled: true`
- **Rollback** -> remove the key (or set `false`) at that layer; next run rewrites puppet.conf back to the legacy server + legacy ssldir (original CA-signed cert intact).
- The k8s endpoints (`new_server` / `new_ca_server` / `new_ssldir`) are `profiles::puppet::migrate` param defaults; override them in hiera if the k8s names change.

## Verification

- Render comparison: unmigrated node renders a **byte-identical** `puppet.conf` to `develop` (diff empty). Migrated render (only `enabled: true`) emits `server`/`ca_server`/`report_server` = k8s endpoints and `ssldir = /etc/puppetlabs/puppet/ssl-k8s`.
- `puppet-lint` (repo args `--no-80chars-check --no-documentation-check --no-puppet_url_without_modules-check --fail-on-warnings`): clean on all 3 manifests.
- `puppet parser validate`: OK. ERB syntax: OK. Repo pre-commit hooks (erb-validate, puppet-lint, puppet-validate): all Passed.

Reviewed-on: #493
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 00:04:11 +10:00
unkinben a69318b62d Pin openbao secrets plugins to exact versions (#492)
## Why

`ensure: present`/`latest` lets the plugin binaries drift from the sha256 pinned in the terraform-vault catalog (`config/plugins/*.yaml`). On the next OpenBao restart, a drifted binary fails the sha check and the plugin won't launch — a latent footgun (hit exactly this with rancher on `ensure: latest`).

## Changes

Pin each secrets plugin to the version whose binary matches its registered catalog sha (all verified against the RPMs in rpm-internal):

- `openbao-plugin-secrets-litellm`: **0.1.1** (sha 2263ebcb…)
- `openbao-plugin-secrets-gpg`: **0.1.0** (sha 0e92d740…)
- `openbao-plugin-secrets-rancher`: **0.1.1** (sha 9e597cd9…; was `ensure: latest`)

All three are no-op on the binary (installed versions already match) — this just locks them so a future release can't silently upgrade the binary out of lockstep with the catalog.

`openbao-plugins` (base bundle) left unpinned — its version couldn't be verified from the tooling side and it tracks the openbao package, not a catalog sha.

## Note

To upgrade a plugin in future: bump the RPM version here **and** the catalog sha256 in terraform-vault in the same change, then `vault write sys/plugins/reload/backend plugin=<name>`.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #492
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-20 23:46:36 +10:00
unkinben 206a4521e5 Fix cobbler.main.unkin.net CNAME to resolve on k8s DNS (#491)
The cobbler master exports `cobbler.main.unkin.net` as a CNAME whose value is the bare hostname (`ausyd1nxvm2098`). The legacy puppet DNS master loaded this into a zone file under `$ORIGIN main.unkin.net.`, so BIND completed the bare label to `ausyd1nxvm2098.main.unkin.net.` and it resolved. `dns-updater` renders RFC2136 updates with no `$ORIGIN`, so the same bare label becomes root-absolute (`ausyd1nxvm2098.`) and dead-ends in NXDOMAIN — the record exists on the k8s authoritative servers (and via the resolvers at 198.18.200.7) but has no reachable address. This is the same failure already documented and fixed for haproxy in `profiles::haproxy::dns`.

Changes:
- Set the cobbler CNAME `value` to the fully-qualified `${facts.networking.fqdn}.` (trailing dot) so the target is origin-independent and resolves identically on both the legacy zone-file export and the dns-updater nsupdate path.
- Add an explanatory comment mirroring the one in `profiles::haproxy::dns` to prevent reintroducing a bare label.

Reviewed-on: #491
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-20 23:17:12 +10:00
benvin e8443ccfa1 feat: migrate clients to use bind-dns in kubernetes (#490)
- change the default anycast nameserver to bind-internal resolvers in
  kubernetes

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #490
2026-07-20 22:41:29 +10:00
unkinben 918c4f20e1 haproxy::dns: fully-qualify halb CNAME targets (#489)
## Why

The halb publishes service CNAMEs (`git.unkin.net`, `dashboard.ceph.unkin.net`, and every other halb-fronted name) with a **bare target label**, e.g.:
```
git.unkin.net.  300  IN  CNAME  au-syd1-prod-halb-vrrp.
```
`dns-updater` builds the RR with `dns.NewRR` and **no $ORIGIN**, so a bare value becomes root-absolute (`au-syd1-prod-halb-vrrp.`) and dead-ends in NXDOMAIN. Once the k8s bind became authoritative for these zones, that broke resolution of every halb-fronted service (git, the Ceph dashboard, ...). `au-syd1-prod-halb-vrrp.unkin.net` / `.main.unkin.net` resolve fine (198.18.19.17) — only the CNAME target was truncated.

## Change (`site/profiles/manifests/haproxy/dns.pp`)

Emit **fully-qualified** CNAME targets (trailing dot), the FQDN form `dns-updater` expects (per its own test fixture `au-syd1-prod-halb.main.unkin.net.`):
- vrrp cnames: `${location_environment}-halb-vrrp.${domain}.`
- non-vrrp cnames: `${location_environment}-halb.${domain}.`

The matching A records are already published in `main.unkin.net`/`unkin.net` just above, so the targets resolve to the VIP.

---------

Co-authored-by: benvin <neotheo@gmail.com>
Reviewed-on: #489
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-19 01:37:06 +10:00
unkinben 732c938c5a ceph/rgw: add s3.ceph.unkin.net to radosgw cert SAN + nginx aliases (#487)
## Why

`s3.ceph.unkin.net` is the RGW S3 endpoint name used by cephrgw-operator consumers. A companion argocd-apps PR adds it as a CNAME to `radosgw.service.consul`. The radosgw hosts front RGW with nginx (443 -> radosgw:7480) using the vault-issued cert, so **both** the certificate and nginx must recognise the name.

## Changes (hieradata/roles/infra/ceph/rgw.yaml)

- Add `s3.ceph.unkin.net` to `profiles::pki::vault::alt_names` — puts the name in the radosgw cert SAN (renewal re-triggers on the alt-names change).
- Add `s3.ceph.unkin.net` to `profiles::nginx::simpleproxy::nginx_aliases` — nginx answers for that server_name.

### nginx: yes, needed

radosgw sits behind `profiles::nginx::simpleproxy` (listens 443, proxies to :7480) sharing the same vault cert, so the alias is required for nginx to serve the hostname — not just the cert SAN.

---------

Co-authored-by: benvin <neotheo@gmail.com>
Reviewed-on: #487
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 17:01:41 +10:00
unkinben 4230370418 Upgrade openbao-plugin-secrets-rancher to latest (v0.1.1) (#488)
## Why

The vault nodes have rancher plugin v0.1.0, which mints non-functional tokens (returns `status.value` not `status.bearerToken`; every cred/rotation 401s). v0.1.1 fixes it. `ensure_packages` defaults to `present`, which won't upgrade an already-installed package, so pin to `latest` to pull v0.1.1.

## Changes

- `openbao-plugin-secrets-rancher: { ensure: latest }` on the vault storage role.

## Note

`latest` will also pull future releases automatically — keep the terraform-vault catalog sha256 (`config/plugins/vault-plugin-secrets-rancher.yaml`) bumped in lockstep, or a vault restart after a future upgrade will fail the sha check. Happy to pin to an exact version instead if you'd prefer lockstep-by-pin.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #488
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 17:01:14 +10:00
unkinben 8f49c35c16 yum: point rpmfusion repos at artifactapi (#486)
Repoints the `rpmfusion-free` and `rpmfusion-nonfree` yum repos from the retired `packagerepo.service.consul` mirror to the artifactapi RPM Fusion remotes.

dnf on media nodes (jellyfin, nzbget) failed with `Cannot download repomd.xml: All mirrors were tried`, which blocked package installs such as `dns-updater`.

Changes:
- `rpmfusion-free` baseurl → `.../api/v1/remote/rpmfusion-free/el/updates/$major/$arch/`, gpgkey → `.../rpmfusion-free/el/RPM-GPG-KEY-rpmfusion-free-el-$major`
- `rpmfusion-nonfree` baseurl → `.../api/v1/remote/rpmfusion-nonfree/el/updates/$major/$arch/`, gpgkey → `.../rpmfusion-nonfree/el/RPM-GPG-KEY-rpmfusion-nonfree-el-$major`

Depends on terraform-artifactapi PR #7 (adds the rpmfusion remotes) being applied first.

https://claude.ai/code/session_01VZ81u2JtjvaPq8Z3EDTSMb
Reviewed-on: #486
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 14:30:25 +10:00
unkinben 9aa470d329 dns: exclude k8s/LB ranges from dns-updater; track latest (#485)
Configures the dns-updater record filter (dns-updater#3) so k8s/LB/internal addresses stay out of the authoritative zones.

## Why
Hosts publish records the authoritative server should not hold — `*-flannel.1 A 10.42.x`, `*-kube-lb0 A 198.18.200.x`, and the matching reverse PTRs (`200.18.198`, `2.42.10`) — which the daemon WARNs `NOTAUTH`/`bad authentication` on.

## Changes
- `DNS_UPDATER_DENY_RANGES=198.18.199.0/24,198.18.200.0/24,10.42.0.0/16,10.43.0.0/16` (pod CIDR, service CIDR, LB VIP pool, .199).
- New class params: `deny_ranges` / `allow_ranges` / `deny_domains` / `allow_domains`.
- `package_ensure` → `latest` so hosts pick up the filter release (rpm-internal `metadata_expire` is 1h, so no thrash).

## Sequencing
Needs dns-updater **#3** merged + a new tag (v0.2.0) so the filter env vars are honored. The env keys are ignored by v0.1.0, so this is safe to merge first — it takes effect when hosts upgrade to v0.2.0.

Reviewed-on: #485
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 13:58:43 +10:00
unkinben 6f70594484 Add rpm-vendor yum repository (#484)
Why: rpmbuilder's vendored third-party RPMs are moving from Gitea's RPM registry to the artifactapi rpm-vendor local repos. Managed AlmaLinux hosts need the matching yum repo to install them; this mirrors the existing rpm-internal repo and selects the per-EL-release repo via os.release.major.

Changes:
- Add rpm-vendor to profiles::yum::global::repos in hieradata/os/AlmaLinux/all_releases.yaml, pointing at .../api/v1/local/rpm-vendor-el<major>/ (el8/el9), gpgcheck disabled and mirrorlist absent, matching the rpm-internal repo definition.

Depends on terraform-artifactapi PR #6 (creates the rpm-vendor-el8/el9 repos).

Reviewed-on: #484
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 11:28:01 +10:00
unkinben 9891311d52 Install openbao-plugin-secrets-rancher on vault nodes (#483)
## Why

The Rancher token secrets engine needs its plugin binary present in the OpenBao plugin directory before terraform-vault can register/mount it. Mirrors #480 (gpg).

## Changes

- Add `openbao-plugin-secrets-rancher` to `profiles::packages::include` on the vault storage role. Installs the RPM (from rpm-internal) to `/opt/openbao-plugins/vault-plugin-secrets-rancher`.

## Merge order

Part 3 of 4 — merge first (binary must exist on disk before the terraform-vault plugin-import #92 registers it with a matching sha256).

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #483
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 11:20:02 +10:00
unkinben d71e221049 dns: deploy dns-updater daemon in place of the shell nsupdate script [HOLD: needs dns-updater RPM published] (#482)
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/erb-validate 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 canceled
ci/woodpecker/pr/ruby-check Pipeline was canceled
## Why
Replace the `profiles::dns::updater` shell mechanism (`dns-update.sh` + `dns-update.path`/`.service` + the in-run `exec`) with the packaged **dns-updater** daemon. The daemon watches the records file (inotify) and network interfaces and pushes TSIG-signed RFC2136 updates to BIND natively — with structured per-zone RCODEs and a status API/facter fact, so failures like the recent `invalid owner name: empty label` / NOTZONE surface directly instead of as opaque nsupdate stderr.

## Changes
- Install the `dns-updater` package; manage `/etc/dns-updater/env`.
- Run the packaged `dns-updater.service`, restarting **only** on env/key change — records-file edits are picked up by the daemon`s own inotify watch, so no service churn on record changes.
- Keep the `concat` records file and the TSIG key file unchanged (same paths/format).
- Ensure the old `/usr/local/bin/dns-update` + `dns-update.path`/`.service` units are absent.
- Drop the now-dead `dns-update.{sh,service,path}.epp` templates.

## Sequencing — HOLD
Do not merge until the `dns-updater` RPM is published to artifactapi `rpm-internal` (needs terraform-git#33 to create the repo, then the daemon code pushed + tagged so Woodpecker builds the RPM). Merging before the package exists makes `package { dns-updater }` fail on every host.

Supersedes the interim shell fix in #481 (which stays valid until this rolls out). Keeps the file as the desired-state interface (puppet owns desired records; the daemon reconciles + reports).

Reviewed-on: #482
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 23:35:00 +10:00
unkinben 0de3ac2a0b dns: fix dns-update fqdn() double-appending zone to FQDN records (#481)
## Why
6 hosts (ausyd1nxvm2069-2073, 2098) ended up with a reverse PTR in bind-authoritative but **no forward A record**, and the `unkin.net` service records (git/grafana/auth/fafflix, all published by the halb host 2069) never landed at all.

VictoriaLogs (`dns-update-apply` on 2069 & 2070) shows the cause:
```
dns-update: nsupdate to 198.18.200.9 failed
invalid owner name: empty label
syntax error
```
`profiles::dns::record` publishes some records whose name is already fully-qualified (trailing dot) — e.g. `au-syd1-pve.main.unkin.net.`, `cobbler.main.unkin.net.`, `dashboard.ceph.unkin.net.`, and the halb CNAMEs. The `dns-update` script `fqdn()` unconditionally appended the zone, producing `…net..main.unkin.net.` — the `..` is an empty label, which nsupdate rejects, failing the entire per-zone `send`. The reverse-PTR send is sorted first and its name is always relative, so it still applied — hence "PTR but no A".

## Change
`fqdn()` now handles three cases:
- `@`/empty → zone apex (unchanged)
- name ending in `.` → already FQDN, used verbatim (**the fix**)
- otherwise → relative, append `.zone.` (unchanged)

Verified against all record shapes (relative host, apex, FQDN CNAME, reverse label) — no more `..`.

## After merge
Once puppet re-runs on the affected hosts their `main.unkin.net`/`unkin.net` updates succeed, filling in the missing A records and the `unkin.net` service zone. Pairs with argocd-apps#260 (adds the `ceph.unkin.net` zone so `dashboard.ceph.unkin.net` does not then hit NOTZONE).

Reviewed-on: #481
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 22:50:28 +10:00
unkinben 0702676da6 Install openbao-plugin-secrets-gpg on vault nodes (#480)
Deploy the GPG/OpenPGP secrets engine to the OpenBao (vault-role) cluster by installing its plugin RPM into `/opt/openbao-plugins`, mirroring the existing `openbao-plugin-secrets-litellm` deployment (#479).

- Add `openbao-plugin-secrets-gpg` to `profiles::packages::include` in the vault role hiera. The RPM ships from artifactapi `rpm-internal` (built on the [vault-plugin-secrets-gpg](https://git.unkin.net/unkin/vault-plugin-secrets-gpg) v0.1.0 tag) and lands the plugin binary in the node's configured `plugin_directory`.

Registering + enabling the secrets backend (`plugin register` / `secrets enable`) is a follow-up terraform-vault change, matching how litellm is wired.

Reviewed-on: #480
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-16 22:48:08 +10:00
unkinben 88fcb97ad1 dns: nsupdate host records to the authoritative server (#475)
Replaces the exported-resources → puppet DNS master zone-file flow with per-host RFC2136 dynamic updates against the k8s **bind-authoritative** write endpoint (198.18.200.9). The master no longer manages zone files.

## Design
Each node assembles its DNS records into a local concat file; a systemd `.path` unit watches it and runs `dns-update` (nsupdate) on change — exactly the watch-a-file model requested.

## Changes
- **profiles::dns::updater** (new): concat records file + TSIG key file + `dns-update` script + `dns-update.service` (oneshot) + `dns-update.path` (watcher). The script sends only the delta since last run and deletes removed records, grouped per zone.
- **profiles::dns::record**: writes a local concat fragment (`zone|name|type|ttl|value`) instead of exporting `@@concat::fragment` to the master.
- **profiles::dns::base**: includes `profiles::dns::updater` (all nodes).
- **hiera**: `profiles::dns::updater` server/key_name/algorithm in common.yaml.

## Inert until keyed
The updater does nothing until `profiles::dns::updater::key_secret` (TSIG) is set in eyaml — records are assembled but not applied, so nodes are safe before the key exists.

## Prerequisites (k8s side, separate)
1. The `bind-authoritative` zones must set `dynamicUpdate: true` + an `updateKeyRef` (a client-update BindTSIGKey) so they accept these updates.
2. The TSIG key must be shared: the operator-generated key value goes into eyaml here (or the planned Vault-sync feature bridges it).

## Validated
puppet parser/epp validate, puppet-lint, and a functional test of the generated per-zone nsupdate message (replace + delete-removed).

Reviewed-on: #475
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-12 22:23:18 +10:00
benvin 69781df412 chore: change to openbao-plugin-secrets-litellm (#479)
change to openbao litellm plugin as it installs to the correct directory

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #479
2026-07-06 23:26:21 +10:00
benvin 2ae215daf7 feat: install vault-plugin-secrets-litellm (#477)
ensure the litellm plugin for vault is installed

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #477
2026-07-06 23:02:12 +10:00
benvin cd1c3e6ae2 chore: add rpm-internal repo (#476)
enable installing locally packaged rpms with no specific linux release

- add the rpm-internal repo from artifactapi

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #476
2026-07-06 23:02:02 +10:00
benvin 00a8088aed chore: reduce requsts for all jobs (#478)
- found jobs stalling

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #478
2026-07-06 22:22:51 +10:00
unkinben aeae26711f Convert RKE2 registries to template, disable default endpoints (#474)
## Summary
- Replace static `registries.yaml` with EPP template driven by `rke2::registries` hash
- Add `disable-default-registry-endpoint: true` to all mirrors — RKE2 will only use artifactapi and never fall back to upstream registries
- Registry configuration now fully managed via hiera data (`roles/infra/k8s.yaml`)

Reviewed-on: #474
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-29 22:30:48 +10:00
39 changed files with 808 additions and 94 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 2
cpu: 1
limits:
memory: 2Gi
cpu: 2
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1 -1
View File
@@ -12,7 +12,7 @@ steps:
resources:
requests:
memory: 512Mi
cpu: 1
cpu: 250m
limits:
memory: 2Gi
cpu: 1
+1
View File
@@ -6,3 +6,4 @@ profiles::consul::token::node_editor::secret_id: ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDo
profiles::consul::server::acl_tokens_initial_management: ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAi1UH7AZirJ1PdxWy+KEgS5ufm0wbn2xy9rkg14hKYpcVjBa4pOZpSLMGMiiUpBIqBytDMZM4ezYa/luktpkBImJbM/TE16beGtsacQGA+9eZk2Tihs9GR2qbAQiu5lLITiDlwNnf0GeWdqHM8CTeD68DczQF320d9U14/k6pG/7z+w/MGLcjsQoSuOFTm42JVn1BI46t1CYSCHMXQc/9Tfs+FzI+vumohI8DxAYBIuyzU5HBX/MntAsvD/yixMJS1pZL9WwgqZJC/wK34rVRB39DpxWf/WROrI+WLuSJwr7WBjaeF9Ju+89WKCgsI53EWhFTj8GgDZm/jqPoE478NjBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAoACRzJdQKNYXZv6cghFIIgDAzB81DMcuY815nb8POtZpiA06jT/068AoZmSctHoFK/zW9tY229N5r1Tb+WHElqLk=]
profiles::consul::server::acl_tokens_default: ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAh4Ag95xgkIZHL0gP9OLnZauih0dB1/2l9Jzw8mP3OiIv7fw23otHYONlS3Emtj7oxW8MKcZGKDCzwCT6T2p+V5wx1n15wr2J+FmL24VbclJwrMPQ4AdgP359B9h21uoyo7Zdy7RuuvLfkU1fWXbs3SeWbi2HJs1Ed1/oI1jzr3OgwMbVtbyzd1VuAXeZ9bHQG3IA8z+w/k5m61th0HTyHjw7eldQulbohDuwv545z9axHEoHKCRT2a3ZwBufV2ST6Dm3g9GERzXE9Adp9DQC5adqM74wfsujOMLK2QFJSSIOj2uCs1CpEnrNrQ8zjP3fudM2z3l7KdSHZazEamCSxTBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBY/Tn9tzEKYc5dxnzP2rP7gDBWKgVP3lf2T4Q0WPQt3ns0E6RUSO6OtBegb/5qDyohY2nsDeJTnMKOYzYt/J1PhnY=]
profiles::consul::server::acl_tokens_replication: ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAEzTIxbaAR/TZYnC671aBhiahsfwf3ieyeSHpD5hQm40sMEF/fXlEDijq9i2ykPikm94074j1Uo4HNzv/V9GTf0NSC/64t61jJ/Ya3QKa5f/36+DcRj3lcETsSIyhWwmU+E+zkY8I2r68MtXAuvSoMSMZdpgWSkPx/3FFgZlrsg//bzDu69jS9cx4UK582N3A6QN4Uy/qwYtJcm+2iTQlqqgGRWGqnSgTirxhegxPbJGWTDoAEpAL4/DyF5/hqcUn6mgoSfAsHF3loPHOqN30lG+9o0THWJ9B8Gf4W/1X2UWA/avmUnqBnumGoz0p7AYdNgpW+qLl2rk4lyGYa4kvQjBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBD9KSLH3Pn/1EIzgVJM+8VOgDDpKWzHfSVsfUMyTD0XIRPGclTdmxqCnsdhKNqmUfSjkYIf2nI9rQtSK6n42TYuO4k=]
profiles::dns::updater::key_secret: ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAJ4GrN3W4zGfVygfji1MeGyAlmlFN45TVP5st6b66jSKeDYNg9BwdnXahUzkmKvFIYdPWyvXqJdvuyUNz7lWr1dw/pTNHrvqUKSvI0C5TOwmtrTwXtTvucDGs7c7FZDfgUxvLs/Vrp0ZdW3C81QXWNBI8vBfVZyiegPNq+5mX7mtcrFE7jZj1g1qwpg/30lrlCguPyXqnmdtBa7VAyU2Al1UZwkrKIYSAOb9GTnvM6bk5L26pW/NXGj+ffacwf58b4AiKCak5lhoCIInVnlogPuThxULN4RF+MYAVIlgwk159CR1abjWjioxvmP7XgjQUwUuTlgoLh9fDZa0amDeFZTBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBEb1BTjhuTN9wGrxc2rdQVgDCctNdt72UR/Hj4wteK7XIX6fF43+7FYKBZnPtyWIWalwDniH//H3gfsJ+ULqSb82w=]
+23 -1
View File
@@ -146,6 +146,8 @@ lookup_options:
strategy: deep
profiles::etcd::node::initial_cluster_token:
convert_to: Sensitive
profiles::dns::updater::key_secret:
convert_to: Sensitive
sysctl::base::values:
merge:
strategy: deep
@@ -206,8 +208,28 @@ vault::manage_config_dir: true
vault::disable_mlock: false
profiles::dns::base::nameservers:
- 198.18.19.16
- 198.18.200.7
profiles::dns::master::basedir: '/var/named/sources'
# dns record publishing. During the k8s cutover both methods run; set
# manage_export false once k8s is authoritative.
# - export: legacy exported-resources -> puppet DNS master
# - nsupdate: RFC2136 to the k8s bind-authoritative write endpoint (.9),
# inert until the TSIG key is set in eyaml:
# profiles::dns::updater::key_secret: ENC[...]
# (must match the key the bind-authoritative zones allow-update
# with; algorithm hmac-sha256)
# k8s is now authoritative: clients resolve via the k8s bind-resolvers
# (profiles::dns::base::nameservers 198.18.200.7, since #490), which forward the
# unkin.net/main.unkin.net/*.18.198.in-addr.arpa zones to the k8s
# bind-authoritative cluster fed by the nsupdate path below. The legacy
# exported-resources -> VM-master /var/named/sources files are no longer in any
# client's resolution path, so stop exporting them. Rollback: set back to true.
profiles::dns::updater::manage_export: false
profiles::dns::updater::manage_nsupdate: true
profiles::dns::updater::server: '198.18.200.9'
profiles::dns::updater::key_name: 'client-update'
profiles::dns::updater::key_algorithm: 'hmac-sha256'
#profiles::dns::base::ns_role: 'roles::infra::dns::resolver'
#profiles::dns::base::use_ns: 'region'
profiles::consul::server::members_role: roles::infra::storage::consul
+1 -1
View File
@@ -2,7 +2,7 @@
timezone::timezone: 'Australia/Sydney'
certbot::client::webserver: ausyd1nxvm2057.main.unkin.net
profiles_dns_upstream_forwarder_unkin:
- 198.18.19.15
- 198.18.200.7
profiles_dns_upstream_forwarder_consul:
- 198.18.19.14
profiles_dns_upstream_forwarder_k8s:
@@ -0,0 +1,3 @@
---
# Canary for the puppet-on-k8s migration (roles::base test host).
profiles::puppet::migrate::enabled: true
+32 -18
View File
@@ -70,12 +70,34 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/epel/%{facts.os.release.major}/Everything/%{facts.os.architecture}
gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/epel/RPM-GPG-KEY-EPEL-%{facts.os.release.major}
mirrorlist: absent
unkinben:
name: unkinben
descr: unkinben repository
target: /etc/yum.repos.d/unkin.repo
baseurl: https://git.query.consul/api/packages/unkinben/rpm/el%{facts.os.release.major}
gpgkey: https://git.query.consul/api/packages/unkinben/rpm/repository.key
rpm-internal:
name: rpm-internal
descr: rpm-internal repository
target: /etc/yum.repos.d/rpm-internal.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal/
gpgcheck: false
mirrorlist: absent
rpm-vendor:
name: rpm-vendor
descr: rpm-vendor repository
target: /etc/yum.repos.d/rpm-vendor.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor/
gpgcheck: false
mirrorlist: absent
# Per-release variants, resolved from the host's EL major version so el8
# hosts pull rpm-internal-el8/rpm-vendor-el8, el9 hosts el9, etc.
rpm-internal-release:
name: rpm-internal-el%{facts.os.release.major}
descr: rpm-internal-el%{facts.os.release.major} repository
target: /etc/yum.repos.d/rpm-internal-release.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
rpm-vendor-release:
name: rpm-vendor-el%{facts.os.release.major}
descr: rpm-vendor-el%{facts.os.release.major} repository
target: /etc/yum.repos.d/rpm-vendor-release.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
@@ -247,8 +269,8 @@ profiles::yum::global::repos:
name: rpmfusion-free
descr: rpmfusion-free repository
target: /etc/yum.repos.d/rpmfusion-free.repo
baseurl: https://packagerepo.service.consul/rpmfusion-free-el%{facts.os.release.major}-%{facts.os.architecture}/
gpgkey: https://packagerepo.service.consul/rpmfusion-free-el%{facts.os.release.major}-%{facts.os.architecture}/repodata/repomd.xml.key
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rpmfusion-free/el/updates/%{facts.os.release.major}/%{facts.os.architecture}/
gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rpmfusion-free/el/RPM-GPG-KEY-rpmfusion-free-el-%{facts.os.release.major}
gpgcheck: 1
mirrorlist: absent
rpmfusion-nonfree:
@@ -256,15 +278,7 @@ profiles::yum::global::repos:
name: rpmfusion-nonfree
descr: rpmfusion-nonfree repository
target: /etc/yum.repos.d/rpmfusion-nonfree.repo
baseurl: https://packagerepo.service.consul/rpmfusion-nonfree-el%{facts.os.release.major}-%{facts.os.architecture}/
gpgkey: https://packagerepo.service.consul/rpmfusion-nonfree-el%{facts.os.release.major}-%{facts.os.architecture}/repodata/repomd.xml.key
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rpmfusion-nonfree/el/updates/%{facts.os.release.major}/%{facts.os.architecture}/
gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rpmfusion-nonfree/el/RPM-GPG-KEY-rpmfusion-nonfree-el-%{facts.os.release.major}
gpgcheck: 1
mirrorlist: absent
unkin:
name: unkin
descr: unkin repository
target: /etc/yum.repos.d/unkin.repo
baseurl: https://git.query.consul/api/packages/unkin/rpm/almalinux/el%{facts.os.release.major}
gpgkey: https://git.query.consul/api/packages/unkin/rpm/repository.key
gpgcheck: false
mirrorlist: absent
+69
View File
@@ -0,0 +1,69 @@
# hieradata/os/Fedora/all_releases.yaml
---
# Fedora reuses the RedHat osfamily code paths (dnf/yum, crypto-policies,
# firewalld, openvox). Everything here keys off facts.os.release.major so a
# future Fedora release only needs its artifactapi local repos created, not new
# hieradata.
# crypto-policies: use the distro default. Kept here (not per-release) so newer
# Fedora releases inherit it for free.
crypto_policies::policy: 'DEFAULT'
# Puppet agent via OpenVox. RedHat-family builds for Fedora live under
# openvox7/fedora/<major> (see profiles::puppet::agent).
profiles::puppet::agent::openvox_enable: true
lm-sensors::package: lm_sensors
# Fedora-specific base setup (NetworkManager, future workstation/laptop hooks).
hiera_include:
- profiles::fedora::base
# Base package set adjustments for Fedora:
# - p7zip was dropped from Fedora; the real 7-Zip ships as "7zip"
# - dstat was removed from Fedora with no drop-in successor in the base set
# - iotop is provided by the C rewrite package "iotop-c"
profiles::packages::exclude:
- p7zip
- dstat
- iotop
profiles::packages::include:
7zip: {}
iotop-c: {}
crypto-policies-scripts: {}
lzo: {}
policycoreutils: {}
unar: {}
xz: {}
profiles::yum::global::repos:
fedora:
name: fedora
descr: Fedora %{facts.os.release.major} - %{facts.os.architecture}
target: /etc/yum.repos.d/fedora.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/fedora/releases/%{facts.os.release.major}/Everything/%{facts.os.architecture}/os/
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-%{facts.os.release.major}-%{facts.os.architecture}
metalink: absent
mirrorlist: absent
updates:
name: updates
descr: Fedora %{facts.os.release.major} - %{facts.os.architecture} - Updates
target: /etc/yum.repos.d/fedora-updates.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/fedora/updates/%{facts.os.release.major}/Everything/%{facts.os.architecture}/
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-%{facts.os.release.major}-%{facts.os.architecture}
metalink: absent
mirrorlist: absent
rpm-internal:
name: rpm-internal-f%{facts.os.release.major}
descr: rpm-internal-f%{facts.os.release.major} repository
target: /etc/yum.repos.d/rpm-internal.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
rpm-vendor:
name: rpm-vendor-f%{facts.os.release.major}
descr: rpm-vendor-f%{facts.os.release.major} repository
target: /etc/yum.repos.d/rpm-vendor.repo
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
-2
View File
@@ -57,5 +57,3 @@ profiles::yum::global::repos:
ensure: present
rpmfusion-nonfree:
ensure: present
unkinben:
ensure: present
+5
View File
@@ -14,6 +14,9 @@ profiles::pki::vault::alt_names:
- radosgw.service.consul
- radosgw.query.consul
- "radosgw.service.%{facts.country}-%{facts.region}.consul"
# S3 endpoint name (s3.ceph.unkin.net CNAME -> radosgw.service.consul); needed
# on the cert so TLS validates for that hostname.
- s3.ceph.unkin.net
# additional repos
profiles::yum::global::repos:
@@ -26,6 +29,8 @@ profiles::yum::global::repos:
profiles::nginx::simpleproxy::nginx_vhost: 'radosgw.service.consul'
profiles::nginx::simpleproxy::nginx_aliases:
- radosgw.service.au-syd1.consul
# nginx (443 -> radosgw:7480) must answer for the S3 endpoint name too.
- s3.ceph.unkin.net
profiles::nginx::simpleproxy::proxy_port: 7480
profiles::nginx::simpleproxy::proxy_path: '/'
nginx::client_max_body_size: 5000M
+55
View File
@@ -12,6 +12,61 @@ hiera_include:
rke2::bootstrap_node: prodnxsr0001.main.unkin.net
rke2::join_url: https://join-k8s.service.consul:9345
rke2::manage_registries: true
rke2::registries:
docker.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "dockerhub/$1"
disable-default-registry-endpoint: true
ghcr.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "ghcr/$1"
disable-default-registry-endpoint: true
quay.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "quay/$1"
disable-default-registry-endpoint: true
registry.k8s.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "k8s-registry/$1"
disable-default-registry-endpoint: true
registry.gitlab.com:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "gitlab/$1"
disable-default-registry-endpoint: true
docker.elastic.co:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "elastic/$1"
disable-default-registry-endpoint: true
gcr.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "gcr/$1"
disable-default-registry-endpoint: true
docker.litellm.ai:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "litellm/$1"
disable-default-registry-endpoint: true
public.ecr.aws:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "ecr-public/$1"
disable-default-registry-endpoint: true
rke2::config_hash:
bind-address: "%{hiera('networking_loopback0_ip')}"
node-ip: "%{hiera('networking_loopback0_ip')}"
+12
View File
@@ -27,4 +27,16 @@ profiles::nginx::simpleproxy::proxy_port: 8200
profiles::nginx::simpleproxy::proxy_path: '/'
profiles::packages::include:
# openbao-plugins (base bundle) left unpinned; it tracks the openbao package.
openbao-plugins: {}
# Secrets plugins pinned to the exact version whose binary matches the sha256
# registered in terraform-vault (config/plugins/*.yaml). Bump both in lockstep
# on upgrade, or OpenBao refuses to launch the plugin after a restart.
openbao-plugin-secrets-litellm:
ensure: '0.1.1'
openbao-plugin-secrets-gpg:
ensure: '0.1.0'
openbao-plugin-secrets-rancher:
ensure: '0.1.1'
openbao-plugin-secrets-gitea:
ensure: '0.1.0'
@@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'facter'
Facter.add(:ceph_osd_devices) do
confine kernel: 'Linux'
confine { Facter.value(:is_virtual) == false }
confine { Facter::Core::Execution.which('pvs') }
setcode do
devices = []
output = Facter::Core::Execution.execute(
'pvs --noheadings -o pv_name,vg_name 2>/dev/null',
on_fail: nil
)
output&.each_line do |line|
pv_name, vg_name = line.split
next unless pv_name && vg_name
devices << pv_name if vg_name.start_with?('ceph-')
end
devices.sort
end
end
+88
View File
@@ -0,0 +1,88 @@
# frozen_string_literal: true
# lib/facter/dns_records.rb
#
# Reports this host's expected DNS records (assembled by profiles::dns::updater
# into its records file) versus what is currently deployed on the authoritative
# server, so puppet can detect drift and re-apply.
#
# Structured value:
# { server, count, expected => [{zone,fqdn,type,ttl,value}], in_sync,
# drift => [{...,deployed => [...]}] }
# Helpers for the dns_records fact.
module DnsRecordsFact
RECORDS_FILE = '/var/lib/dns-updater/records'
SERVER_FILE = '/var/lib/dns-updater/server'
module_function
# normalise a value for comparison: strip, drop trailing dot, downcase
def norm(value)
value.to_s.strip.chomp('.').downcase
end
def server
File.exist?(SERVER_FILE) ? File.read(SERVER_FILE).strip : nil
end
# a name relative to a zone (or @) as a fully-qualified name
def to_fqdn(name, zone)
return "#{zone}." if name.to_s.empty? || name == '@'
"#{name}.#{zone}."
end
# parse one "zone|name|type|ttl|value" line into a record hash (nil to skip)
def parse_line(line)
line = line.strip
return nil if line.empty? || line.start_with?('#')
zone, name, type, ttl, value = line.split('|', 5)
return nil unless zone && type && value
{ 'zone' => zone, 'fqdn' => to_fqdn(name, zone), 'type' => type, 'ttl' => ttl, 'value' => value }
end
# parse the records file into record hashes
def expected
return [] unless File.exist?(RECORDS_FILE)
File.readlines(RECORDS_FILE).filter_map { |line| parse_line(line) }
end
# the values currently deployed for a record, per the authoritative server
def deployed(record, srv)
cmd = ['dig', '+short', '+time=2', '+tries=1']
cmd << "@#{srv}" if srv && !srv.empty?
cmd += [record['fqdn'], record['type']]
out = Facter::Core::Execution.execute(cmd.join(' '), on_fail: '')
out.to_s.split("\n").map { |line| norm(line) }.reject(&:empty?)
end
def report
srv = server
exp = expected
drift = exp.filter_map do |record|
dep = deployed(record, srv)
record.merge('deployed' => dep) unless dep.include?(norm(record['value']))
end
{ 'server' => srv, 'count' => exp.length, 'expected' => exp, 'in_sync' => drift.empty?, 'drift' => drift }
end
end
Facter.add(:dns_records) do
confine kernel: 'Linux'
setcode do
File.exist?(DnsRecordsFact::RECORDS_FILE) ? DnsRecordsFact.report : nil
end
end
# Convenience boolean for `if $facts['dns_records_insync']` guards.
Facter.add(:dns_records_insync) do
confine kernel: 'Linux'
setcode do
v = Facter.value(:dns_records)
v.nil? ? nil : v['in_sync']
end
end
-38
View File
@@ -1,38 +0,0 @@
---
# DO NOT MODIFY - MANAGED BY PUPPET
mirrors:
docker.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "dockerhub/$1"
ghcr.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "ghcr/$1"
quay.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "quay/$1"
registry.k8s.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "k8s-registry/$1"
registry.gitlab.com:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "gitlab/$1"
docker.elastic.co:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "elastic/$1"
gcr.io:
endpoint:
- "https://artifactapi.k8s.syd1.au.unkin.net"
rewrite:
"^(.*)$": "gcr/$1"
+2 -1
View File
@@ -8,6 +8,7 @@ class rke2::config (
String $node_token = $rke2::node_token,
Array[String[1]] $extra_config_files = $rke2::extra_config_files,
Boolean $manage_registries = $rke2::manage_registries,
Hash $registries = $rke2::registries,
){
# if its not the bootstrap node, add join path to config
@@ -35,7 +36,7 @@ class rke2::config (
owner => 'root',
group => 'root',
mode => '0644',
source => 'puppet:///modules/rke2/registries.yaml',
content => epp('rke2/registries.yaml.epp', { registries => $registries }),
require => Package["rke2-${node_type}"],
notify => Service["rke2-${node_type}"],
}
+1
View File
@@ -13,6 +13,7 @@ class rke2 (
Array[String[1]] $extra_config_files = $rke2::params::extra_config_files,
Stdlib::HTTPUrl $container_archive_source = $rke2::params::container_archive_source,
Boolean $manage_registries = $rke2::params::manage_registries,
Hash $registries = $rke2::params::registries,
) inherits rke2::params {
include rke2::install
+1
View File
@@ -13,4 +13,5 @@ class rke2::params (
Array[String[1]] $extra_config_files = [],
Stdlib::HTTPUrl $container_archive_source = 'https://github.com/rancher/rke2/releases/download',
Boolean $manage_registries = false,
Hash $registries = {},
) {}
@@ -0,0 +1,20 @@
<%- | Hash $registries | -%>
---
# DO NOT MODIFY - MANAGED BY PUPPET
mirrors:
<%- $registries.each |$registry, $config| { -%>
<%= $registry %>:
endpoint:
<%- $config['endpoint'].each |$ep| { -%>
- "<%= $ep %>"
<%- } -%>
<%- if $config['rewrite'] { -%>
rewrite:
<%- $config['rewrite'].each |$pattern, $replacement| { -%>
"<%= $pattern %>": "<%= $replacement %>"
<%- } -%>
<%- } -%>
<%- if $config['disable-default-registry-endpoint'] { -%>
disable-default-registry-endpoint: true
<%- } -%>
<%- } -%>
+3
View File
@@ -12,6 +12,9 @@ class profiles::base () {
# manage the puppet agent
include profiles::puppet::agent
include profiles::puppet::client
# k8s migration switch; ships disabled, flip its ::enabled boolean in
# hiera (node/role/common) to repoint a target onto puppet-on-k8s.
include profiles::puppet::migrate
# include the base profiles
include profiles::base::repos
+4 -1
View File
@@ -68,7 +68,10 @@ class profiles::cobbler::config {
# export cnames for cobbler
if $is_cobbler_master {
profiles::dns::record { "${::facts['networking']['fqdn']}_${service_cname}_CNAME":
value => $::facts['networking']['hostname'],
# CNAME target must be a fully-qualified name (trailing dot): dns-updater
# parses the value with no $ORIGIN, so a bare label becomes root-absolute
# ("ausyd1nxvm2098.") and dead-ends in NXDOMAIN.
value => "${::facts['networking']['fqdn']}.",
type => 'CNAME',
record => "${service_cname}.",
zone => $::facts['networking']['domain'],
+4 -1
View File
@@ -11,9 +11,12 @@ class profiles::dns::base (
Optional[String] $ns_role = undef,
){
# install bind_utils
# install bind_utils (provides nsupdate)
include bind::updater
# assemble the host's DNS records and nsupdate them to the authoritative server
include profiles::dns::updater
# if ns_role is set, find all hosts matching that enc_role
$nameserver_array = $ns_role ? {
undef => $nameservers,
+27 -6
View File
@@ -1,4 +1,10 @@
# defines the base record that will be exported
# profiles::dns::record
#
# Declares a DNS record for this host. Publishes it via either or both methods,
# controlled by profiles::dns::updater's toggles (both on during cutover):
# - nsupdate: a local concat fragment consumed by profiles::dns::updater,
# which nsupdates it to the authoritative server.
# - export: the legacy @@concat::fragment exported to the puppet DNS master.
define profiles::dns::record (
String $record,
Enum[
@@ -13,11 +19,26 @@ define profiles::dns::record (
String $value,
String $zone,
Integer $order,
Stdlib::AbsolutePath $basedir = lookup('profiles::dns::master::basedir'),
Integer $ttl = 300,
) {
@@concat::fragment { "${zone}_${name}":
target => "${basedir}/${zone}.conf",
content => "${record} IN ${type} ${value}\n",
order => $order,
include profiles::dns::updater
# new: local records file consumed by the nsupdate service
if $profiles::dns::updater::manage_nsupdate {
# zone|name|type|ttl|value (parsed by the dns-update script)
concat::fragment { "dns-record-${name}":
target => $profiles::dns::updater::records_file,
content => "${zone}|${record}|${type}|${ttl}|${value}\n",
order => sprintf('%03d', $order),
}
}
# legacy: export the fragment to the puppet DNS master
if $profiles::dns::updater::manage_export {
@@concat::fragment { "${zone}_${name}":
target => "${profiles::dns::updater::master_basedir}/${zone}.conf",
content => "${record} IN ${type} ${value}\n",
order => $order,
}
}
}
+173
View File
@@ -0,0 +1,173 @@
# profiles::dns::updater
#
# Publishes this host's DNS records. Two methods, independently toggled so both
# can run during the k8s cutover (profiles::dns::record honours the same flags):
#
# - nsupdate ($manage_nsupdate): assemble the records into a local file and
# nsupdate them to the k8s authoritative write endpoint via a systemd .path
# unit that watches the file. Inert until $key_secret (TSIG) is set.
# - export ($manage_export): the legacy exported-resources flow to the puppet
# DNS master. Kept during cutover; disable once k8s is authoritative.
#
# nsupdate comes from bind-utils (installed via bind::updater in
# profiles::dns::base).
class profiles::dns::updater (
Boolean $manage_nsupdate = true,
Boolean $manage_export = true,
String $server = '198.18.200.9',
String $key_name = 'client-update',
String $key_algorithm = 'hmac-sha256',
Optional[Sensitive[String]] $key_secret = undef,
Integer $default_ttl = 300,
Stdlib::AbsolutePath $records_file = '/var/lib/dns-updater/records',
Stdlib::AbsolutePath $state_dir = '/var/lib/dns-updater',
Stdlib::AbsolutePath $config_dir = '/etc/dns-updater',
Stdlib::AbsolutePath $master_basedir = lookup('profiles::dns::master::basedir'),
# dns-updater daemon (replaces the dns-update shell script). 'latest' so hosts
# pick up new releases (e.g. the record filter); rpm-internal metadata_expire
# is 1h so this does not thrash.
String $package_ensure = 'latest',
Stdlib::AbsolutePath $api_socket = '/run/dns-updater/api.sock',
String $resync = '10m',
Enum['debug', 'info', 'warn', 'error'] $log_level = 'info',
Boolean $watch_interfaces = true,
# Never publish records whose address falls in these ranges: the k8s pod CIDR
# (10.42.0.0/16) and service CIDR (10.43.0.0/16), the LB VIP pool
# (198.18.200.0/24) and 198.18.199.0/24. Keeps kube-lb0/flannel/etc. out of
# the authoritative zones and stops NOTAUTH updates for zones we do not host.
Array[String] $deny_ranges = ['198.18.199.0/24', '198.18.200.0/24', '10.42.0.0/16', '10.43.0.0/16'],
Array[String] $allow_ranges = [],
Array[String] $deny_domains = [],
Array[String] $allow_domains = [],
) {
$state_file = "${state_dir}/applied"
$server_file = "${state_dir}/server"
$key_file = "${config_dir}/key"
if $manage_nsupdate {
file { $state_dir:
ensure => directory,
owner => 'root',
group => 'root',
mode => '0755',
}
# Server address, read by the dns_records fact for drift detection.
file { $server_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => "${server}\n",
require => File[$state_dir],
}
# Records file, assembled from profiles::dns::record fragments.
concat { $records_file:
ensure => present,
owner => 'root',
group => 'root',
mode => '0644',
ensure_newline => true,
warn => false,
require => File[$state_dir],
}
concat::fragment { 'dns-update-header':
target => $records_file,
content => "# Managed by puppet (profiles::dns::record): zone|name|type|ttl|value\n",
order => '00',
}
if $key_secret =~ Undef {
notify { 'dns-updater-inert':
message => 'profiles::dns::updater: key_secret unset; records assembled but not applied.',
loglevel => 'info',
}
} else {
file { $config_dir:
ensure => directory,
owner => 'root',
group => 'root',
mode => '0700',
}
file { $key_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
show_diff => false,
content => Sensitive(epp('profiles/dns/tsig-key.epp', {
'name' => $key_name,
'algorithm' => $key_algorithm,
'secret' => $key_secret.unwrap,
})),
}
# dns-updater daemon: watches the records file (inotify) and network
# interfaces, pushes TSIG-signed RFC2136 updates to $server natively.
package { 'dns-updater':
ensure => $package_ensure,
}
$deny_ranges_str = join($deny_ranges, ',')
$allow_ranges_str = join($allow_ranges, ',')
$deny_domains_str = join($deny_domains, ',')
$allow_domains_str = join($allow_domains, ',')
$env_content = @("ENV")
# Managed by puppet (profiles::dns::updater).
DNS_UPDATER_SERVER=${server}
DNS_UPDATER_KEY_FILE=${key_file}
DNS_UPDATER_RECORDS_FILE=${records_file}
DNS_UPDATER_STATE_FILE=${state_file}
DNS_UPDATER_API=${api_socket}
DNS_UPDATER_RESYNC=${resync}
DNS_UPDATER_WATCH_INTERFACES=${watch_interfaces}
DNS_UPDATER_LOG_LEVEL=${log_level}
DNS_UPDATER_DENY_RANGES=${deny_ranges_str}
DNS_UPDATER_ALLOW_RANGES=${allow_ranges_str}
DNS_UPDATER_DENY_DOMAINS=${deny_domains_str}
DNS_UPDATER_ALLOW_DOMAINS=${allow_domains_str}
| ENV
file { "${config_dir}/env":
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => $env_content,
require => [File[$config_dir], Package['dns-updater']],
}
# Restart only on config/key change; records-file changes are picked up by
# the daemon's own inotify watch, so no service churn on record edits.
service { 'dns-updater':
ensure => running,
enable => true,
subscribe => [File["${config_dir}/env"], File[$key_file]],
require => [Package['dns-updater'], Concat[$records_file], File[$key_file]],
}
# Retire the previous shell-based mechanism.
file { '/usr/local/bin/dns-update':
ensure => absent,
}
systemd::unit_file { 'dns-update.service':
ensure => absent,
active => false,
enable => false,
}
systemd::unit_file { 'dns-update.path':
ensure => absent,
active => false,
enable => false,
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
# base fedora settings
#
# Fedora hosts share the RedHat-family base profiles (yum/dnf, crypto-policies,
# firewalld); this class carries the handful of Fedora-specific bits. Kept
# deliberately small so it can grow into workstation/laptop (e.g. sway) support
# later without disturbing servers.
class profiles::fedora::base {
# Fedora manages interfaces through NetworkManager. Ensure it is enabled and
# running (some minimal and cloud images ship it disabled).
service { 'NetworkManager':
ensure => running,
enable => true,
}
}
+8 -2
View File
@@ -21,7 +21,10 @@ class profiles::haproxy::dns (
$parts = split($cname, '\.')
$domain = join($parts[1, $parts.length], '.')
profiles::dns::record { "${::facts['networking']['fqdn']}_${cname}_CNAME":
value => "${location_environment}-halb",
# CNAME target must be a fully-qualified name (trailing dot): dns-updater
# parses the value with no $ORIGIN, so a bare label becomes root-absolute
# ("au-syd1-prod-halb.") and dead-ends in NXDOMAIN.
value => "${location_environment}-halb.${facts['networking']['domain']}.",
type => 'CNAME',
record => "${cname}.",
zone => $domain,
@@ -58,7 +61,10 @@ class profiles::haproxy::dns (
$parts = split($cname, '\.')
$domain = join($parts[1, $parts.length], '.')
profiles::dns::record { "${::facts['networking']['fqdn']}_${cname}_CNAME":
value => "${location_environment}-halb-vrrp",
# Fully-qualified target (trailing dot); see the note on the non-vrrp
# cnames above. The matching A record is published in main.unkin.net
# and unkin.net just above.
value => "${location_environment}-halb-vrrp.${facts['networking']['domain']}.",
type => 'CNAME',
record => "${cname}.",
zone => $domain,
+8 -1
View File
@@ -16,11 +16,18 @@ class profiles::puppet::agent (
$use_service = 'puppet'
}
# OpenVox publishes RedHat-family builds per distribution: enterprise-linux
# under el/<major>, Fedora under fedora/<major>.
$openvox_dist = $facts['os']['name'] ? {
'Fedora' => 'fedora',
default => 'el',
}
# manage the yumrepo for the given package
if $openvox_enable and $facts['os']['family'] == 'RedHat' {
yumrepo { 'openvox':
ensure => 'present',
baseurl => "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/openvox/openvox7/el/${facts['os']['release']['major']}/${facts['os']['architecture']}/",
baseurl => "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/openvox/openvox7/${openvox_dist}/${facts['os']['release']['major']}/${facts['os']['architecture']}/",
descr => 'openvox repository',
gpgkey => 'https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/openvox/GPG-KEY-openvox.pub',
notify => Exec['dnf_makecache'],
+56 -11
View File
@@ -4,19 +4,64 @@
#
# site/profile/manifests/puppet/client.pp
class profiles::puppet::client (
Array $dns_alt_names = [$trusted['certname']],
String $server = 'puppetmaster',
String $ca_server = 'puppetca',
String $environment = 'develop',
Integer $runinterval = 1800,
Integer $runtimeout = 3600,
Boolean $show_diff = true,
Boolean $usecacheonfailure = false,
Integer $facts_soft_limit = 4096,
Boolean $splay = true,
Integer $splaylimit = 600,
Array $dns_alt_names = [$trusted['certname']],
String $server = 'puppetmaster',
String $ca_server = 'puppetca',
Optional[String] $report_server = undef,
String $environment = 'develop',
Integer $runinterval = 1800,
Integer $runtimeout = 3600,
Boolean $show_diff = true,
Boolean $usecacheonfailure = false,
Integer $facts_soft_limit = 4096,
Boolean $splay = true,
Integer $splaylimit = 600,
Optional[Stdlib::Absolutepath] $ssldir = undef,
) {
# Pull in the k8s-migration switch. When
# profiles::puppet::migrate::enabled is true at ANY hiera layer
# (node/role/common) the effective endpoints below flip to the k8s
# servers and a fresh ssldir -- no other hiera keys required. This class
# never includes client.pp, so there is no include cycle.
include profiles::puppet::migrate
# Effective values fed to the template. Precedence differs by param type:
#
# ssldir / report_server (Optional, default undef): an explicit client
# param wins; else the migrate-derived value when enabled; else the
# legacy default (undef -> template omits ssldir; report_server -> server).
#
# server / ca_server (String, no undef sentinel): the migrate value wins
# when migrate::enabled, otherwise the client param (which is the legacy
# default unless an operator set it explicitly in hiera). To pin a
# bespoke server while enabled, leave enabled => false for that target
# and set profiles::puppet::client::server directly.
$migrate_on = $profiles::puppet::migrate::enabled
$effective_server = $migrate_on ? {
true => $profiles::puppet::migrate::new_server,
default => $server,
}
$effective_ca_server = $migrate_on ? {
true => $profiles::puppet::migrate::new_ca_server,
default => $ca_server,
}
# report_server: explicit param wins; else follow the effective server.
$effective_report_server = $report_server ? {
undef => $effective_server,
default => $report_server,
}
# ssldir: explicit param wins; else the fresh k8s ssldir when migrating;
# else undef (template omits the key, i.e. puppet's built-in default).
$effective_ssldir = $ssldir ? {
undef => $migrate_on ? {
true => $profiles::puppet::migrate::new_ssldir,
default => undef,
},
default => $ssldir,
}
# dont manage puppet.conf if this is a puppetmaster
if $facts['enc_role'] != 'roles::infra::puppet::master' {
+87
View File
@@ -0,0 +1,87 @@
# Class: profiles::puppet::migrate
#
# Repoints a VM puppet agent from the legacy VM puppetmasters onto the new
# puppet-on-kubernetes servers.
#
# The agent's existing ssldir holds a client certificate signed by the OLD CA;
# the new k8s CA will neither trust that cert nor recognise the agent. Rather
# than revoke/clean the old creds (which would burn the rollback path), this
# class switches the agent to a FRESH ssldir. On the next run the agent finds
# an empty ssldir, generates a new key + CSR, submits it to the new CA
# (autosign = true on the k8s side signs it immediately), and then pulls its
# catalog from the new compilers. The OLD ssldir is left untouched on disk, so
# reverting the hiera below restores the node to the legacy masters with no
# re-enrolment.
#
# The catalog that performs the switch is compiled by the OLD server (the node
# still points at it when it applies this change). profiles::puppet::client
# rewrites puppet.conf with the new server / ca_server / report_server / ssldir
# and notifies Service['puppet'], so the very next agent run lands on k8s.
#
# NOTE: this is deliberately hiera-driven per node/role so waves can be rolled
# out and rolled back one target at a time. The class only manages the fresh
# ssldir; the actual puppet.conf changes ride on profiles::puppet::client's
# existing template. client.pp `include`s this class and, when enabled, folds
# new_server / new_ca_server / new_ssldir into the effective server /
# ca_server / report_server / ssldir it renders. Do NOT add a second File
# resource for puppet.conf here -- client.pp already owns it.
#
# == Wiring (single boolean)
#
# This class ships wired into profiles::base with enabled => false, so every
# node evaluates it but nothing changes until the boolean is flipped. Because
# client.pp includes it and reads its params, setting the ONE key below at any
# hiera layer repoints a target -- no client.pp param overrides needed.
#
# The endpoints (new_server / new_ca_server / new_ssldir) are param defaults
# on this class; override them in hiera too if the k8s names ever change.
#
# --- Canary: one node -> hieradata/nodes/<certname>.yaml -------------------
# profiles::puppet::migrate::enabled: true
# ---------------------------------------------------------------------------
#
# --- Wave: a whole role -> hieradata/roles/<t1>/<t2>[/<t3>].yaml -----------
# profiles::puppet::migrate::enabled: true
# ---------------------------------------------------------------------------
#
# --- Estate flip -> hieradata/common.yaml (retires the legacy masters) -----
# profiles::puppet::migrate::enabled: true
# ---------------------------------------------------------------------------
#
# Manual override: profiles::puppet::client::ssldir and ::report_server are
# Optional and, when set explicitly in hiera, win over the migrate-derived
# value. server / ca_server have no undef sentinel, so while enabled => true
# the migrate endpoints win; to point a target at a bespoke server leave
# enabled => false for it and set profiles::puppet::client::server directly.
#
# == Rollback (per target)
#
# Remove the profiles::puppet::migrate::enabled key (or set it back to false)
# at the node/role/common layer. The next run rewrites puppet.conf back to the
# legacy server + legacy ssldir, which still contains the original CA-signed
# cert, and the node is back on the VM masters.
#
# site/profiles/manifests/puppet/migrate.pp
class profiles::puppet::migrate (
Boolean $enabled = false,
String $new_server = 'puppet.k8s.syd1.au.unkin.net',
String $new_ca_server = 'puppetca.k8s.syd1.au.unkin.net',
Stdlib::Absolutepath $new_ssldir = '/etc/puppetlabs/puppet/ssl-k8s',
) {
# never touch the puppet masters themselves (same guard as client.pp)
if $enabled and $facts['enc_role'] != 'roles::infra::puppet::master' {
# Ensure the fresh ssldir exists with agent-correct ownership/perms. The
# puppet agent will create it on demand, but pre-creating it keeps the very
# first post-switch run from racing directory creation and makes the
# migration state auditable. Contents (keys, certs, crl) are populated by
# the agent against the new CA -- we only own the directory itself.
file { $new_ssldir:
ensure => 'directory',
owner => 'root',
group => 'root',
mode => '0771',
}
}
}
+60
View File
@@ -134,4 +134,64 @@ class profiles::puppet::server (
enable => true,
require => File['/usr/local/bin/puppet_generate_types.sh'],
}
# Guard against an OpenJDK (or other JVM library) package upgrade landing
# underneath a long-running puppetserver. When the java package is replaced
# out-of-band (e.g. a yum repo/version bump), the old versioned JAVA_HOME is
# removed while the JVM keeps running against the now-deleted files. The JVM
# still execs jspawnhelper from its original (deleted) JAVA_HOME on every
# subprocess spawn, so ProcessBuilder fails with "error=2, No such file or
# directory" -- which breaks the exec ENC (/opt/cobbler-enc/cobbler-enc) and
# 500s every catalog compile fleet-wide. A restart re-binds to the new JVM.
# This timer detects the running JVM executing from deleted binaries and
# restarts the service to recover automatically.
file { '/usr/local/bin/puppetserver_jvm_guard.sh':
ensure => file,
mode => '0755',
content => @(EOF),
#!/bin/bash
# Restart puppetserver if its running JVM is executing deleted binaries
# (e.g. an OpenJDK package upgrade replaced the versioned JAVA_HOME),
# which breaks subprocess spawning and thus ENC / catalog compilation.
for pid in $(pgrep -f puppet-server-release.jar); do
exe=$(readlink "/proc/${pid}/exe" 2>/dev/null)
case "${exe}" in
*'(deleted)'*)
logger -t puppetserver-jvm-guard "puppetserver JVM (pid ${pid}) running on deleted binaries; restarting"
systemctl restart puppetserver
exit 0
;;
esac
done
exit 0
| EOF
}
$_guard_timer = @(EOT)
[Unit]
Description=puppetserver JVM guard timer
[Timer]
OnCalendar=*:0/5
RandomizedDelaySec=30s
[Install]
WantedBy=timers.target
EOT
$_guard_service = @(EOT)
[Unit]
Description=puppetserver JVM guard service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/puppetserver_jvm_guard.sh
User=root
Group=root
EOT
systemd::timer { 'puppetserver-jvm-guard.timer':
timer_content => $_guard_timer,
service_content => $_guard_service,
active => true,
enable => true,
require => File['/usr/local/bin/puppetserver_jvm_guard.sh'],
}
}
+9
View File
@@ -122,6 +122,15 @@ class profiles::vault::server (
]
}
# go-plugin creates each secrets plugin's control socket as /tmp/pluginNNN;
# systemd-tmpfiles-clean reaps files aged past the /tmp policy, severing the
# socket of a long-lived plugin and orphaning the still-running process
# (every request then fails: rpc Unavailable, dial unix /tmp/pluginNNN: no
# such file). Exclude the sockets from cleaning for all plugins.
systemd::tmpfile { 'openbao-plugin-sockets.conf':
content => "x /tmp/plugin*\n",
}
# ensure the vault audit log exists
file { $audit_log:
ensure => 'file',
+5
View File
@@ -0,0 +1,5 @@
<%- | String $name, String $algorithm, String $secret | -%>
key "<%= $name %>" {
algorithm <%= $algorithm %>;
secret "<%= $secret %>";
};
@@ -1,12 +1,15 @@
[main]
dns_alt_names = <%= @dns_alt_names_string %>
<% unless @effective_ssldir.nil? -%>
ssldir = <%= @effective_ssldir %>
<% end -%>
[agent]
server = <%= @server %>
ca_server = <%= @ca_server %>
server = <%= @effective_server %>
ca_server = <%= @effective_ca_server %>
environment = <%= @environment %>
report = true
report_server = <%= @server %>
report_server = <%= @effective_report_server.nil? ? @effective_server : @effective_report_server %>
runinterval = <%= @runinterval %>
runtimeout = <%= @runtimeout %>
show_diff = <%= @show_diff %>