RPM HOWTO: query, verify, build and sign RPM packages

Updated November 2026

RPM is the package format and low-level tool behind Fedora, RHEL, AlmaLinux, Rocky Linux and openSUSE. Use dnf (or zypper on openSUSE) to install and update software, because it resolves dependencies. Use rpm to ask questions: which package owns a file, what changed since install, whether a package file is signed. To make your own packages, you write a spec file and run rpmbuild.

RPM 6.0 (September 2025) can read and write a new v6 package format, supports several OpenPGP signatures per package and newer key types, and dropped support for installing very old v3 packages. RPM 6.1 (August 2026) is a feature update on top of it. As of November 2026, Fedora 45 (released November 3, 2026) ships RPM 6.1.0 and Fedora 43 and 44 ship RPM 6.0.2. All three still produce v4 packages by default, so files built there install on older systems. Fedora 45 is also the first Fedora release in which rpm itself refuses unsigned packages (see below).

Install, update and remove

sudo dnf install httpd                # install with dependencies
sudo dnf install ./hello-1.0-1.fc45.noarch.rpm   # a local file, still resolving dependencies
sudo dnf upgrade                      # update everything
sudo dnf remove httpd
dnf provides /usr/bin/dig             # which package would give me this file?
dnf history                           # past transactions
sudo dnf history undo last            # roll back the last transaction (DNF 4 and dnf5)

You can still run sudo rpm -Uvh file.rpm (install or upgrade, verbose, with progress bar), but it stops at the first missing dependency and never downloads anything. rpm -i is right mainly for kernels, where several versions are meant to stay installed side by side.

Query installed packages

rpm -qa | sort                 # every installed package
rpm -qa --last | head          # most recently installed first
rpm -qi openssh-server         # name, version, vendor, build date, signature, description
rpm -ql openssh-server         # files in the package
rpm -qc openssh-server         # only its configuration files
rpm -qf /etc/ssh/sshd_config   # which package owns this file?
rpm -q --changelog openssl | head
rpm -q --scripts httpd         # install/remove scriptlets
rpm -q --whatrequires bash     # installed packages that need bash
rpm -qpl ./hello-1.0-1.fc45.noarch.rpm   # -p: ask a package file, not the database

rpm -qf answers "where did this come from?" in a second. If it prints "not owned by any package", someone installed the file by hand.

A query format prints exactly the fields you want, which is handy in scripts:

rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n' | sort | head -3

Verify files against the database

RPM remembers the size, permissions, owner, digest and time of every file it installed. rpm -V compares them with what is on disk now and prints only the files that differ:

$ sudo rpm -V openssh-server
S.5....T.  c /etc/ssh/sshd_config
What the nine result characters mean
LetterMeaningLetterMeaning
Sfile size differsUuser owner differs
Mmode (permissions or type) differsGgroup owner differs
5digest (content) differsTmodification time differs
Ddevice number differsPcapabilities differ
Lsymlink target differs. / ?test passed / could not run

The letter after the codes marks the file type: c configuration, d documentation, l license, g ghost. A changed c file is usually your own edit. A changed binary in /usr/bin is not normal. Check the whole system with sudo rpm -Va (it takes a few minutes).

Not a security scanner. An attacker with root can change the RPM database too. For tamper detection, compare against a trusted copy or use a file-integrity tool such as AIDE; see the Security HOWTO.

Check signatures and keys

Distribution packages are signed with OpenPGP keys. dnf checks them automatically using the keys listed in each repository's gpgkey= setting. To check a downloaded file by hand:

rpmkeys -K hello-1.0-1.fc45.noarch.rpm      # same as: rpm -K
rpmkeys -Kv hello-1.0-1.fc45.noarch.rpm     # per-check detail

If the key is missing, the output says the signature is not OK or the key is unknown. Import a vendor key only from a source you trust, such as the file the distribution already ships in /etc/pki/rpm-gpg/:

ls /etc/pki/rpm-gpg/ /usr/share/pki/rpm-gpg/   # keys shipped by the distribution
sudo rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-<name>
rpmkeys --list                     # RPM 4.20 and newer (Fedora 41+)
rpm -q gpg-pubkey                  # older RPM (RHEL 9 and 10): keys appear as gpg-pubkey packages

On Fedora 45 the distribution keys moved to /usr/share/pki/rpm-gpg/ and the packaged .repo files to /usr/share/dnf5/repos.d/; /etc/yum.repos.d/ is still read for your own repositories. RPM 6 identifies keys by their full fingerprint everywhere and enforces signature checking by default upstream. Fedora 43 and 44 keep the old behavior (check signatures if present, never require them). Fedora 45 switched enforcement on: rpm refuses to install, and rpmkeys -K fails on, a package without a valid signature from an imported key, unless you add --nosignature. Packages from signed repositories are not affected; dnf has always required signatures for repositories with gpgcheck=1.

Build a package

Never build packages as root: a mistake in the spec file could overwrite system files. Install the tools and create the build tree in your home directory:

sudo dnf install rpm-build rpmdevtools rpmlint
rpmdev-setuptree          # creates ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

Make a small source tarball. Here the "software" is a shell script plus a license file:

mkdir hello-1.0
printf '#!/bin/bash\necho "Hello, world"\n' > hello-1.0/hello
printf 'MIT License\n' > hello-1.0/LICENSE      # use the real license text
tar -czf ~/rpmbuild/SOURCES/hello-1.0.tar.gz hello-1.0

Write ~/rpmbuild/SPECS/hello.spec (rpmdev-newspec hello gives you a template):

Name:           hello
Version:        1.0
Release:        1%{?dist}
Summary:        Print a friendly greeting
License:        MIT
URL:            https://example.com/hello
Source0:        %{name}-%{version}.tar.gz
BuildArch:      noarch
Requires:       bash

%description
A tiny shell script that prints a greeting. It shows how RPM packaging works.

%prep
%setup -q

%build
# nothing to compile for a shell script

%install
install -D -m 0755 %{name} %{buildroot}%{_bindir}/%{name}

%files
%license LICENSE
%{_bindir}/%{name}

%changelog
* Wed Nov 04 2026 Your Name <you@example.com> - 1.0-1
- First package
Header
Name, Version and Release form the package file name. %{?dist} adds the distribution tag, such as .fc45. BuildArch: noarch is for packages with no compiled code.
%prep
Unpacks the source. %setup -q extracts Source0 and changes into hello-1.0/.
%build
Compiles. For a C program using autotools this is usually %configure and %make_build; then use %make_install in %install.
%install
Copies files into the fake root %{buildroot}. Use macros such as %{_bindir} (/usr/bin); rpm --eval %{_bindir} shows any macro's value.
%files
Lists every file the package owns. A file installed but not listed stops the build with an "unpackaged files" error.

Build the source and binary packages, then check them:

cd ~/rpmbuild/SPECS
rpmbuild -ba hello.spec
rpmlint hello.spec ~/rpmbuild/RPMS/noarch/hello-1.0-1.*.noarch.rpm
rpm -qpl ~/rpmbuild/RPMS/noarch/hello-1.0-1.*.noarch.rpm

The end of the rpmbuild output shows where the files went, with lines like Wrote: /home/you/rpmbuild/RPMS/noarch/hello-1.0-1.fc45.noarch.rpm and one for the .src.rpm in SRPMS/. Use -bb for the binary package only, -bs for the source package only, and rpmbuild --rebuild file.src.rpm to rebuild someone else's source package. For packages that need compilers and libraries, sudo dnf builddep hello.spec installs everything listed in BuildRequires:.

Fedora 45: local builds are unsigned. Because rpm now enforces signatures, a package you just built will not install with rpm until it is signed. For local work, run rpm-setup-autosign once (RPM 6.1 puts it in the normal $PATH; on RPM 6.0 it is /usr/lib/rpm/rpm-setup-autosign) and import the key it reports; after that rpmbuild signs every package automatically. For a one-off test, sudo rpm -i --nosignature file.rpm skips the check. Do not turn enforcement off system-wide.

Build in a clean chroot with mock

A package that builds on your workstation may only work because of something you happen to have installed. mock builds in a fresh chroot, and can target other releases:

sudo dnf install mock
sudo usermod -a -G mock "$USER"     # log out and back in
mock -r fedora-45-x86_64 ~/rpmbuild/SRPMS/hello-1.0-1.fc45.src.rpm

On RHEL and its rebuilds, mock comes from EPEL. Membership in the mock group is close to root access, so give it only to trusted users.

Sign your packages

Create a signing key with GnuPG (gpg --full-generate-key), then tell RPM which key to use. With RPM 4 (RHEL 9 and 10), put the key's user ID in ~/.rpmmacros:

%_gpg_name Your Name <you@example.com>

With RPM 6 (Fedora 43 and newer), use the key's full fingerprint (gpg --list-keys --with-fingerprint shows it) in ~/.config/rpm/macros; %_gpg_name still works but is now the legacy name:

%_openpgp_sign_id <your-key-fingerprint>

Sign, publish the public key, and check from a client:

sudo dnf install rpm-sign
rpmsign --addsign ~/rpmbuild/RPMS/noarch/hello-1.0-1.fc45.noarch.rpm
gpg --export --armor you@example.com > RPM-GPG-KEY-example
# on a client:
sudo rpmkeys --import RPM-GPG-KEY-example
rpmkeys -K hello-1.0-1.fc45.noarch.rpm

For a repository, run createrepo_c on the directory of signed files and give clients a .repo file with gpgcheck=1 and gpgkey= pointing at your public key. RPM 6 can also use Sequoia PGP instead of GnuPG (%_openpgp_sign sq), and rpmsign --resign replaces all old signatures.

Mistakes to avoid

The classic 1999 RPM HOWTO is kept by the LDP at tldp.org. For release practices such as versioning and signed tags, see the Software Release Practice HOWTO.

Sources