The DNF history database is a forgotten audit-trail goldmine on every RHEL, Rocky, AlmaLinux, and Fedora server. Every transaction โ installs, upgrades, removals, group operations โ is recorded with timestamp, originating user, and the exact list of packages affected. Even better: most transactions are reversible with a single command. This guide covers the queries that surface real changes, the rollback workflow, and how to integrate the history into your security audit routine.
The history command
sudo dnf history # transaction list, newest first
sudo dnf history list --reverse | head # oldest first
sudo dnf history info 47 # detail for transaction 47
sudo dnf history info last # most recent transaction
sudo dnf history list openssh-server # only transactions that touched a package
Each entry has an ID, command-line that ran, who ran it (login or sudo), action summary, and a list of altered packages with old/new versions.
Filtering recent activity
sudo dnf history list --since=2026-01-01
sudo dnf history list --since=-7d # last 7 days
sudo dnf history userinstalled # explicitly installed (not deps)
sudo dnf repoquery --installed --queryformat \
'%{INSTALLTIME:date} %{NAME}-%{VERSION}\n' \
| sort -n | tail -30 # 30 most recent installs
The userinstalled view is invaluable for distinguishing "intentional install" from "pulled in as a dependency" โ the latter is rarely interesting; the former is who-did-what.
What changed in a single transaction
sudo dnf history info 47
# Output includes:
# Transaction ID : 47
# Begin time : Mon 14 Apr 2026 14:32
# Begin rpmdb : 1234:ab12cd...
# End time : Mon 14 Apr 2026 14:33 (45 seconds)
# User : Bob <bob@example.com>
# Return-Code : Success
# Releasever : 9
# Command Line : install nginx
# Packages Altered:
# Install nginx-1.24-1.el9.x86_64 @rhel9-appstream
# Dep-Install nginx-filesystem-...
The Begin rpmdb checksum lets you verify whether the package database has been tampered with since.
Rolling back a transaction
sudo dnf history rollback 46 # undo everything since transaction 46
sudo dnf history undo 47 # undo transaction 47 specifically
sudo dnf history redo 47 # repeat transaction 47
Rollback handles version reversion automatically, including downgrades. Caveats: rolled-back packages must still be available in a configured repository; rollback does not undo configuration file changes (those are governed by RPM's %config(noreplace) semantics).
Integrating with security baselines
A weekly script that exports the new transactions for your SIEM:
#!/bin/bash
last_seen_file=/var/lib/dnf-history-last
last_seen=$(cat "$last_seen_file" 2>/dev/null || echo 0)
sudo dnf history list --reverse 2>/dev/null | awk -v cutoff="$last_seen" '
NR < 3 { next }
$1+0 > cutoff && NF > 3 {
print
}'
sudo dnf history list --reverse | awk 'NR==3{print $1; exit}' > "$last_seen_file"
Ship to your log collector. Alert on any transaction outside an authorised maintenance window.
Verifying installed package integrity
sudo rpm -Va | head # verify all installed packages
sudo rpm -V openssh-server # verify a single package
sudo rpm -qf /usr/sbin/sshd # which package owns this file
sudo rpm -ql openssh-server | head # files in package
sudo rpm -qi openssh-server # vendor, signature, timestamps
The output of rpm -V uses single-letter codes per attribute: S (size), M (mode), 5 (md5/sha digest), D (device), L (symlink), U (user), G (group), T (modification time), P (caps). A 5 on a binary means the file content has been modified since installation โ investigate immediately.
Repository auditing
sudo dnf repolist all
sudo dnf repolist enabled
sudo dnf config-manager --dump | head
ls /etc/yum.repos.d/
gpg --list-keys | grep -i 'rpm-gpg'
Every enabled repository can install packages. Verify gpgcheck=1 in every .repo file, and that the GPG keys are pinned. An attacker who adds a malicious repo with a low-priority can backdoor any future install.
The audit script
#!/bin/bash
echo "== Transaction count =="
sudo dnf history list --reverse | tail -n +3 | wc -l
echo "== Last 10 transactions =="
sudo dnf history list --reverse | head -13 | tail -10
echo "== Transactions in last 7 days =="
sudo dnf history list --since=-7d
echo "== Modified package files =="
sudo rpm -Va 2>/dev/null | grep -v '^missing' | head -20
echo "== Enabled repositories =="
sudo dnf repolist enabled
echo "== GPG check disabled (FAIL) =="
grep -L '^gpgcheck=1' /etc/yum.repos.d/*.repo 2>/dev/null
Distinguishing automatic from manual
dnf-automatic and Cockpit's automatic-update mechanism record their own user (typically root with command-line including --security or upgrade). Manual sudo invocations include the originating user. Filter:
sudo dnf history list | awk '/dnf-automatic/{print "auto"; next} {print "manual"}' | sort | uniq -c
Common pitfalls
- Running
dnf clean allafter a successful install โ this clears the cached RPMs but does not remove history; rollback may fail if cached packages are gone. - Trusting history alone after a root compromise; an attacker can manipulate
/var/lib/dnf/history.sqlite. Pair with off-host log shipping. - Disabling
dnf historyfor "performance" โ the overhead is negligible and the audit trail is irreplaceable. - Missing the rollback caveat about config files; verify post-rollback whether
/etc/myapp/confis the version you expect.
DNF history is one of those features RHEL family ships free that other ecosystems would charge for. Use it: weekly review, automated export, integrity verification with rpm -V, and the same monthly rollback test you would run on backups. The next "what changed?" question becomes a one-line answer.