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
Tools
Compare all tools- 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
| Item | Path | Notes |
|---|---|---|
| Main config | /etc/logrotate.conf | Global 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 |
| Scheduler | logrotate.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.gzor-YYYYMMDDwithdateext). - Whether the configuration itself was modified to destroy evidence:
rotate 0,maxage 1,size 1,shred, or apostrotatecommand 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
}
| Element | Meaning |
|---|---|
| State line | Log path and date of the last rotation (hour, minute, second usually zero for daily runs) |
rotate N | Generations kept; 0 deletes the old log at each rotation |
maxage D | Removes rotated logs older than D days |
dateext, dateformat | Rotated file names carry a date instead of a number |
copytruncate | Copies then truncates the live file; a few lines may be lost in the gap |
prerotate/postrotate/firstaction/lastaction | Shell blocks run as root |
shred | Overwrites 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 4with weekly rotation on a host up for months should giveauth.logplus 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
.1suggests 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;
wtmpandlastlogedited 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.logwith no matching gap in the journal is strong evidence of targeted editing. - Treat every
postrotateblock as code: compare it with the package's original (dpkg -V,rpm -V) and check any script it calls, like any other scheduled task.