diff --git a/.claude/plans/infrahouse-keyring-convergence.md b/.claude/plans/infrahouse-keyring-convergence.md index a46e5a7..5779071 100644 --- a/.claude/plans/infrahouse-keyring-convergence.md +++ b/.claude/plans/infrahouse-keyring-convergence.md @@ -88,15 +88,19 @@ Test node: `jumphost-sincere-crawdad.rmdbkn.ci-cd.infrahouse.com` > note. `gpg --show-keys` "lists every key in the bundle" check deferred until then. **1b. Cut a release** (once the live test looks good) -- [ ] Merge to `main` → CD publishes the `.deb` to the APT repo (dev picks it up on next Puppet run) +- [x] Merge to `main` → CD publishes the `.deb` to the APT repo (dev picks it up on next Puppet run) + — PR [#278](https://github.com/infrahouse/puppet-code/pull/278) **merged**. -**1c. Zero-bootstrap check via instance-refresh** -- [ ] `instance-refresh` the dev jumphost ASG so a brand-new instance provisions from scratch -- [ ] On the fresh instance confirm cloud-init first-boot + Puppet convergence agree; keyring valid, - `apt-get update` clean — i.e. Puppet bootstraps an instance from zero, not just an existing one +**1c. Zero-bootstrap check via instance-refresh** — done 2026-07-04 on fresh instance `ip-10-1-100-168`. +- [x] Brand-new instance provisioned from scratch off the released `.deb` +- [x] cloud-init first-boot + Puppet convergence agree: script deployed, `Exec[…::converge]` no-op + (keyring already == bundle), source list byte-identical (no rewrite), no spurious apt refresh +- [x] `keyring == published bundle` (`curl … | cmp` → converged), `apt-get update` clean + (`Hit … noble InRelease`, no `EXPKEYSIG`) — Puppet bootstraps an instance from zero ### Phase 2: Sandbox -- [ ] Promote: copy the change into `environments/sandbox/modules/profile` +- [x] Promote: copy the change into `environments/sandbox/modules/profile` (byte-identical to dev: + `infrahouse_repo.pp` + `files/converge-infrahouse-keyring.sh` + `base.pp` include). PR pending. - [ ] Cut a release (merge → CD) - [ ] Watch sandbox nodes across roles: keyring converges, `apt-get update` clean, no drift diff --git a/debian/changelog b/debian/changelog index 2c8f995..8e1980a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +puppet-code (0.1.0-1build312) noble; urgency=medium + + * commit event. see changes history in git log + + -- root Sat, 04 Jul 2026 00:47:05 +0000 + puppet-code (0.1.0-1build311) noble; urgency=medium * commit event. see changes history in git log diff --git a/environments/sandbox/modules/profile/files/converge-infrahouse-keyring.sh b/environments/sandbox/modules/profile/files/converge-infrahouse-keyring.sh new file mode 100644 index 0000000..d16d476 --- /dev/null +++ b/environments/sandbox/modules/profile/files/converge-infrahouse-keyring.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Managed by Puppet (profile::infrahouse_repo). Do not edit locally. +# +# Converge /etc/apt/keyrings/infrahouse.gpg from the published InfraHouse +# signing-key bundle so that long-lived instances pick up rotated GPG keys +# without being reprovisioned. Trust is anchored on the TLS channel to +# release-.infrahouse.com (no fingerprint pinning), consistent with +# cloud-init. The (possibly concatenated multi-key) armored bundle is fetched +# each run and dearmored into the keyring. +# +# Usage: +# converge-infrahouse-keyring.sh check +# exit 0 if the installed keyring already matches the published bundle, +# exit 1 otherwise (including when the bundle cannot be fetched, so the +# caller re-runs 'apply' and surfaces the underlying error). +# converge-infrahouse-keyring.sh apply +# fetch + dearmor + install the keyring atomically. A fetch/dearmor +# failure leaves the existing keyring intact. Refreshing apt is left to +# the Puppet-managed 'apt-get update' that this exec notifies. +# +set -euo pipefail + +export PATH=/usr/sbin:/usr/bin:/sbin:/bin + +KEYRING=/etc/apt/keyrings/infrahouse.gpg + +usage() { + echo "usage: $0 {check|apply} " >&2 + exit 2 +} + +[ "$#" -eq 2 ] || usage +action=$1 +codename=$2 +url="https://release-${codename}.infrahouse.com/DEB-GPG-KEY-release-${codename}.infrahouse.com" + +# Fetch the armored bundle over TLS and dearmor it into $1. Returns non-zero +# without touching $KEYRING on any failure (fetch, dearmor, or empty result). +fetch_dearmored() { + local out=$1 armored + armored=$(mktemp) + # shellcheck disable=SC2064 + trap "rm -f '${armored}'" RETURN + curl --fail --silent --show-error --location \ + --retry 3 --retry-delay 2 --max-time 30 \ + --output "${armored}" "${url}" + gpg --dearmor <"${armored}" >"${out}" + # Refuse to install an empty/garbage keyring. + test -s "${out}" +} + +candidate=$(mktemp) +trap 'rm -f "${candidate}"' EXIT + +case "${action}" in + check) + fetch_dearmored "${candidate}" || exit 1 + # 0 = already converged, 1 = differs or keyring missing. + cmp -s "${candidate}" "${KEYRING}" + ;; + apply) + fetch_dearmored "${candidate}" + if cmp -s "${candidate}" "${KEYRING}"; then + exit 0 + fi + install -D -o root -g root -m 0644 "${candidate}" "${KEYRING}" + ;; + *) + usage + ;; +esac \ No newline at end of file diff --git a/environments/sandbox/modules/profile/manifests/base.pp b/environments/sandbox/modules/profile/manifests/base.pp index 5790001..1f84f07 100644 --- a/environments/sandbox/modules/profile/manifests/base.pp +++ b/environments/sandbox/modules/profile/manifests/base.pp @@ -5,6 +5,7 @@ include 'profile::ntp' include 'profile::repos' + include 'profile::infrahouse_repo' include 'profile::packages' include 'profile::infrahouse_toolkit' include 'profile::puppet_apply' diff --git a/environments/sandbox/modules/profile/manifests/infrahouse_repo.pp b/environments/sandbox/modules/profile/manifests/infrahouse_repo.pp new file mode 100644 index 0000000..ee65810 --- /dev/null +++ b/environments/sandbox/modules/profile/manifests/infrahouse_repo.pp @@ -0,0 +1,67 @@ +# @summary: Converges the InfraHouse APT repository (source list + signing key). +# +# cloud-init seeds both `/etc/apt/sources.list.d/50-infrahouse.list` and +# `/etc/apt/keyrings/infrahouse.gpg` at **first boot only** — it must, because the +# InfraHouse toolkit (ih-secrets) has to be installable before Puppet ever runs. +# This class **converges** both on every Puppet run so long-lived instances pick +# up a rotated signing key (the urgent case: the key expires) or a changed repo +# line without being reprovisioned. +# +# Ownership is intentionally shared: cloud-init writes the seed, Puppet keeps it +# in sync. The rendered source line is kept byte-identical to cloud-init's seed so +# the two never fight. Trust is anchored on the TLS channel to +# release-.infrahouse.com (no fingerprint pinning), consistent with +# cloud-init. On a fetch failure the previously installed keyring is left intact. +# +# @param codename APT codename whose repo/bundle to converge; derived from the +# node's OS facts by default. +class profile::infrahouse_repo ( + String[1] $codename = $facts['os']['distro']['codename'], +) { + $script = '/usr/local/sbin/converge-infrahouse-keyring.sh' + $keyring = '/etc/apt/keyrings/infrahouse.gpg' + $source_list = '/etc/apt/sources.list.d/50-infrahouse.list' + + stdlib::ensure_packages(['curl', 'gnupg', 'ca-certificates']) + + file { $script: + ensure => file, + owner => 'root', + group => 'root', + mode => '0755', + source => 'puppet:///modules/profile/converge-infrahouse-keyring.sh', + } + + # Converge the keyring: 'check' short-circuits when already in sync, so steady + # state is a no-op and the shared 'apt-get update' only fires on a key change. + exec { 'profile::infrahouse_repo::converge': + command => "${script} apply ${codename}", + unless => "${script} check ${codename}", + path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'], + require => [ + File[$script], + Package['curl'], + Package['gnupg'], + ], + notify => Exec['profile::infrahouse_repo::apt_update'], + } + + # Converge the repo line. Kept byte-identical to cloud-init's first-boot seed; + # $codename fills both the host (release-) and the suite. + file { $source_list: + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => "deb [signed-by=${keyring}] https://release-${codename}.infrahouse.com/ ${codename} main\n", + require => Exec['profile::infrahouse_repo::converge'], + notify => Exec['profile::infrahouse_repo::apt_update'], + } + + # Single refresh point for both the keyring and the source list. + exec { 'profile::infrahouse_repo::apt_update': + command => 'apt-get update', + path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'], + refreshonly => true, + } +} \ No newline at end of file