From a0f03d9e8e2abb83d394977995a6a13722d95433 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 7 Oct 2026 09:02:20 +1100 Subject: [PATCH 1/2] fix: add packer as its own opt-in role Packer is the one HashiCorp tool we install: it is BUSL with no open-source fork, and image builds need it. It gets its own role, run only by --tags packer and pulled in by no persona, so a default or persona run installs nothing from HashiCorp. Ubuntu and Fedora take HashiCorp's repo. The signing key is staged, checked for exactly one primary key matching the fingerprint HashiCorp publishes (D55C0D1A...CA026560), and only then trusted. A mismatch removes the repo, keeps the previously trusted key and lands in the end-of-run warnings. A host still trusting the expired 798A...E701 key gets the new one, and the legacy hashicorp.list is removed so apt does not see the repo twice. macOS takes hashicorp/tap. --tags removals now takes terraform and vault only. It used to delete the HashiCorp repo, keyrings and tap as well, which broke Packer on every host it ran on. The remediation scenario asserts the repo and keys survive. --- README.md | 4 +- ansible/molecule/remediation/prepare.yml | 12 +- ansible/molecule/remediation/verify.yml | 15 +- ansible/playbooks/main.yml | 5 + ansible/roles/infrastructure/tasks/cloud.yml | 11 +- ansible/roles/infrastructure/tasks/main.yml | 9 +- .../roles/infrastructure/tasks/removals.yml | 50 +---- ansible/roles/packer/README.md | 39 ++++ ansible/roles/packer/defaults/main.yml | 9 + ansible/roles/packer/tasks/main.yml | 195 ++++++++++++++++++ ansible/roles/packer/vars/main.yml | 10 + docs/install-matrix.md | 9 + install.sh | 4 + 13 files changed, 301 insertions(+), 71 deletions(-) create mode 100644 ansible/roles/packer/README.md create mode 100644 ansible/roles/packer/defaults/main.yml create mode 100644 ansible/roles/packer/tasks/main.yml create mode 100644 ansible/roles/packer/vars/main.yml diff --git a/README.md b/README.md index 34ec972..19b5dc2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ flowchart TD | `developer-gui` | VS Code, Ghostty, DBeaver. Privacy + AI-upsell de-nag profile for VSCode/VSCodium/Cursor off unless `-e vscode_privacy_enabled=true` | | `developer-rust` / `-go` / `-python` / `-node` / `-typescript` / `-c` | Language toolchains | | `infrastructure` | OpenTofu, OpenBao, AWS CLI, helm, terraform-docs, `k8s` (kubectl, kubectx, kubens, k9s, kind, argocd, dive, kustomize, kubeconform, kube-linter), `data` (clickhouse-client, confluent-cli, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) | +| `packer` | HashiCorp Packer from HashiCorp's repo or tap. Opt-in, in no persona | | `contributor` | hyperi-ci + its check tools (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, yamllint, ansible-lint, pre-commit, act, git-scrub, macbash | | `soe` / `soe-gui` | HyperI org policy (opt-in) | | `--full-stack` / `--infra` / `--languages [list]` | Persona bundles (see `--help`) | @@ -185,7 +186,8 @@ digest. Read that before changing a role or adding a tool. - `developer-gui`: VS Code, Ghostty (Solarized theme), DBeaver - `vscode-privacy` (off by default): strips the Copilot/AI upsell UI and the telemetry that stock VSCode ships enabled, across VSCode, VSCodium and Cursor. Enable with `-e vscode_privacy_enabled=true`. It merges one marked block into `settings.json` and never touches a comment or a key it does not manage, backs the file up before its first write, and `-e vscode_privacy_uninstall=true` takes only its own keys back out. Where you have set one of those keys yourself further down the file, yours wins and the run tells you which ones -- so it cannot look applied while changing nothing - Languages: Rust, Go, Python, C/C++, Node.js, TypeScript (the Astral suite -- uv, ruff, ty -- ships in the base, as does Node.js: it is core tooling that semantic-release and CI need) -- `infrastructure`: OpenTofu + OpenBao (the OSS forks, no HashiCorp BUSL tools), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) +- `infrastructure`: OpenTofu + OpenBao (the OSS forks in place of terraform and vault), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) +- `packer` (off by default, and in no persona): HashiCorp Packer, the one HashiCorp tool we install, because it is BUSL with no open-source fork. Ubuntu and Fedora take HashiCorp's repo, with its signing key checked against the fingerprint HashiCorp publishes before it is trusted; macOS takes `hashicorp/tap`. `--tags removals` leaves that repo alone -- see [roles/packer/README.md](ansible/roles/packer/README.md) - `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change - `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar - `developer-ai` (off by default): the OpenAI Codex CLI as a second opinion alongside Claude Code rather than a replacement for it, plus OpenAI's Codex plugin FOR Claude Code, so `/codex:review` and `/codex:adversarial-review` are things Claude asks Codex for. The plugin is skipped -- with a warning naming the tag that fixes it -- unless claude, codex and a new enough node are all present for that user, because it installs happily without them and then throws on every invocation. Sign-in stays the person's: `codex login --device-auth` on a box with no browser diff --git a/ansible/molecule/remediation/prepare.yml b/ansible/molecule/remediation/prepare.yml index 9591521..a292766 100644 --- a/ansible/molecule/remediation/prepare.yml +++ b/ansible/molecule/remediation/prepare.yml @@ -288,9 +288,8 @@ # The Fedora yq superseded by dnf is NOT planted: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is planted above, in the superseded-by-a-package section. - # The HashiCorp repo is the half that matters: left behind it keeps serving - # BUSL packages and updates. Removing it is a remediation, so only a - # `--tags removals` run does it -- which is what this scenario converges. + # The HashiCorp repo serves Packer, so a `--tags removals` run must leave it + # and its keys in place while it takes terraform and vault away. - name: Plant the HashiCorp apt repository (Ubuntu) ansible.builtin.copy: content: | @@ -307,9 +306,10 @@ # An older revision used apt_repository, which wrote .list, so a host # provisioned before that migration carries both. - # A parseable line, because apt refuses EVERY operation when any source list - # is malformed -- including the removal that is supposed to clear it. - # `trusted=yes` keeps the fixture off the network and off the keyring. + # A parseable line with the same options as the .sources above, because apt + # refuses EVERY operation when a source list is malformed or one repo is + # configured with conflicting options. + # `trusted=yes` keeps the fixture off the keyring. - name: Plant the legacy HashiCorp .list (Ubuntu) ansible.builtin.copy: content: "deb [trusted=yes] https://apt.releases.hashicorp.com noble main\n" diff --git a/ansible/molecule/remediation/verify.yml b/ansible/molecule/remediation/verify.yml index cac2283..3d8c3c4 100644 --- a/ansible/molecule/remediation/verify.yml +++ b/ansible/molecule/remediation/verify.yml @@ -198,27 +198,26 @@ # The Fedora yq migration is not asserted here: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is covered in the superseded-by-a-package section above. - # The repo is the half that keeps serving BUSL packages if it survives. + # Packer installs from the HashiCorp repo, so removing terraform and vault + # must leave the repo and its keys where they are. - name: Stat the HashiCorp repository artefacts (Ubuntu) ansible.builtin.stat: path: "{{ item }}" loop: - /etc/apt/sources.list.d/hashicorp.sources - - /etc/apt/sources.list.d/hashicorp.list - /usr/share/keyrings/hashicorp-archive-keyring.asc - /usr/share/keyrings/hashicorp-archive-keyring.gpg register: verify_hashicorp when: ansible_facts['distribution'] == 'Ubuntu' - - name: Assert the HashiCorp repository was unhooked + - name: Assert the HashiCorp repository survived the removals ansible.builtin.assert: that: - - not item.stat.exists + - item.stat.exists fail_msg: >- - {{ item.item }} survived remediation. The HashiCorp repo goes on - serving BUSL packages and updates for as long as it is configured, - so leaving it is worse than leaving the binaries. - success_msg: "{{ item.item }} removed" + {{ item.item }} was removed. `--tags removals` takes terraform and + vault only; the repo and its keys are what Packer installs from. + success_msg: "{{ item.item }} kept" loop: "{{ verify_hashicorp.results | default([]) }}" loop_control: label: "{{ item.item | default('skipped') }}" diff --git a/ansible/playbooks/main.yml b/ansible/playbooks/main.yml index b97e62a..4e4e36b 100644 --- a/ansible/playbooks/main.yml +++ b/ansible/playbooks/main.yml @@ -252,6 +252,11 @@ become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" tags: ['infrastructure', 'never'] + # Packer from HashiCorp's repo (opt-in, in no persona): BUSL, no fork. + - role: packer + become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" + tags: ['packer', 'never'] + # ======================================================================== # PERSONAS (meta-roles) — opinionated bundles that resolve to the roles # above via meta/dependencies. They pull the CLEAN developer base: its diff --git a/ansible/roles/infrastructure/tasks/cloud.yml b/ansible/roles/infrastructure/tasks/cloud.yml index 93cb6b4..ecf3f49 100644 --- a/ansible/roles/infrastructure/tasks/cloud.yml +++ b/ansible/roles/infrastructure/tasks/cloud.yml @@ -13,10 +13,9 @@ # `aws-vault` below is NOT HashiCorp Vault -- it is aws-vault (the ByteNess fork # of 99designs/aws-vault), an unrelated tool for keeping AWS credentials in the OS keychain. It stays. # -# Packer has no viable fork (the community never produced one) and is -# deliberately NOT installed here. hyperi-infra needs it for image builds; its -# developers install it themselves rather than have us ship a BUSL binary to -# every workstation. +# Packer has no viable fork (the community never produced one), so it comes +# from HashiCorp's repo in its own opt-in role, `packer`, rather than shipping a +# BUSL binary to every box that asks for the IaC tools. # ============================================================================ # OPENTOFU REPOSITORY (Ubuntu and Fedora) @@ -27,8 +26,8 @@ # upstream 1.12.6), which is the wrong side of "almost current" for the tool # that plans and applies infrastructure. # -# The apt suite is literally "any" and the packages are arch-generic, so unlike -# the HashiCorp repo this needs no LTS-codename mapping and no arch handling. +# The apt suite is literally "any" and the packages are arch-generic, so this +# needs no codename and no arch handling. # # The package is `tofu` on both. Fedora's own package is `opentofu`, and the two # conflict over /usr/bin/tofu, so the distro one is removed below. diff --git a/ansible/roles/infrastructure/tasks/main.yml b/ansible/roles/infrastructure/tasks/main.yml index a1b285c..27a5ad5 100644 --- a/ansible/roles/infrastructure/tasks/main.yml +++ b/ansible/roles/infrastructure/tasks/main.yml @@ -3,13 +3,12 @@ # Not HyperI-specific. Org tooling lives in `soe`. # Before the installs: removing terraform/vault must not race the tofu/bao -# install, and the HashiCorp repo has to go before an apt update reads it again. +# install. # # `removals` ONLY, and never on a propagated tag. Installing the IaC tools is -# not a request to delete someone's Terraform or unhook their HashiCorp repo -- -# that is a remediation, asked for explicitly. `--tags infrastructure` and -# `--tags cloud` used to fire it too, which meant a routine IaC install silently -# took both away. +# not a request to delete someone's Terraform -- that is a remediation, asked +# for explicitly. `--tags infrastructure` and `--tags cloud` used to fire it +# too, which meant a routine IaC install silently took it away. - name: Remove the retired HashiCorp tools (opt-in) ansible.builtin.include_tasks: file: removals.yml diff --git a/ansible/roles/infrastructure/tasks/removals.yml b/ansible/roles/infrastructure/tasks/removals.yml index 6a4a6b5..35b67fb 100644 --- a/ansible/roles/infrastructure/tasks/removals.yml +++ b/ansible/roles/infrastructure/tasks/removals.yml @@ -5,17 +5,16 @@ # vault -> bao (OpenBao, MPL-2.0) # # Ansible cannot remove what we simply stop declaring, so every host we have -# ever provisioned keeps terraform, vault AND the HashiCorp repo until we say -# otherwise. Leaving the repo behind is the worse half: it would go on serving -# BUSL packages and updates forever. +# ever provisioned keeps terraform and vault until we say otherwise. +# +# The HashiCorp repo, its keys and the Homebrew tap are NOT removed: Packer +# comes from them (the opt-in `packer` role), and hyperi-infra's image builds +# need it. # # Removing terraform WILL break tooling that still shells out to it -- notably # hyperi-infra, which invokes `terraform` directly. That is why this runs on # `--tags removals` alone: installing the IaC tools is not a request to delete # somebody's Terraform. -# -# apt refuses every operation while any source list is malformed, so a host with -# a broken hashicorp.list cannot run the removal that would clear it. - name: Remove Terraform and Vault (Ubuntu) ansible.builtin.apt: @@ -34,36 +33,6 @@ state: absent when: ansible_facts['distribution'] == 'Fedora' -# The repo itself. Both filenames are removed: deb822_repository writes -# .sources, and older revisions of this role used apt_repository, which wrote -# .list. A host provisioned before that migration carries the .list. -- name: Remove the HashiCorp APT repository (Ubuntu) - ansible.builtin.file: - path: "{{ item }}" - state: absent - loop: - - /etc/apt/sources.list.d/hashicorp.sources - - /etc/apt/sources.list.d/hashicorp.list - - /usr/share/keyrings/hashicorp-archive-keyring.asc - - /usr/share/keyrings/hashicorp-archive-keyring.gpg - register: infrastructure_hashicorp_repo_removed - when: ansible_facts['distribution'] == 'Ubuntu' - -- name: Refresh the APT cache after removing the repository - ansible.builtin.apt: - update_cache: true - when: - - ansible_facts['distribution'] == 'Ubuntu' - - infrastructure_hashicorp_repo_removed is changed - -- name: Remove the HashiCorp YUM repository (Fedora) - ansible.builtin.file: - path: /etc/yum.repos.d/hashicorp.repo - state: absent - when: ansible_facts['distribution'] == 'Fedora' - -# macOS. Untap only after the formulae are gone, or brew refuses. -# # NOT touched: `aws-vault`. It is the ByteNess aws-vault, nothing to do with # HashiCorp Vault despite the name, and it is still installed on purpose. - name: Remove Terraform and Vault (macOS) @@ -76,12 +45,3 @@ environment: "{{ homebrew_env }}" failed_when: false when: ansible_facts['distribution'] == 'MacOSX' - -- name: Remove the HashiCorp Homebrew tap (macOS) - community.general.homebrew_tap: - name: hashicorp/tap - state: absent - become: false - environment: "{{ homebrew_env }}" - failed_when: false - when: ansible_facts['distribution'] == 'MacOSX' diff --git a/ansible/roles/packer/README.md b/ansible/roles/packer/README.md new file mode 100644 index 0000000..167fd73 --- /dev/null +++ b/ansible/roles/packer/README.md @@ -0,0 +1,39 @@ +# packer + +HashiCorp Packer, from HashiCorp's own package repositories. **Opt-in** -- not in the default install, not in any persona, not in `contributor` or `soe`. + + ./install.sh --tags packer + +## Why its own role + +Packer is the one HashiCorp tool we install. HashiCorp moved its tools to BUSL in 2023; terraform and vault have open-source forks (OpenTofu, OpenBao) and the `infrastructure` role installs those instead. Packer has no fork, so it ships only to boxes that ask for it -- image builds in hyperi-infra are the usual reason. + +## What it touches + +| Platform | Repository | Key | Package | +|---|---|---|---| +| Ubuntu | `/etc/apt/sources.list.d/hashicorp.sources`, suite = the host's codename | `/usr/share/keyrings/hashicorp-archive-keyring.asc` | `packer` | +| Fedora | `/etc/yum.repos.d/hashicorp.repo` | `/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp` | `packer` | +| macOS | `hashicorp/tap` | - | `hashicorp/tap/packer` | + +On Ubuntu it also removes `/etc/apt/sources.list.d/hashicorp.list`. Older tooling (hyperi-infra included) wrote the repo that way, and apt refuses every operation while one URI is configured twice with different `Signed-By` keys. + +The Ubuntu key path is the one hyperi-infra uses, so a host it set up converges onto the same trusted file. + +## Key pinning + +The signing key is downloaded to `.unverified` and only copied to the trusted path when it holds exactly one primary key and that key's fingerprint is `packer_hashicorp_key_fingerprint`. The default is the Linux repository key HashiCorp publishes at https://www.hashicorp.com/en/trust/security: + + D55C 0D1A C78A 8D81 26CB 631C FC9C A96A CA02 6560 + +It replaced `798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701`, which expired on 2026-09-10. A host still trusting the old key gets the new one on the next run. + +A mismatch fails closed: the run removes the HashiCorp repo it would have used, leaves the previously trusted key alone, records a warning in the end-of-run report and carries on. Update the pin after checking the new fingerprint against HashiCorp's page. + +## Removals + +`--tags removals` takes terraform and vault away and leaves the HashiCorp repo, its keys and the tap in place, since this role installs from them. + +## Verifying + + packer version diff --git a/ansible/roles/packer/defaults/main.yml b/ansible/roles/packer/defaults/main.yml new file mode 100644 index 0000000..a4cc039 --- /dev/null +++ b/ansible/roles/packer/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# Packer role defaults + +# Primary key fingerprint HashiCorp publishes for its Linux package repos at +# https://www.hashicorp.com/en/trust/security, checked before the key is trusted. +# The same key signs apt.releases.hashicorp.com and rpm.releases.hashicorp.com. +packer_hashicorp_key_fingerprint: D55C0D1AC78A8D8126CB631CFC9CA96ACA026560 # gitleaks:allow -- public key fingerprint +packer_hashicorp_apt_key_url: https://apt.releases.hashicorp.com/gpg +packer_hashicorp_rpm_key_url: https://rpm.releases.hashicorp.com/gpg diff --git a/ansible/roles/packer/tasks/main.yml b/ansible/roles/packer/tasks/main.yml new file mode 100644 index 0000000..f2919a1 --- /dev/null +++ b/ansible/roles/packer/tasks/main.yml @@ -0,0 +1,195 @@ +--- +# Packer from HashiCorp's own repositories. +# +# Packer is the one HashiCorp tool this project installs: it is BUSL-licensed +# and has no open-source fork, so it is opt-in (`--tags packer`) and never part +# of a default run or a persona. terraform and vault stay replaced by OpenTofu +# and OpenBao in the infrastructure role. +# +# Optional: any failure lands in deploy_warnings and the run carries on. + +# ============================================================================ +# LINUX - HashiCorp's apt repo on Ubuntu, its dnf repo on Fedora +# ============================================================================ +- name: Install Packer from the HashiCorp repository (Linux) + vars: + # Check mode adds no repo, so there is nothing to install from yet. + packer_repo_pending: >- + {{ ansible_check_mode and ((packer_deb_repo | default({})) is changed + or (packer_rpm_repo | default({})) is changed) }} + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] + block: + # Runs before any apt call: older setups, hyperi-infra's included, write this + # .list for the same URI, and apt refuses every operation when the two name + # different Signed-By keys. + - name: Remove the legacy HashiCorp .list (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/hashicorp.list + state: absent + when: ansible_facts['distribution'] == 'Ubuntu' + + # A minimal Ubuntu ships gpgv but not gpg, which the fingerprint read needs, + # nor python3-debian, which deb822_repository needs. cache_valid_time, + # because `--tags packer` alone can land on a host whose apt lists were + # never fetched. + - name: Ensure gpg and python3-debian are present (Ubuntu) + ansible.builtin.apt: + name: [gpg, python3-debian] + state: present + update_cache: true + cache_valid_time: 3600 + when: ansible_facts['distribution'] == 'Ubuntu' + + - name: Ensure gpg is present for the fingerprint check (Fedora) + ansible.builtin.dnf: + name: gnupg2 + state: present + when: ansible_facts['distribution'] == 'Fedora' + + # Staged under a name nothing trusts and forced, so a rejected download is + # replaced on the next run instead of kept by an If-Modified-Since 304. + - name: Download the HashiCorp signing key + ansible.builtin.get_url: + url: >- + {{ packer_hashicorp_apt_key_url if ansible_facts['distribution'] == 'Ubuntu' + else packer_hashicorp_rpm_key_url }} + dest: "{{ packer_hashicorp_key_dest }}.unverified" + mode: '0644' + force: true + register: packer_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the HashiCorp signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ packer_hashicorp_key_dest }}.unverified"] + register: packer_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and packer_key_download is changed) + + # Fails closed: apt and rpm trust every key in the file, so a second key + # riding along with the pinned one would be trusted too. + - name: Verify the HashiCorp signing key fingerprint + ansible.builtin.assert: + that: + - packer_key_primaries | int == 1 + - packer_key_fpr == packer_hashicorp_key_fingerprint | upper | replace(' ', '') + fail_msg: >- + HashiCorp signing key holds {{ packer_key_primaries }} key(s), first + {{ packer_key_fpr or 'missing' }}; expected only {{ packer_hashicorp_key_fingerprint }} + quiet: true + when: not (ansible_check_mode and packer_key_download is changed) + vars: + packer_key_primaries: "{{ packer_key_read.stdout_lines | select('match', '^pub:') | list | length }}" + packer_key_fpr: >- + {{ (packer_key_read.stdout_lines | select('match', '^fpr:') | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified HashiCorp signing key + ansible.builtin.copy: + src: "{{ packer_hashicorp_key_dest }}.unverified" + dest: "{{ packer_hashicorp_key_dest }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and packer_key_download is changed) + + # HashiCorp publishes a suite per Ubuntu codename, 24.04 and 26.04 included. + - name: Add the HashiCorp APT repository (Ubuntu) + ansible.builtin.deb822_repository: + name: hashicorp + types: deb + uris: https://apt.releases.hashicorp.com + suites: "{{ ansible_facts['distribution_release'] }}" + components: main + signed_by: "{{ packer_hashicorp_key_dest }}" + state: present + register: packer_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' + + - name: Install Packer (Ubuntu) + ansible.builtin.apt: + name: packer + state: present + update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not packer_repo_pending + + - name: Add the HashiCorp repository (Fedora) + ansible.builtin.yum_repository: + name: hashicorp + description: HashiCorp Stable - $basearch + baseurl: https://rpm.releases.hashicorp.com/fedora/$releasever/$basearch/stable + enabled: true + gpgcheck: true + gpgkey: "file://{{ packer_hashicorp_key_dest }}" + register: packer_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' + + - name: Install Packer (Fedora) + ansible.builtin.dnf: + name: packer + state: present + when: + - ansible_facts['distribution'] == 'Fedora' + - not packer_repo_pending + + - name: Check that Packer runs (Linux) + ansible.builtin.command: + argv: [packer, version] + changed_when: false + when: not packer_repo_pending + + rescue: + # After a key rotation the repo no longer verifies against the trusted key, + # and the apt module fails every later cache refresh in the run on it. The + # previously trusted key stays. + - name: Remove the HashiCorp APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/hashicorp.sources + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' + + - name: Remove the HashiCorp repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: + name: hashicorp + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' + + - name: Record that Packer did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['Packer: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} + +# ============================================================================ +# MACOS - HashiCorp's tap; homebrew-core does not ship BUSL tools +# ============================================================================ +- name: Install Packer from the HashiCorp tap (macOS) + become: false + environment: "{{ packer_homebrew_env }}" + when: ansible_facts['distribution'] == 'MacOSX' + block: + - name: Tap hashicorp/tap (macOS) + community.general.homebrew_tap: + name: hashicorp/tap + state: present + + - name: Install Packer (macOS) + community.general.homebrew: + name: hashicorp/tap/packer + state: present + + rescue: + - name: Record that Packer did not install (macOS) + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['Packer: ' ~ (ansible_failed_result.msg | default('brew install failed'))] }} diff --git a/ansible/roles/packer/vars/main.yml b/ansible/roles/packer/vars/main.yml new file mode 100644 index 0000000..cbf2223 --- /dev/null +++ b/ansible/roles/packer/vars/main.yml @@ -0,0 +1,10 @@ +--- +# Homebrew lives outside the default PATH of a non-login Ansible shell. +packer_homebrew_env: + PATH: "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:{{ ansible_facts['env'].PATH }}" + +# Ubuntu keeps the key where hyperi-infra puts it, so a host it configured +# converges onto one trusted file. +packer_hashicorp_key_dest: >- + {{ '/usr/share/keyrings/hashicorp-archive-keyring.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp' }} diff --git a/docs/install-matrix.md b/docs/install-matrix.md index 519fcad..cc7e18a 100644 --- a/docs/install-matrix.md +++ b/docs/install-matrix.md @@ -97,6 +97,7 @@ flowchart TD | developer-typescript | `developer-typescript` | developer-node | opt-in | | developer-languages | `developer-languages` | all `developer-` | opt-in (meta) | | infrastructure | `infrastructure` | - | opt-in | +| packer | `packer` | - | opt-in, in no persona | | contributor | `contributor` | developer | opt-in | | soe | `soe` | contributor | opt-in | | soe-gui | `soe-gui` | astral, rdp-client | opt-in | @@ -333,6 +334,14 @@ from v4 that SDK is generated and carries no `cmd/` directory. The Linux build needs a Go toolchain (`--tags developer-go`); without one the run records a warning and continues. +### packer + +| Tool(s) | Platforms | Method | +|---|---|---| +| packer | all | vendor-repo (apt.releases.hashicorp.com, rpm.releases.hashicorp.com), signing key fingerprint-pinned / brew (`hashicorp/tap`) | + +The one HashiCorp tool installed: BUSL, with no open-source fork, so it is its own opt-in role rather than part of `infrastructure`. `--tags removals` takes terraform and vault and leaves this repo in place. + ### contributor (CI toolchain - what `hyperi-ci check` drives) `*` = blocking CI gate. diff --git a/install.sh b/install.sh index 095aebb..f045e11 100755 --- a/install.sh +++ b/install.sh @@ -182,6 +182,10 @@ Infrastructure (infrastructure): cloudflare cloudflare group: flarectl, wrangler (flarectl builds from source; Linux needs developer-go) +Packer (packer) - opt-in, in no persona: + packer HashiCorp Packer from HashiCorp's repo / tap (BUSL, + no open-source fork) + Contributor (contributor) - to work ON a HyperI product, no org policy: hyperi-ci hyperi-ci + semgrep, alint gitleaks Secret scanner From 77ae3c0d621bb560c6f46a20676a2a072df52c5c Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 7 Oct 2026 10:11:24 +1100 Subject: [PATCH 2/2] fix: heal and unhook stale hashicorp repos A host still trusting the superseded HashiCorp key fails every apt refresh, so the packer role could not get as far as replacing the key. The prerequisite install now drops the HashiCorp sources and retries when that refresh fails, and the repo is written back with the verified key further down. --tags removals unhooks the HashiCorp repo, its key and the tap again, but only where Packer is not installed. It runs before the terraform and vault removal, because that removal refreshes the apt cache too. The remediation scenario checks both branches, with a stand-in packer package on one host. Fedora now also checks the signature on HashiCorp's repo metadata. The molecule matrix converges packer on all four releases. --- README.md | 2 +- ansible/molecule/matrix/molecule.yml | 5 +- ansible/molecule/matrix/verify.yml | 1 + ansible/molecule/remediation/prepare.yml | 53 ++++++++++-- ansible/molecule/remediation/verify.yml | 31 +++++-- ansible/roles/infrastructure/tasks/main.yml | 3 +- .../roles/infrastructure/tasks/removals.yml | 84 ++++++++++++++++++- ansible/roles/packer/README.md | 10 +-- ansible/roles/packer/tasks/main.yml | 53 ++++++------ ansible/roles/packer/vars/main.yml | 3 +- docs/install-matrix.md | 2 +- 11 files changed, 196 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 19b5dc2..0878479 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ digest. Read that before changing a role or adding a tool. - `vscode-privacy` (off by default): strips the Copilot/AI upsell UI and the telemetry that stock VSCode ships enabled, across VSCode, VSCodium and Cursor. Enable with `-e vscode_privacy_enabled=true`. It merges one marked block into `settings.json` and never touches a comment or a key it does not manage, backs the file up before its first write, and `-e vscode_privacy_uninstall=true` takes only its own keys back out. Where you have set one of those keys yourself further down the file, yours wins and the run tells you which ones -- so it cannot look applied while changing nothing - Languages: Rust, Go, Python, C/C++, Node.js, TypeScript (the Astral suite -- uv, ruff, ty -- ships in the base, as does Node.js: it is core tooling that semantic-release and CI need) - `infrastructure`: OpenTofu + OpenBao (the OSS forks in place of terraform and vault), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) -- `packer` (off by default, and in no persona): HashiCorp Packer, the one HashiCorp tool we install, because it is BUSL with no open-source fork. Ubuntu and Fedora take HashiCorp's repo, with its signing key checked against the fingerprint HashiCorp publishes before it is trusted; macOS takes `hashicorp/tap`. `--tags removals` leaves that repo alone -- see [roles/packer/README.md](ansible/roles/packer/README.md) +- `packer` (off by default, and in no persona): HashiCorp Packer, the one HashiCorp tool we install, because it is BUSL with no open-source fork. Ubuntu and Fedora take HashiCorp's repo, with its signing key checked against the fingerprint HashiCorp publishes before it is trusted; macOS takes `hashicorp/tap`. `--tags removals` removes that repo only where Packer is not installed -- see [roles/packer/README.md](ansible/roles/packer/README.md) - `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change - `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar - `developer-ai` (off by default): the OpenAI Codex CLI as a second opinion alongside Claude Code rather than a replacement for it, plus OpenAI's Codex plugin FOR Claude Code, so `/codex:review` and `/codex:adversarial-review` are things Claude asks Codex for. The plugin is skipped -- with a warning naming the tag that fixes it -- unless claude, codex and a new enough node are all present for that user, because it installs happily without them and then throws on every invocation. Sign-in stays the person's: `codex login --device-auth` on a box with no browser diff --git a/ansible/molecule/matrix/molecule.yml b/ansible/molecule/matrix/molecule.yml index e406148..aadbc0f 100644 --- a/ansible/molecule/matrix/molecule.yml +++ b/ansible/molecule/matrix/molecule.yml @@ -5,7 +5,8 @@ # installs, which is why Fedora belongs here -- there are no deployed Fedora # clients to have drifted, but a fresh Fedora box must come up correctly. # -# Scoped to the CLI base (`repository`, `utilities`, `git`). Docker, snap and the +# Scoped to the CLI base (`repository`, `utilities`, `git`) plus `packer`, the +# opt-in role with its own vendor repo on every platform. Docker, snap and the # GNOME paths need a daemon or a session a container does not have, so widening # the tag set means solving that first -- privileged containers or systemd # images -- not just adding a tag. @@ -69,7 +70,7 @@ ansible: ansible_playbook: - --diff - --tags - - repository,utilities,git + - repository,utilities,git,packer playbooks: converge: converge.yml verify: verify.yml diff --git a/ansible/molecule/matrix/verify.yml b/ansible/molecule/matrix/verify.yml index 805f6cc..24a792e 100644 --- a/ansible/molecule/matrix/verify.yml +++ b/ansible/molecule/matrix/verify.yml @@ -26,6 +26,7 @@ - {name: git, cmd: git --version} - {name: tmux, cmd: tmux -V} - {name: age, cmd: age --version} + - {name: packer, cmd: packer version} loop_control: label: "{{ item.name }}" register: matrix_tools diff --git a/ansible/molecule/remediation/prepare.yml b/ansible/molecule/remediation/prepare.yml index a292766..29a7d01 100644 --- a/ansible/molecule/remediation/prepare.yml +++ b/ansible/molecule/remediation/prepare.yml @@ -288,8 +288,8 @@ # The Fedora yq superseded by dnf is NOT planted: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is planted above, in the superseded-by-a-package section. - # The HashiCorp repo serves Packer, so a `--tags removals` run must leave it - # and its keys in place while it takes terraform and vault away. + # The HashiCorp repo goes on a removals run unless Packer is installed, which + # the gosrc host fakes below so both branches are exercised. - name: Plant the HashiCorp apt repository (Ubuntu) ansible.builtin.copy: content: | @@ -306,9 +306,8 @@ # An older revision used apt_repository, which wrote .list, so a host # provisioned before that migration carries both. - # A parseable line with the same options as the .sources above, because apt - # refuses EVERY operation when a source list is malformed or one repo is - # configured with conflicting options. + # A parseable line, because apt refuses EVERY operation when any source list + # is malformed -- including the removal that is supposed to clear it. # `trusted=yes` keeps the fixture off the keyring. - name: Plant the legacy HashiCorp .list (Ubuntu) ansible.builtin.copy: @@ -331,6 +330,50 @@ - /usr/share/keyrings/hashicorp-archive-keyring.gpg when: ansible_facts['distribution'] == 'Ubuntu' + # An empty package named packer is all the removals check reads, and it + # keeps the fixture off HashiCorp's BUSL binary. + - name: Create the stand-in packer package tree (gosrc host) + ansible.builtin.file: + path: /root/packer-fixture/DEBIAN + state: directory + owner: root + group: root + mode: '0755' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Write the control file for a stand-in packer package (gosrc host) + ansible.builtin.copy: + content: | + Package: packer + Version: 0.0.0-fixture + Architecture: all + Maintainer: fixture + Description: Stand-in for HashiCorp Packer in the remediation fixture + dest: /root/packer-fixture/DEBIAN/control + owner: root + group: root + mode: '0644' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Build the stand-in packer package (gosrc host) + ansible.builtin.command: + argv: [dpkg-deb, --build, /root/packer-fixture, /root/packer-fixture.deb] + creates: /root/packer-fixture.deb + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Install the stand-in packer package (gosrc host) + ansible.builtin.apt: + deb: /root/packer-fixture.deb + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + - name: Install the DBeaver flatpak superseded by the vendor apt repo (Ubuntu) community.general.flatpak: name: io.dbeaver.DBeaverCommunity diff --git a/ansible/molecule/remediation/verify.yml b/ansible/molecule/remediation/verify.yml index 3d8c3c4..564c7f9 100644 --- a/ansible/molecule/remediation/verify.yml +++ b/ansible/molecule/remediation/verify.yml @@ -198,30 +198,49 @@ # The Fedora yq migration is not asserted here: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is covered in the superseded-by-a-package section above. - # Packer installs from the HashiCorp repo, so removing terraform and vault - # must leave the repo and its keys where they are. + # Without Packer the repo is unhooked, because one left trusting a superseded + # key fails every apt refresh. With Packer (the gosrc host) it stays. - name: Stat the HashiCorp repository artefacts (Ubuntu) ansible.builtin.stat: path: "{{ item }}" loop: - /etc/apt/sources.list.d/hashicorp.sources + - /etc/apt/sources.list.d/hashicorp.list - /usr/share/keyrings/hashicorp-archive-keyring.asc - /usr/share/keyrings/hashicorp-archive-keyring.gpg register: verify_hashicorp when: ansible_facts['distribution'] == 'Ubuntu' - - name: Assert the HashiCorp repository survived the removals + - name: Assert the HashiCorp repository was unhooked where Packer is absent + ansible.builtin.assert: + that: + - not item.stat.exists + fail_msg: >- + {{ item.item }} survived remediation on a host without Packer. A + HashiCorp repo whose key the host no longer trusts fails every apt + refresh, and nothing else here removes it. + success_msg: "{{ item.item }} removed" + loop: "{{ verify_hashicorp.results | default([]) }}" + loop_control: + label: "{{ item.item | default('skipped') }}" + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' not in inventory_hostname" + + - name: Assert the HashiCorp repository was kept where Packer is installed ansible.builtin.assert: that: - item.stat.exists fail_msg: >- - {{ item.item }} was removed. `--tags removals` takes terraform and - vault only; the repo and its keys are what Packer installs from. + {{ item.item }} was removed from a host with Packer installed, which + leaves Packer with no update source. success_msg: "{{ item.item }} kept" loop: "{{ verify_hashicorp.results | default([]) }}" loop_control: label: "{{ item.item | default('skipped') }}" - when: ansible_facts['distribution'] == 'Ubuntu' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" - name: Stat the retired vendor repository artefacts (Ubuntu) ansible.builtin.stat: diff --git a/ansible/roles/infrastructure/tasks/main.yml b/ansible/roles/infrastructure/tasks/main.yml index 27a5ad5..bfc54b8 100644 --- a/ansible/roles/infrastructure/tasks/main.yml +++ b/ansible/roles/infrastructure/tasks/main.yml @@ -7,8 +7,7 @@ # # `removals` ONLY, and never on a propagated tag. Installing the IaC tools is # not a request to delete someone's Terraform -- that is a remediation, asked -# for explicitly. `--tags infrastructure` and `--tags cloud` used to fire it -# too, which meant a routine IaC install silently took it away. +# for explicitly. - name: Remove the retired HashiCorp tools (opt-in) ansible.builtin.include_tasks: file: removals.yml diff --git a/ansible/roles/infrastructure/tasks/removals.yml b/ansible/roles/infrastructure/tasks/removals.yml index 35b67fb..7451e21 100644 --- a/ansible/roles/infrastructure/tasks/removals.yml +++ b/ansible/roles/infrastructure/tasks/removals.yml @@ -7,15 +7,70 @@ # Ansible cannot remove what we simply stop declaring, so every host we have # ever provisioned keeps terraform and vault until we say otherwise. # -# The HashiCorp repo, its keys and the Homebrew tap are NOT removed: Packer -# comes from them (the opt-in `packer` role), and hyperi-infra's image builds -# need it. +# The HashiCorp repo, its key and the Homebrew tap go only where Packer is not +# installed, since the opt-in `packer` role installs from them. A repo left +# behind with a key the host no longer trusts fails every apt refresh. # # Removing terraform WILL break tooling that still shells out to it -- notably # hyperi-infra, which invokes `terraform` directly. That is why this runs on # `--tags removals` alone: installing the IaC tools is not a request to delete # somebody's Terraform. +# Before the package removals, which refresh the apt cache and fail on a repo +# signed by a key the host no longer trusts. +- name: Check whether Packer is installed (Ubuntu) + ansible.builtin.command: + argv: [dpkg-query, --show, '--showformat=${Status}', packer] + register: infrastructure_packer_deb + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Ubuntu' + +# Both filenames: deb822_repository writes .sources, and older setups wrote .list. +- name: Remove the HashiCorp APT repository where Packer is not installed (Ubuntu) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/apt/sources.list.d/hashicorp.sources + - /etc/apt/sources.list.d/hashicorp.list + - /usr/share/keyrings/hashicorp-archive-keyring.asc + - /usr/share/keyrings/hashicorp-archive-keyring.asc.unverified + - /usr/share/keyrings/hashicorp-archive-keyring.gpg + register: infrastructure_hashicorp_repo_removed + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'install ok installed' not in infrastructure_packer_deb.stdout | default('')" + +- name: Refresh the APT cache after removing the repository + ansible.builtin.apt: + update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - infrastructure_hashicorp_repo_removed is changed + +- name: Check whether Packer is installed (Fedora) + ansible.builtin.command: + argv: [rpm, -q, packer] + register: infrastructure_packer_rpm + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Fedora' + +- name: Remove the HashiCorp YUM repository where Packer is not installed (Fedora) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/yum.repos.d/hashicorp.repo + - /etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp + - /etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp.unverified + when: + - ansible_facts['distribution'] == 'Fedora' + - infrastructure_packer_rpm.rc | default(0) != 0 + - name: Remove Terraform and Vault (Ubuntu) ansible.builtin.apt: name: @@ -45,3 +100,26 @@ environment: "{{ homebrew_env }}" failed_when: false when: ansible_facts['distribution'] == 'MacOSX' + +- name: Check whether Packer is installed (macOS) + ansible.builtin.command: + argv: [brew, list, hashicorp/tap/packer] + register: infrastructure_packer_brew + become: false + environment: "{{ homebrew_env }}" + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'MacOSX' + +# Untap only after the formulae are gone, or brew refuses. +- name: Remove the HashiCorp Homebrew tap where Packer is not installed (macOS) + community.general.homebrew_tap: + name: hashicorp/tap + state: absent + become: false + environment: "{{ homebrew_env }}" + failed_when: false + when: + - ansible_facts['distribution'] == 'MacOSX' + - infrastructure_packer_brew.rc | default(0) != 0 diff --git a/ansible/roles/packer/README.md b/ansible/roles/packer/README.md index 167fd73..67be9f4 100644 --- a/ansible/roles/packer/README.md +++ b/ansible/roles/packer/README.md @@ -6,7 +6,7 @@ HashiCorp Packer, from HashiCorp's own package repositories. **Opt-in** -- not i ## Why its own role -Packer is the one HashiCorp tool we install. HashiCorp moved its tools to BUSL in 2023; terraform and vault have open-source forks (OpenTofu, OpenBao) and the `infrastructure` role installs those instead. Packer has no fork, so it ships only to boxes that ask for it -- image builds in hyperi-infra are the usual reason. +Packer is the one HashiCorp tool we install. HashiCorp moved its tools to BUSL in 2023; terraform and vault have open-source forks (OpenTofu, OpenBao) and the `infrastructure` role installs those instead. Packer has no fork, so it ships only to boxes that ask for it -- machine image builds are the usual reason. ## What it touches @@ -16,9 +16,9 @@ Packer is the one HashiCorp tool we install. HashiCorp moved its tools to BUSL i | Fedora | `/etc/yum.repos.d/hashicorp.repo` | `/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp` | `packer` | | macOS | `hashicorp/tap` | - | `hashicorp/tap/packer` | -On Ubuntu it also removes `/etc/apt/sources.list.d/hashicorp.list`. Older tooling (hyperi-infra included) wrote the repo that way, and apt refuses every operation while one URI is configured twice with different `Signed-By` keys. +On Ubuntu it also removes `/etc/apt/sources.list.d/hashicorp.list`, the older format of the same repo, which apt would otherwise read as a duplicate source. -The Ubuntu key path is the one hyperi-infra uses, so a host it set up converges onto the same trusted file. +Other tooling also writes the Ubuntu key path, so a host it set up converges onto one trusted file. A tool that writes an unverified key there replaces the pinned one until this role runs again. ## Key pinning @@ -26,13 +26,13 @@ The signing key is downloaded to `.unverified` and only copied to the trust D55C 0D1A C78A 8D81 26CB 631C FC9C A96A CA02 6560 -It replaced `798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701`, which expired on 2026-09-10. A host still trusting the old key gets the new one on the next run. +It replaced `798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701`, which HashiCorp lists as superseded and which signed the repos until 2026-09-10. A host still trusting the old key gets the new one on the next run, even when its apt cache can no longer refresh against the old key. A mismatch fails closed: the run removes the HashiCorp repo it would have used, leaves the previously trusted key alone, records a warning in the end-of-run report and carries on. Update the pin after checking the new fingerprint against HashiCorp's page. ## Removals -`--tags removals` takes terraform and vault away and leaves the HashiCorp repo, its keys and the tap in place, since this role installs from them. +`--tags removals` takes terraform and vault away. It removes the HashiCorp repo, its key and the tap only where Packer is not installed, since this role installs from them. ## Verifying diff --git a/ansible/roles/packer/tasks/main.yml b/ansible/roles/packer/tasks/main.yml index f2919a1..d3f9b1e 100644 --- a/ansible/roles/packer/tasks/main.yml +++ b/ansible/roles/packer/tasks/main.yml @@ -1,12 +1,5 @@ --- -# Packer from HashiCorp's own repositories. -# -# Packer is the one HashiCorp tool this project installs: it is BUSL-licensed -# and has no open-source fork, so it is opt-in (`--tags packer`) and never part -# of a default run or a persona. terraform and vault stay replaced by OpenTofu -# and OpenBao in the infrastructure role. -# -# Optional: any failure lands in deploy_warnings and the run carries on. +# Packer is BUSL with no open-source fork, so it installs only on `--tags packer`, and a failure lands in deploy_warnings rather than ending the run. # ============================================================================ # LINUX - HashiCorp's apt repo on Ubuntu, its dnf repo on Fedora @@ -19,26 +12,39 @@ or (packer_rpm_repo | default({})) is changed) }} when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] block: - # Runs before any apt call: older setups, hyperi-infra's included, write this - # .list for the same URI, and apt refuses every operation when the two name - # different Signed-By keys. + # Runs before any apt call, so apt does not read this repo twice as a duplicate source. - name: Remove the legacy HashiCorp .list (Ubuntu) ansible.builtin.file: path: /etc/apt/sources.list.d/hashicorp.list state: absent when: ansible_facts['distribution'] == 'Ubuntu' - # A minimal Ubuntu ships gpgv but not gpg, which the fingerprint read needs, - # nor python3-debian, which deb822_repository needs. cache_valid_time, - # because `--tags packer` alone can land on a host whose apt lists were - # never fetched. - - name: Ensure gpg and python3-debian are present (Ubuntu) - ansible.builtin.apt: - name: [gpg, python3-debian] - state: present - update_cache: true - cache_valid_time: 3600 + # A minimal Ubuntu lacks gpg and python3-debian, which the fingerprint read and deb822_repository need. + - name: Install the key-check prerequisites (Ubuntu) when: ansible_facts['distribution'] == 'Ubuntu' + block: + - name: Ensure gpg and python3-debian are present (Ubuntu) + ansible.builtin.apt: + name: [gpg, python3-debian] + state: present + update_cache: true + cache_valid_time: 3600 + + rescue: + # A HashiCorp source signed by a key the host no longer trusts fails every cache refresh, and the verified repo is written again below. + - name: Remove the HashiCorp sources apt cannot verify (Ubuntu) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/apt/sources.list.d/hashicorp.sources + - /etc/apt/sources.list.d/hashicorp.list + + - name: Retry gpg and python3-debian without the HashiCorp sources (Ubuntu) + ansible.builtin.apt: + name: [gpg, python3-debian] + state: present + update_cache: true - name: Ensure gpg is present for the fingerprint check (Fedora) ansible.builtin.dnf: @@ -123,6 +129,7 @@ baseurl: https://rpm.releases.hashicorp.com/fedora/$releasever/$basearch/stable enabled: true gpgcheck: true + repo_gpgcheck: true gpgkey: "file://{{ packer_hashicorp_key_dest }}" register: packer_rpm_repo when: ansible_facts['distribution'] == 'Fedora' @@ -142,9 +149,7 @@ when: not packer_repo_pending rescue: - # After a key rotation the repo no longer verifies against the trusted key, - # and the apt module fails every later cache refresh in the run on it. The - # previously trusted key stays. + # A repo the trusted key cannot verify fails every later cache refresh in the run, so it goes and the trusted key stays. - name: Remove the HashiCorp APT repository after a key mismatch (Ubuntu) ansible.builtin.file: path: /etc/apt/sources.list.d/hashicorp.sources diff --git a/ansible/roles/packer/vars/main.yml b/ansible/roles/packer/vars/main.yml index cbf2223..a1f7f02 100644 --- a/ansible/roles/packer/vars/main.yml +++ b/ansible/roles/packer/vars/main.yml @@ -3,8 +3,7 @@ packer_homebrew_env: PATH: "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:{{ ansible_facts['env'].PATH }}" -# Ubuntu keeps the key where hyperi-infra puts it, so a host it configured -# converges onto one trusted file. +# Ubuntu shares this key path with other tooling, so a host it configured converges onto one trusted file. packer_hashicorp_key_dest: >- {{ '/usr/share/keyrings/hashicorp-archive-keyring.asc' if ansible_facts['distribution'] == 'Ubuntu' else '/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp' }} diff --git a/docs/install-matrix.md b/docs/install-matrix.md index cc7e18a..fda0f33 100644 --- a/docs/install-matrix.md +++ b/docs/install-matrix.md @@ -340,7 +340,7 @@ warning and continues. |---|---|---| | packer | all | vendor-repo (apt.releases.hashicorp.com, rpm.releases.hashicorp.com), signing key fingerprint-pinned / brew (`hashicorp/tap`) | -The one HashiCorp tool installed: BUSL, with no open-source fork, so it is its own opt-in role rather than part of `infrastructure`. `--tags removals` takes terraform and vault and leaves this repo in place. +The one HashiCorp tool installed: BUSL, with no open-source fork, so it is its own opt-in role rather than part of `infrastructure`. `--tags removals` takes terraform and vault, and removes this repo only where Packer is not installed. ### contributor (CI toolchain - what `hyperi-ci check` drives)