Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .claude/plans/infrahouse-keyring-convergence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
puppet-code (0.1.0-1build312) noble; urgency=medium

* commit event. see changes history in git log

-- root <packager@infrahouse.com> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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-<codename>.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 <codename>
# 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 <codename>
# 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} <codename>" >&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
1 change: 1 addition & 0 deletions environments/sandbox/modules/profile/manifests/base.pp
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
67 changes: 67 additions & 0 deletions environments/sandbox/modules/profile/manifests/infrahouse_repo.pp
Original file line number Diff line number Diff line change
@@ -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-<codename>.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-<codename>) 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,
}
}
Loading