Skip to content

LogsExecutionPersistence

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
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

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

ItemDebian / UbuntuRHEL / 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.logconsole_output table in DNF 4 history
Transaction databasenone/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 scriptsinside 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, RPM INSTALLTIME).
  • The command line that triggered a transaction (Commandline: in history.log, cmdline in DNF history).
  • The account behind it: APT writes Requested-By: when it runs through sudo, pkexec or PackageKit; DNF history stores the user_id (UID).
  • Removal of a tool after use (remove/purge lines, Erase in DNF).
  • A package in dpkg.log with no matching APT transaction usually means a direct dpkg -i of 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:

FieldMeaning
Start-Date / End-DateLocal time, YYYY-MM-DD HH:MM:SS (two spaces)
Commandlineapt or apt-get arguments as typed
Requested-Byuser (uid) taken from SUDO_UID, PKEXEC_UID or PACKAGEKIT_CALLER_UID; absent when run directly as root
Install, Reinstall, Upgrade, Downgrade, Remove, PurgePackage lists with versions; automatic marks dependencies
Errordpkg 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

SourceFormatZone
dpkg.logYYYY-MM-DD HH:MM:SSLocal, no offset
apt/history.log, term.logYYYY-MM-DD HH:MM:SSLocal, no offset
dnf.log, dnf.rpm.logISO 8601 with numeric offsetLocal with offset
yum.logMmm dd HH:MM:SS, no yearLocal
DNF history dt_begin/dt_endUnix epoch secondsUTC
RPM INSTALLTIMEUnix epoch secondsUTC

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.log and apt/term.log monthly 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_rotate in dnf.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-By and user_id with 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 .deb with a malicious postinst is a persistence and execution vector; compare against the official package.
  • A package removed without purge keeps its conffiles and shows as rc in dpkg -l, so the status file still names it after removal.
  • If logs were wiped, fall back on inode times of /var/lib/dpkg/info/*.list and the RPM INSTALLTIME.
  • New kernel module or library packages deserve a look at kernel modules and ld.so.preload.

See also