Skip to content

Anti-forensicsLogsPersistence

logrotate State and Log Gaps: Detecting Linux Log Tampering

logrotate configuration and state files on Linux: how rotation shapes what logs survive, how to tell normal rotation from deletion, and postrotate persistence.

Location
/etc/logrotate.conf, /etc/logrotate.d/, state in /var/lib/logrotate/status (Debian) or /var/lib/logrotate/logrotate.status (RHEL)
Proves
When each log was last rotated, how many generations should exist, and whether missing, truncated or out-of-pattern log files are explained by rotation or by tampering
Timestamps
State file dates in local time (YYYY-M-D-H:M:S); rotated file mtimes; dateext suffixes
Access
root
Retention
Configuration persistent; the state file is rewritten at every run and keeps one line per log
Collection
UAC, Velociraptor, cp -a, tar
  • logrotateCLI · built into Linux
  • statCLI · built into Linux
  • findCLI · built into Linux
  • grep / zgrepCLI · built into Linux
  • journalctlCLI · built into Linux
  • systemctlCLI · built into Linux

What it is

logrotate renames, compresses and deletes text logs on a schedule. It decides how far back auth.log or secure, web server logs and many application logs reach. Knowing its configuration lets you say "the host keeps four weeks of secure logs, and we have two", which is the difference between normal retention and evidence destruction.

It is also an execution point. prerotate, postrotate, firstaction and lastaction blocks run shell commands as root every time rotation happens, which attackers use for persistence or to wipe logs quietly.

Where it lives

ItemPathNotes
Main config/etc/logrotate.confGlobal defaults (weekly, rotate 4, create, dateext on RHEL/SUSE)
Per-package rules/etc/logrotate.d/*rsyslog, apache2/httpd, nginx, mysql, wtmp/btmp (Debian: in logrotate.conf or /etc/logrotate.d/wtmp/btmp)
State (Debian/Ubuntu)/var/lib/logrotate/status
State (RHEL/Fedora)/var/lib/logrotate/logrotate.status
State (SUSE, Arch)/var/lib/misc/logrotate.status (SUSE), /var/lib/logrotate.status (Arch, upstream default)Check the -s option in the service unit if unsure
Schedulerlogrotate.timer + logrotate.service (systemd distros), or /etc/cron.daily/logrotate (older and some Debian installs)The timer is daily by default
Journal/var/log/journal/Not rotated by logrotate; see the journal
auditd logs/var/log/audit/Rotated by auditd itself

What it proves

  • When logrotate last processed each log file (state file), and on what schedule it is supposed to run.
  • How many rotated generations should exist (rotate N) and how they are named (.1, .2.gz or -YYYYMMDD with dateext).
  • Whether the configuration itself was modified to destroy evidence: rotate 0, maxage 1, size 1, shred, or a postrotate command that deletes other files.
  • It does not record what logrotate deleted; it just stops listing the file.

Key fields

State file:

logrotate state -- version 2
"/var/log/syslog" 2026-9-29-0:0:0
"/var/log/auth.log" 2026-9-27-0:0:0
"/var/log/nginx/access.log" 2026-9-29-0:0:0

Suspicious configuration:

/var/log/auth.log /var/log/secure {
    daily
    rotate 0
    missingok
    postrotate
        /usr/lib/.x/sync >/dev/null 2>&1 &
    endscript
}
ElementMeaning
State lineLog path and date of the last rotation (hour, minute, second usually zero for daily runs)
rotate NGenerations kept; 0 deletes the old log at each rotation
maxage DRemoves rotated logs older than D days
dateext, dateformatRotated file names carry a date instead of a number
copytruncateCopies then truncates the live file; a few lines may be lost in the gap
prerotate/postrotate/firstaction/lastactionShell blocks run as root
shredOverwrites before deleting; nothing to carve afterwards

Timestamps

The state file stores local time without a zone, in YYYY-M-D-H:M:S form with no zero padding. Rotated files keep the mtime of their last write, so auth.log.1 normally ends shortly before the time recorded for auth.log in the state file. With dateext, the suffix is the rotation date, which should match the state line of the previous cycle.

The timer's last run is visible in the journal (logrotate.service start and finish) and in /var/lib/systemd/timers/stamp-logrotate.timer when Persistent=true is set.

Retention

The state file keeps only the latest rotation per log. Configuration persists until changed. The rotated logs themselves live as long as rotate and maxage allow: Debian and RHEL default to weekly rotation with four generations for most system logs, and many package rules differ (Ubuntu has long rotated syslog daily with seven generations, nginx on Debian keeps 14 daily files, wtmp rotates monthly); read the rules on the image rather than assuming.

Collection

tar -C /mnt/evidence -cpf /cases/2026-017/logrotate.tar etc/logrotate.conf etc/logrotate.d \
  var/lib/logrotate var/lib/logrotate.status var/lib/misc/logrotate.status \
  var/lib/systemd/timers etc/cron.daily/logrotate 2>/dev/null
# Full inventory of log files with sizes and times, before copying anything
find /mnt/evidence/var/log -xdev -type f -printf '%T+\t%C+\t%s\t%p\n' | sort -k4 > varlog_inventory.tsv

UAC collects /etc and /var/log; add /var/lib/logrotate* and /var/lib/misc to be sure the state file is included. Never run logrotate against the evidence; even -d (debug) should only be used on a copy of the configuration.

Parsing

R=/mnt/evidence
cat "$R"/var/lib/logrotate/status "$R"/var/lib/logrotate/logrotate.status 2>/dev/null | sort -k2
grep -rnE 'rotate +0|maxage|shred|size +[0-9]+[kK]?$|prerotate|postrotate|firstaction|lastaction' \
  "$R"/etc/logrotate.conf "$R"/etc/logrotate.d/
# Unowned or recently changed rules
ls -la --time-style=full-iso "$R"/etc/logrotate.d/
# Journal: when logrotate actually ran, and failures
journalctl -D "$R"/var/log/journal -u logrotate.service -o short-iso-precise | tail -50

For gaps inside a single file, list the timestamps of consecutive lines and flag intervals far longer than the host's normal activity (a busy auth log rarely goes silent for hours):

zcat -f "$R"/var/log/auth.log* | awk '{print $1" "$2" "$3}' | uniq -c | less

Investigator tips

  • Count generations: rotate 4 with weekly rotation on a host up for months should give auth.log plus four older files. Fewer, with no config change, points to deletion; check the directory's mtime and the file names' inode numbers.
  • A zero-byte current log with a recent mtime and a normal-sized .1 suggests truncation (> /var/log/auth.log), not rotation, unless the state file shows a rotation at that exact time.
  • Wiped binary logs often leave blocks of NUL bytes; wtmp and lastlog edited by log cleaners are covered in wtmp, btmp and lastlog.
  • Text logs and the journal record many of the same events. A gap in auth.log with no matching gap in the journal is strong evidence of targeted editing.
  • Treat every postrotate block as code: compare it with the package's original (dpkg -V, rpm -V) and check any script it calls, like any other scheduled task.

See also