TT Lab
Get started
Learn Learning paths Courses

RHEL-Family Administration

Six Things to Put on the Media

Continue in TT Lab

In one line

An import medium must not hold only rpms. It needs all six: rpm, repodata, GPG key, manifest, checksums, and an install procedure document. The ones most often missing in practice are the GPG key and the manifest.

Why this exists

A download command that succeeded on the connected network does not guarantee the same result on the air-gapped network if the target's version, architecture, or already-installed state differs. A missing dependency produces a re-import, and without a key to verify the origin or an approved list, you cannot prove permission to install even if the files are there. So a bundle must be not a collection of packages but a distribution unit that includes the reproduction conditions and the grounds for verification.

How it works

Downloading on the outside

There are two branches.

Moving the resolved result

dnf download --resolve --alldeps --destdir=/srv/bundle/rpms httpd mod_ssl

If you use only --resolve, it downloads only what this machine lacks. For the purpose of building an air-gapped bundle, this is almost always the wrong result.

But even --alldeps alone is not enough. Dependency resolution itself is still done on the premise of this system's state. If the connected-side machine is 9.4 and the target is 9.2, the result can differ. The standard is to use an empty root.

mkdir -p /var/tmp/airgap-root
dnf download --installroot=/var/tmp/airgap-root --releasever=9.4 \
  --setopt=reposdir=/etc/yum.repos.d --resolve --alldeps \
  --destdir=/srv/bundle/rpms httpd mod_ssl

You must specify --releasever together. The warning in the documentation: "If you do not use --releasever when creating an installroot, the $releasever value comes from the rpmdb inside the installroot, and at creation time it is empty, so the transaction fails."

Moving the whole repository

dnf reposync --repoid=rhel-9-for-x86_64-baseos-rpms \
  --download-path=/srv/sync --download-metadata --gpgcheck \
  --newest-only --arch=x86_64 --arch=noarch --remote-time

Main options: --download-metadata (download the metadata too), --gpgcheck (delete packages that fail signature verification; if even one is deleted, the exit code is 1), --newest-only (only the newest per repository), --delete (delete what is not on the remote), --norepopath (do not attach the repo name to the path).

Trap: if you use --newest-only and --download-metadata together, only the newest packages are downloaded, but the metadata keeps information about the earlier packages. If you request a particular old version on the inside, you end up in a state where it is in the metadata but the file is missing.

Manifest

The five elements a manifest must hold: the complete NEVRA (Name, Epoch, Version, Release, Architecture), the SHA-256 per package, the snapshot date, the releasever used, and the source repository ID.

rpm -qp --queryformat '%{NAME}\t%|EPOCH?{%{EPOCH}}:{0}|\t%{VERSION}\t%{RELEASE}\t%{ARCH}' "$f"
sha256sum "$f" | cut -d' ' -f1

The epoch ternary notation is the key. A package such as mod_ssl has epoch 1, but that number appears nowhere in the file name.

Verifying on the inside

rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
rpmkeys --list
rpmkeys --checksig /srv/bundle/rpms/*.rpm | grep -v 'digests signatures OK'

If the last line prints nothing, everything is fine. And you also check files that are on the medium but not in the manifest.

comm -13 <(cut -f7 MANIFEST.tsv | sort) <(cd rpms && ls -1 *.rpm | sort)

A missing file shows up quickly as an install failure, but an added file passes without anyone knowing. From the viewpoint of import review, this is the bigger problem.

Setting up on the inside

createrepo_c /srv/repo/labhub
cat > /etc/yum.repos.d/airgap.repo <<'EOF'
[airgap-baseos]
name=RHEL 9 BaseOS (airgap local, snapshot 2026-08-20)
baseurl=file:///srv/repo/labhub
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
metadata_expire=-1
EOF
dnf --disablerepo='*' --enablerepo=airgap-baseos install -y httpd

What it looks like in the field

Leaving out the architecture. If you download on an x86_64 laptop and import into an ARM server, the wrong thing goes in without an error. If you do not specify noarch together in --arch, Python modules and configuration packages drop out entirely.

What you will do in the next lab

You build an import bundle, verify it with the manifest, pack it into an archive and unpack it, set up a repository, and install from it alone.