diff --git a/setup/provenance/README.md b/setup/provenance/README.md index b072006..0460ac9 100644 --- a/setup/provenance/README.md +++ b/setup/provenance/README.md @@ -159,8 +159,12 @@ installed. Its effective `terra` configuration reported `gpgcheck = 1`, `pkg_gpgcheck = 1`, and `repo_gpgcheck = 1`; no GPG-bypass option was used. The package's own `/etc/yum.repos.d/terra.repo` uses its Terra metalink and `RPM-GPG-KEY-terra44`. That differs from Panama's deliberately staged local -key/base-URL file in `install-packages`, which replaces the release-generated -file only after this verified bootstrap step. +key and base URL. `install-packages` applies those after this verified +bootstrap step as `/etc/dnf/repos.override.d/80-panama-terra.repo`, which dnf +lays over the package's file. The pin does not edit `terra.repo` itself, +because terra-release replaces that file on every update. Release 44-11 did +so on 2026-10-28, shipping `repo_gpgcheck=0`, and every Panama machine +stopped installing packages until the pin moved to the override directory. Although the command runner returned after 30 seconds while DNF was still loading metadata, Podman's retained event log records the exact command's diff --git a/setup/scripts/install-packages b/setup/scripts/install-packages index be28697..0884224 100755 --- a/setup/scripts/install-packages +++ b/setup/scripts/install-packages @@ -729,7 +729,8 @@ _publish_repository_pair() { # Reads the gpgkey of the single enabled Terra identity out of dnf's effective # configuration, applying the structural safety rules either way: one enabled -# identity, named terra, with every signature check turned on. +# identity, named terra, with package signature checks turned on (and, in the +# pinned form, repository metadata checks too). # # With require_pinned set it additionally demands Panama's own reviewed baseurl # and key path. Without it, the answer is just "what trust root is this machine @@ -772,7 +773,11 @@ _effective_terra_key() { bad = 1 } if (values["gpgcheck"] != "1" || values["pkg_gpgcheck"] != "1" \ - || values["repo_gpgcheck"] != "1" || values["gpgkey"] == "") bad = 1 + || values["gpgkey"] == "") bad = 1 + # Metadata signing is part of the pin, not of what adoption accepts: + # terra-release ships repo_gpgcheck=0, and the override adoption + # writes turns it back on. + if (require_pinned != "" && values["repo_gpgcheck"] != "1") bad = 1 trusted_key = values["gpgkey"] } BEGIN { @@ -864,10 +869,12 @@ _terra_effective_status() { # # The trust root is the signing key, not the URL it is served from. A machine # that installed Terra the way Terra documents has terra-release's own repo -# file: a metalink instead of the reviewed baseurl, and the key at -# RPM-GPG-KEY-terra44 rather than Panama's renamed copy. Every signature check -# is already on, and that key is the same fingerprint this repository reviewed -# and pinned. Cosmetics, in other words -- not a compromised trust root. +# file: a metalink instead of the reviewed baseurl, the key at +# RPM-GPG-KEY-terra44 rather than Panama's renamed copy, and (since +# terra-release 44-11) repo_gpgcheck=0. Package signature checks are on, and +# that key is the same fingerprint this repository reviewed and pinned. The pin +# adoption writes restores metadata checking, so none of that is a compromised +# trust root. # # Refusing it outright built a gate with no door. install_terra_repository # declined to touch a machine terra-release had already reached, so an ordinary @@ -1002,33 +1009,51 @@ install_rpmfusion_repositories() { # trust root rolls back. The effective state is re-read afterwards, because the # only acceptable proof that adoption worked is the check that judged it. # -# terra-release owns /etc/yum.repos.d/terra.repo, so a later update to that -# package restores the stock file. That is fine and deliberate: the next run -# adopts it again, which is why adoption has to be repeatable rather than a -# one-time migration. +# Panama's Terra pin, as a dnf repos.override.d drop-in on top of whatever +# /etc/yum.repos.d/terra.repo says. terra-release owns that file and replaces +# it on every update (an edited copy is set aside as terra.repo.rpmsave), so a +# pin written there lasted only until the next terra-release release. Nothing +# owns the override directory, and its values win over the repo file's. +TERRA_OVERRIDE=/etc/dnf/repos.override.d/80-panama-terra.repo + +_write_terra_pin() { + printf '%s\n' \ + '[terra]' \ + 'name=Panama reviewed Terra 44' \ + "baseurl=${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ + 'metalink=' \ + 'mirrorlist=' \ + 'enabled=1' \ + 'gpgcheck=1' \ + 'repo_gpgcheck=1' \ + 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' > "$1" + chmod 0600 "$1" +} + +_ensure_override_dir() { + [[ -d "$PANAMA_SYSTEM_ETC/dnf/repos.override.d" ]] && return 0 + sudo mkdir -p -- /etc/dnf/repos.override.d +} + +# A stock terra-release file (or one an older Panama rewrote) gets the pin laid +# over it. Adoption stays repeatable: if the override is ever removed, the next +# run puts it back. adopt_terra_repository() { local work staged_key staged_repo status=0 effective_status=0 work="$(mktemp -d)" || return 1 chmod 0700 "$work" staged_key="$work/terra44.asc" - staged_repo="$work/terra.repo" + staged_repo="$work/80-panama-terra.repo" if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/terra44.asc" "$staged_key" \ TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F; then rm -rf -- "$work" return 1 fi - printf '%s\n' \ - '[terra]' \ - 'name=Panama reviewed Terra 44' \ - "baseurl=${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ - 'enabled=1' \ - 'gpgcheck=1' \ - 'repo_gpgcheck=1' \ - 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' > "$staged_repo" - chmod 0600 "$staged_repo" - _publish_repository_pair \ + _write_terra_pin "$staged_repo" + _ensure_override_dir || status=$? + (( status == 0 )) && { _publish_repository_pair \ "$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \ - "$staged_repo" /etc/yum.repos.d/terra.repo || status=$? + "$staged_repo" "$TERRA_OVERRIDE" || status=$?; } if (( status == 0 )); then _terra_effective_status || effective_status=$? (( effective_status == 0 )) || status="$TERRA_TRUST_FAILURE_STATUS" @@ -1064,7 +1089,7 @@ install_terra_repository() { work="$(mktemp -d)" || return 1 chmod 0700 "$work" staged_key="$work/terra44.asc" - staged_repo="$work/terra.repo" + staged_repo="$work/80-panama-terra.repo" if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/terra44.asc" "$staged_key" \ TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F; then rm -rf -- "$work" @@ -1074,15 +1099,7 @@ install_terra_repository() { rm -rf -- "$work" return 1 } - printf '%s\n' \ - '[terra]' \ - 'name=Panama reviewed Terra 44' \ - "baseurl=${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ - 'enabled=1' \ - 'gpgcheck=1' \ - 'repo_gpgcheck=1' \ - 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' > "$staged_repo" - chmod 0600 "$staged_repo" + _write_terra_pin "$staged_repo" status=0 sudo dnf install -y \ --repofrompath "terra,${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ @@ -1091,7 +1108,10 @@ install_terra_repository() { --setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \ terra-release || status=$? if (( status == 0 )); then - sudo install -m 0644 "$staged_repo" /etc/yum.repos.d/terra.repo || status=$? + _ensure_override_dir || status=$? + fi + if (( status == 0 )); then + sudo install -m 0644 "$staged_repo" "$TERRA_OVERRIDE" || status=$? fi if (( status == 0 )); then effective_status=0 diff --git a/tests/setup/package-provenance-contract b/tests/setup/package-provenance-contract index 853affd..e6ce825 100755 --- a/tests/setup/package-provenance-contract +++ b/tests/setup/package-provenance-contract @@ -901,17 +901,26 @@ if [[ -n "$query" ]]; then wrong-url) mode=override-url ;; stock|stock-wrong-key) mode=stock ;; esac - # Adoption rewrites the repository file. Once it is the pinned form the - # dump has to say so, or the re-verification adoption performs on itself - # could never pass. - if [[ "$mode" == stock && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \ - && grep -q '^gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama$' \ - "$STUB_ETC/yum.repos.d/terra.repo"; then + # Adoption and a fresh install write the pin as a repos.override.d + # drop-in, which dnf lays over the repository file. Once it is there + # the dump has to say so, or the re-verification each performs on + # itself could never pass. + override="$STUB_ETC/dnf/repos.override.d/80-panama-terra.repo" + pinned_override=0 + if [[ -f "$override" ]] \ + && grep -q '^gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama$' "$override" \ + && grep -q '^baseurl=https://repos.fyralabs.com/terra44$' "$override" \ + && grep -qx 'metalink=' "$override"; then + pinned_override=1 + fi + if [[ "$mode" == stock && "$pinned_override" == 1 ]]; then mode=trusted fi if [[ "$mode" == auto && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \ && grep -q '^baseurl=https://repos.fyralabs.com/terra44$' "$STUB_ETC/yum.repos.d/terra.repo"; then mode=trusted + elif [[ "$mode" == auto && "$pinned_override" == 1 ]]; then + mode=trusted elif [[ "$mode" == auto ]]; then mode=absent fi @@ -945,7 +954,7 @@ if [[ -n "$query" ]]; then printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n' printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n' printf 'metalink = https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=x86_64\n' - printf 'mirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n' + printf 'mirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 0\n' ;; override-url) printf '======== "terra" repository configuration: ========\n' @@ -1150,7 +1159,7 @@ run_installer_function() { cp "$installer_fixture/setup/provenance/keys/flathub.asc" \ "$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44" fi - printf '[terra]\nmetalink=https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n' \ + printf '[terra]\nmetalink=https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n' \ > "$case_root/etc/yum.repos.d/terra.repo" ;; wrong-key) @@ -1663,7 +1672,8 @@ rpm:query:terra-release gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F sudo:install:terra44.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama sudo:dnf install -y --repofrompath terra,https://repos.fyralabs.com/terra44 --setopt=terra.pkg_gpgcheck=1 --setopt=terra.repo_gpgcheck=1 --setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama terra-release -sudo:install:terra.repo:/etc/yum.repos.d/terra.repo +sudo:mkdir -p -- /etc/dnf/repos.override.d +sudo:install:80-panama-terra.repo:/etc/dnf/repos.override.d/80-panama-terra.repo dnf:dump-all:locale=C gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F @@ -1672,10 +1682,12 @@ EXPECTED cmp -s "$installer_fixture/setup/provenance/keys/terra44.asc" \ "$test_tmp/cases/terra/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama" \ || fail 'Terra privileged install did not preserve the fully staged reviewed key' -assert_file_bytes "$test_tmp/cases/terra/etc/yum.repos.d/terra.repo" "$(cat <<'EXPECTED' +assert_file_bytes "$test_tmp/cases/terra/etc/dnf/repos.override.d/80-panama-terra.repo" "$(cat <<'EXPECTED' [terra] name=Panama reviewed Terra 44 baseurl=https://repos.fyralabs.com/terra44 +metalink= +mirrorlist= enabled=1 gpgcheck=1 repo_gpgcheck=1 @@ -1804,9 +1816,12 @@ for mode in nogpg wrong-url wrong-key absent; do done # A machine that installed Terra the way Terra documents it. The repository file -# is terra-release's own -- a metalink, and the key at its stock path -- so it is -# not Panama's pinned form, but it IS the fingerprint this repository reviewed, -# with every signature check on. That is an adoption, not a compromise. +# is terra-release's own -- a metalink, the key at its stock path, and +# repo_gpgcheck=0 as terra-release 44-11 ships it -- so it is not Panama's +# pinned form, but it IS the fingerprint this repository reviewed, with package +# signature checks on. That is an adoption, not a compromise. The pin goes in a +# repos.override.d drop-in, because terra-release replaces its own repo file on +# every update and a pin written there did not survive the next one. # # Refusing it was a gate with no door: the ordinary Fedora desktop could never # reach the pinned state, and status 78 then stopped every stage of every run, @@ -1818,13 +1833,17 @@ STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \ reset_installer_fixture STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \ expect_success run_installer_function terra-stock-adopt install_terra_repository -terra_adopted="$test_tmp/cases/terra-stock-adopt/etc/yum.repos.d/terra.repo" +terra_adopted="$test_tmp/cases/terra-stock-adopt/etc/dnf/repos.override.d/80-panama-terra.repo" grep -qx 'baseurl=https://repos.fyralabs.com/terra44' "$terra_adopted" \ || fail 'adoption left Terra off the reviewed baseurl' grep -qx 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' "$terra_adopted" \ || fail 'adoption left Terra off the reviewed key path' -grep -q 'metalink' "$terra_adopted" \ - && fail 'adoption kept the metalink it was supposed to replace' +grep -qx 'metalink=' "$terra_adopted" \ + || fail 'adoption did not clear the metalink it was supposed to replace' +grep -qx 'repo_gpgcheck=1' "$terra_adopted" \ + || fail 'adoption did not turn repository metadata checks back on' +grep -q 'RPM-GPG-KEY-terra44$' "$test_tmp/cases/terra-stock-adopt/etc/yum.repos.d/terra.repo" \ + || fail "adoption rewrote terra-release's own repository file" [[ "$(<"$test_tmp/cases/terra-stock-adopt/commands.log")" != *'dnf:install'* ]] \ || fail 'adoption opened a DNF transaction it does not need'