Skip to content

필사 모드: Air-Gap Transfer Procedure and Integrity — Building a Bundle That Still Installs the Same Way Six Months Later

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Opening — why signature verification matters more in an air-gapped network

On a connected network, if something looks wrong you just download it again. If the hash does not match you compare against the original; if the signature looks off you ask the distributor. The thing you compare against is always within reach.

An air-gapped network breaks that premise. The moment the medium goes inside, the way to compare against the original is gone. Six months later, when someone asks "where did this rpm come from", if the evidence that answers the question was not carried in on the same medium, you can never answer it.

So in an air-gapped network, integrity verification is an operational requirement before it is a security requirement. This post is about putting that evidence inside the bundle.

What goes on the medium

A transfer unit contains more than rpm files. At minimum, these six things.

  1. The rpm file set — what you built in part 2
  2. repodata — the repository metadata generated in part 3
  3. The GPG public key — required to verify signatures on the inside
  4. The manifest — what is inside, and from which point in time
  5. The checksum file — separate from the manifest, for checking the integrity of the medium
  6. The install procedure — what to run, and in what order

Leave out item 3 and gpgcheck=1 blocks the very first install on the inside. Leave out item 4 and reproduction becomes impossible. In practice these two are the ones most often forgotten.

Verify the signatures

An rpm signature check verifies the digests and the signatures of a package together. The current upstream man pages put this function in rpmkeys and classify rpm's -K, --checksig, and --import as "Obsolete compatibility aliases". Both spellings work on RHEL 8, 9, and 10, but for a new procedure document you are better off using the rpmkeys form.

# Signature verification only means something once the key has been imported
sudo rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

# Check the signatures and digests of the entire bundle
rpmkeys --checksig /var/tmp/airgap-bundle/rpms/*.rpm

# Show only the failures
rpmkeys --checksig /var/tmp/airgap-bundle/rpms/*.rpm | grep -v 'digests signatures OK'

The rpmkeys man page defines -K, --checksig as "Verify the digests and signatures contained in PACKAGE_FILE to ensure the integrity and origin of the package". The second clause is the point. The digest guarantees integrity, the signature guarantees origin. They answer different questions, and you need both.

If you run the check without importing the key, the digest passes and the signature comes back as unverifiable. Reading that state as a "pass" is a common mistake, so before checking, use rpmkeys --list to confirm the key is actually imported.

You can also filter at the download stage. dnf reposync's -g, --gpgcheck from part 2 is documented as "Remove packages that fail GPG signature checking after downloading. Exit code is 1 if at least one package was removed", so with that option a package with a broken signature never enters the bundle in the first place, and the exit code tells you it happened.

A reproducible bundle — building the manifest

This is the heart of this post.

A bundle being reproducible means that with the same manifest in hand you can get the same install result six months later. For that, the manifest needs at least five things.

  • The complete NEVRA — name, epoch, version, release, architecture
  • A per-package checksum — SHA-256
  • The repository snapshot date — when it was downloaded
  • The release version used — what was passed to --releasever
  • The source repository ID — which repository it came from

There is a reason to go out of your way to include the epoch. The RPM tags reference defines Epoch as "Package epoch (optional). An absent epoch is equal to epoch value 0", and the file name does not carry the epoch. So a list built from file names alone cannot tell two packages with different epochs apart. Since the epoch takes the highest precedence in version comparison, that difference is both quiet and fatal.

You extract the manifest in a single pass with an rpm query.

#!/usr/bin/env bash
# make-manifest.sh — generate a reproducible manifest from a bundle directory
set -euo pipefail

BUNDLE_DIR="${1:?usage: make-manifest.sh <bundle-dir>}"
RELEASEVER="${2:?usage: make-manifest.sh <bundle-dir> <releasever>}"
SNAPSHOT_DATE="$(date -u +%Y-%m-%d)"

OUT="${BUNDLE_DIR}/MANIFEST.tsv"

{
  echo "# bundle_id: rhel-airgap-${SNAPSHOT_DATE}"
  echo "# snapshot_date: ${SNAPSHOT_DATE}"
  echo "# releasever: ${RELEASEVER}"
  echo "# source_repos: rhel-9-for-x86_64-baseos-rpms,rhel-9-for-x86_64-appstream-rpms"
  echo "# generated_by: $(rpm --version)"
  printf 'NAME\tEPOCH\tVERSION\tRELEASE\tARCH\tSHA256\tFILE\n'
} > "${OUT}"

for f in "${BUNDLE_DIR}"/rpms/*.rpm; do
  # For a package with no epoch, the ternary fills in 0
  nevra="$(rpm -qp --queryformat \
    '%{NAME}\t%|EPOCH?{%{EPOCH}}:{0}|\t%{VERSION}\t%{RELEASE}\t%{ARCH}' \
    "$f" 2>/dev/null)"
  sum="$(sha256sum "$f" | cut -d' ' -f1)"
  printf '%s\t%s\t%s\n' "${nevra}" "${sum}" "$(basename "$f")"
done >> "${OUT}"

echo "wrote ${OUT} ($(grep -cv '^#' "${OUT}") lines)"

The rpm man page defines -p, --package as "Query an (uninstalled) package PACKAGE_FILE" and --queryformat as "Output format of each queried package, as described by rpm-queryformat(7)". The ternary notation comes from the rpm query format documentation and specifies the value to use when a tag is absent. You can use it exactly as-is to fill in 0 for packages whose epoch is empty.

The resulting file looks like this.

# bundle_id: rhel-airgap-2026-08-15
# snapshot_date: 2026-08-15
# releasever: 9.4
# source_repos: rhel-9-for-x86_64-baseos-rpms,rhel-9-for-x86_64-appstream-rpms
# generated_by: RPM version 4.16.1.3
NAME	EPOCH	VERSION	RELEASE	ARCH	SHA256	FILE
httpd	0	2.4.57	11.el9_4	x86_64	3f1c...	httpd-2.4.57-11.el9_4.x86_64.rpm
mod_ssl	1	2.4.57	11.el9_4	x86_64	9ab2...	mod_ssl-2.4.57-11.el9_4.x86_64.rpm

Look at the epoch on the mod_ssl line: it is 1. That number appears nowhere in the file name.

Compare after transfer

Creating the manifest is not where the job ends. You have to compare against it right after the bundle arrives on the inside.

#!/usr/bin/env bash
# verify-bundle.sh — compare the manifest against the actual bundle
set -euo pipefail

BUNDLE_DIR="${1:?usage: verify-bundle.sh <bundle-dir>}"
MANIFEST="${BUNDLE_DIR}/MANIFEST.tsv"
ERR=0

echo "== bundle metadata"
grep '^#' "${MANIFEST}"

echo "== checksum comparison"
while IFS=$'\t' read -r name epoch version release arch sha file; do
  path="${BUNDLE_DIR}/rpms/${file}"
  if [ ! -f "${path}" ]; then
    echo "MISSING ${file}"; ERR=$((ERR+1)); continue
  fi
  actual="$(sha256sum "${path}" | cut -d' ' -f1)"
  if [ "${actual}" != "${sha}" ]; then
    echo "MISMATCH ${file}"; ERR=$((ERR+1))
  fi
done < <(grep -v '^#' "${MANIFEST}" | tail -n +2)

echo "== files present but not in the manifest"
comm -13 \
  <(grep -v '^#' "${MANIFEST}" | tail -n +2 | cut -f7 | sort) \
  <(cd "${BUNDLE_DIR}/rpms" && ls -1 *.rpm | sort)

echo "== signature verification"
rpmkeys --checksig "${BUNDLE_DIR}"/rpms/*.rpm \
  | grep -v 'digests signatures OK' || true

echo "== result: ${ERR} error(s)"
exit "${ERR}"

There is a reason the "files not in the manifest" check is in there. A missing file shows up quickly as an install failure, but a file that sits on the medium without being on the list slips past unnoticed. From the point of view of a transfer review board, that one is the bigger problem.

Shipping the checksum file on the medium also deserves a word. A checksum stored only on the same medium changes right along with it if the whole medium is swapped. The real guarantee comes from the signature. The checksum is for detecting corruption in transit; the guarantee of origin is what the GPG signature provides. That is why the script above checks both.

What to record in the chain of custody

Audit response and reproduction are solved by the same record. This much per bundle is enough.

ItemExampleWhy it is needed
Bundle IDrhel-airgap-2026-08-15The key that ties a server to a bundle
Snapshot date2026-08-15Which point in time the content is from
releasever9.4The pinned minor version (part 5)
Source repository IDrhel-9-for-x86_64-baseos-rpmsThe input when you rebuild it
Creation commanddnf download --installroot=... --releasever=9.4 ...The heart of reproduction
Medium hashSHA-256 of the medium imageIntegrity at the level of the medium
Transfer date and ownerAudit requirement
Target serversTracking the blast radius

The row telling you to record the whole creation command is the important one. When you have to rebuild the same bundle six months later, one changed option produces a different result. Write it into a file instead of relying on human memory. Put that information into the bundle as something like a PROVENANCE.txt file and the medium becomes self-contained.

Redistributing Red Hat content without a subscription may violate your agreement, so check your organization's license terms first. Keeping the source repository ID in the chain-of-custody record also gives you the evidence you need later when you have to confirm the scope of that license.

Closing — the medium has to stand on its own

There is a single principle in this post. With nothing but the medium in your hand, you should be able to tell what is inside it, where it came from, and how to build it again.

A USB stick holding nothing but rpm files tells you nothing six months later. Add the manifest, the public key, and the creation command, and that medium turns into its own documentation. The extra cost is two text files.

The commands and options were verified against the official documentation on 2026-08-15. They vary by RHEL version, so re-check against the documentation for the version you are running.

Try it yourself

Previous / next in the series

References

현재 단락 (1/109)

On a connected network, if something looks wrong you just download it again. If the hash does not ma...

작성 글자: 0원문 글자: 9,461작성 단락: 0/109