dpkg, APT, RPM and DNF Logs: Linux Package History
Linux package manager logs and databases (dpkg, APT, RPM, DNF, YUM) that date software installs and removals and record who ran them.
- Location
- /var/log/dpkg.log, /var/log/apt/history.log, /var/log/dnf.rpm.log
- Distributions
- Proves
- Which packages were installed, upgraded or removed, when, with which command line and by which sudo user
- Timestamps
- dpkg/APT/DNF logs: host local time; DNF history and RPM install time: Unix epoch seconds (UTC)
- Access
- dpkg.log and apt/history.log are world-readable; root for complete collection
- Retention
- dpkg and APT: monthly rotation, 12 kept; DNF 4: 1 MB x 4 files; databases until the package is removed
- Collection
- UAC, Velociraptor, cp -a, tar
Tools
Compare all tools- grep / zgrepCLI · built into Linux
- sqlite3CLI · open source
- rpmCLI · built into Linux
- dpkgCLI · built into Linux
- PlasoCLI · open source
What it is
Every mainstream Linux package manager keeps two kinds of records: a text log of transactions and a database of what is currently installed. On Debian and Ubuntu, dpkg writes its own low-level log and APT adds a transaction history with the command line. On RHEL, Fedora, Rocky and Alma, the RPM database holds install times per package, while DNF (or YUM on RHEL 7) keeps text logs and a SQLite transaction history.
For an investigation this answers two questions: when did new software (a compiler, socat, nmap, a malicious .deb or .rpm) arrive, and who asked for it. Integrity checking with the same databases is covered in depth in the package manager guide; this page is the quick reference.
Where it lives
| Item | Debian / Ubuntu | RHEL / Fedora family |
|---|---|---|
| Low-level action log | /var/log/dpkg.log | /var/log/dnf.rpm.log (DNF 4) |
| Transaction log | /var/log/apt/history.log | /var/log/dnf.log (DNF 4), /var/log/dnf5.log (DNF 5), /var/log/yum.log (RHEL/CentOS 7) |
| Terminal output | /var/log/apt/term.log | console_output table in DNF 4 history |
| Transaction database | none | /var/lib/dnf/history.sqlite (DNF 4); /usr/lib/sysimage/libdnf5/transaction_history.sqlite (DNF 5); /var/lib/yum/history/history-*.sqlite (YUM) |
| Installed package database | /var/lib/dpkg/status | /var/lib/rpm/ (Berkeley DB on RHEL 7/8, rpmdb.sqlite on RHEL 9 and Fedora 33+) |
| Per-package file lists | /var/lib/dpkg/info/<pkg>.list, .md5sums, maintainer scripts | inside the RPM database |
| Status backups | /var/backups/dpkg.status.* | none |
Fedora 36 and later store the RPM database in /usr/lib/sysimage/rpm and leave /var/lib/rpm as a symlink. On a mounted image make sure the link resolves inside the mount, not on your analysis host. DNF 4 history stays in /var/lib/dnf.
What it proves
- The exact time a package was unpacked, configured, upgraded or removed (
dpkg.log,dnf.rpm.log, RPMINSTALLTIME). - The command line that triggered a transaction (
Commandline:inhistory.log,cmdlinein DNF history). - The account behind it: APT writes
Requested-By:when it runs throughsudo,pkexecor PackageKit; DNF history stores theuser_id(UID). - Removal of a tool after use (
remove/purgelines,Erasein DNF). - A package in
dpkg.logwith no matching APT transaction usually means a directdpkg -iof a local.deb. - It does not prove the files are still unmodified (use verification), and it says nothing about software built from source, copied in, installed with
pip, or run from/tmp.
Key fields
dpkg.log (four line types: startup, status, action, conffile):
2026-09-14 10:22:31 startup archives unpack
2026-09-14 10:22:31 install socat:amd64 <none> 1.7.4.4-2
2026-09-14 10:22:32 status installed socat:amd64 1.7.4.4-2
2026-09-14 10:22:32 conffile /etc/default/foo keep
Action words are install, upgrade, configure, trigproc, disappear, remove and purge, followed by package, installed version and available version.
/var/log/apt/history.log stanza:
| Field | Meaning |
|---|---|
Start-Date / End-Date | Local time, YYYY-MM-DD HH:MM:SS (two spaces) |
Commandline | apt or apt-get arguments as typed |
Requested-By | user (uid) taken from SUDO_UID, PKEXEC_UID or PACKAGEKIT_CALLER_UID; absent when run directly as root |
Install, Reinstall, Upgrade, Downgrade, Remove, Purge | Package lists with versions; automatic marks dependencies |
Error | dpkg error for a failed transaction |
DNF 4 history.sqlite, table trans: id, dt_begin, dt_end, user_id, cmdline, releasever, state, rpmdb_version_begin/_end. Packages link through trans_item (trans_id, item_id, action, reason) to rpm (name, epoch, version, release, arch). console_output keeps the transaction's stdout and stderr lines.
dnf.rpm.log lines look like 2026-09-14T10:22:31+0200 SUBDEBUG Installed: socat-1.7.4.1-5.el9.x86_64.
Timestamps
| Source | Format | Zone |
|---|---|---|
dpkg.log | YYYY-MM-DD HH:MM:SS | Local, no offset |
apt/history.log, term.log | YYYY-MM-DD HH:MM:SS | Local, no offset |
dnf.log, dnf.rpm.log | ISO 8601 with numeric offset | Local with offset |
yum.log | Mmm dd HH:MM:SS, no year | Local |
DNF history dt_begin/dt_end | Unix epoch seconds | UTC |
RPM INSTALLTIME | Unix epoch seconds | UTC |
Read the image's /etc/localtime before merging local-time logs into a super timeline.
rpm --root=/mnt/evidence -qa --qf '%{INSTALLTIME:date} %{NAME}-%{VERSION}-%{RELEASE}\n' | sort
date -u -d @1789388551
Retention
- Debian packaging rotates
dpkg.log,apt/history.logandapt/term.logmonthly and keeps 12 compressed generations, so about a year. - DNF 4 rotates its own logs at 1 MB and keeps 4 old copies (
log_size,log_rotateindnf.conf), which can be only weeks on a busy host. - The DNF and YUM history databases and the dpkg/RPM databases are not rotated. Package entries disappear from the installed databases on removal, while the history keeps the transaction.
/var/backups/dpkg.status.*holds daily backups of the status file on Debian-family systems.
Collection
UAC collects /var/log (all these logs), /var/lib/dpkg/status and runs dpkg -l, dpkg -V, rpm -qa with install times, rpm -V -a and dnf history list in live response. Velociraptor has Linux.Debian.Packages and Linux.RHEL.Packages. From a read-only mount:
E=/mnt/evidence
tar -C "$E" -cpf /cases/2026-017/pkg.tar var/log/dpkg.log* var/log/apt var/log/dnf* \
var/log/yum.log* var/lib/dpkg/status var/lib/dpkg/info var/backups \
var/lib/dnf var/lib/yum/history usr/lib/sysimage 2>/dev/null
Copy the whole RPM database directory with any -wal/-shm files if you need to query it later.
Parsing
# Debian: all install/remove actions in order
zcat -f dpkg.log* | grep -E ' (install|upgrade|remove|purge) ' | sort
# APT transactions with user and command line
zcat -f apt/history.log* | grep -E '^(Start-Date|Commandline|Requested-By|Install|Remove|Purge):'
# DNF 4 history: packages per transaction
sqlite3 -readonly history.sqlite "SELECT t.id, datetime(t.dt_begin,'unixepoch'), t.user_id, t.cmdline,
r.name||'-'||r.version||'-'||r.release FROM trans t JOIN trans_item ti ON ti.trans_id=t.id
JOIN rpm r ON r.item_id=ti.item_id ORDER BY t.id;"
Plaso parses dpkg.log (text/dpkg) and history.log (text/apt_history). rpm --root and dpkg --root query offline databases.
Investigator tips
- Line up
Requested-Byanduser_idwith sudo logs and the account's shell history. - Unattended upgrades produce large batches at regular times; attacker installs tend to be single packages at odd hours.
- Maintainer scripts (
/var/lib/dpkg/info/*.postinst) run as root. A locally built.debwith a maliciouspostinstis a persistence and execution vector; compare against the official package. - A package removed without purge keeps its conffiles and shows as
rcindpkg -l, so the status file still names it after removal. - If logs were wiped, fall back on inode times of
/var/lib/dpkg/info/*.listand the RPMINSTALLTIME. - New kernel module or library packages deserve a look at kernel modules and ld.so.preload.