Logs: the journal and /var/log

journalctl filters, text logs and rotation.

Beginner14 min · lesson 20 of 29

Most of what happens on a server is written down twice: in the systemd journal, and on Ubuntu and RHEL also in plain text files under /var/log. This lesson shows how to get from a symptom to the lines that explain it with journalctl filters, how to read the fields each journal entry carries, who is allowed to read the logs, where the text files are on each distribution, and how old logs are rotated away. Logs are also your record of who did what, so the same skills answer security questions such as "did anyone try to log in overnight?".

One unit and a time window

The systemd lesson used journalctl -u to read one service's messages. Add a time window and you have the question most investigations start with. In the example, someone reported failed SSH logins at about 08:40, so you note the time and ask the SSH service's unit, ssh on Ubuntu (sshd on RHEL), for everything since then. The lab produced these events from addresses in the documentation ranges (a real server would show real addresses): one machine tried three accounts, and an administrator, ess-journald-alice, logged in from another. On your own machine, ssh nosuchuser@localhost (answer the host-key question, then press Ctrl-C at the password prompt) produces an Invalid user line of your own to search for.

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -u ssh --since 08:39:49
Sep 27 08:39:50 web01 sshd-session[83602]: Invalid user admin from 203.0.113.7 port 51225 Sep 27 08:39:50 web01 sshd-session[83602]: Connection closed by invalid user admin 203.0.113.7 port 51225 [preauth] Sep 27 08:39:51 web01 sshd-session[83620]: Invalid user oracle from 203.0.113.7 port 50705 Sep 27 08:39:51 web01 sshd-session[83620]: Connection closed by invalid user oracle 203.0.113.7 port 50705 [preauth] Sep 27 08:39:52 web01 sshd-session[83638]: Connection closed by authenticating user root 203.0.113.7 port 46813 [preauth] Sep 27 08:39:53 web01 sshd-session[83656]: Accepted publickey for ess-journald-alice from 198.51.100.24 port 41097 ssh2: ED25519 SHA256:m8usWcyK9C+5T9u3kfVfiVPhKbCX+460jr4z9kV8RcA Sep 27 08:39:53 web01 sshd-session[83656]: pam_unix(sshd:session): session opened for user ess-journald-alice(uid=4201) by ess-journald-alice(uid=0) Sep 27 08:39:53 web01 sshd-session[83656]: pam_unix(sshd:session): session closed for user ess-journald-alice

--since (and --until) take a time such as 08:39:49, a date and time such as "2026-09-27 06:00", or words such as today, yesterday and "1 hour ago". Each line shows the time, the host, the program that logged it with its PID in brackets, and the message. Since OpenSSH 9.8 the per-connection messages come from sshd-session, not sshd. Invalid user admin means the account does not exist; Connection closed by ... [preauth] means the client gave up before it authenticated; authenticating user root means the account exists and the client disconnected before authenticating (here, because its key was not accepted); Accepted publickey is a successful login, with the fingerprint of the key that was used, and the pam_unix lines mark the session opening and closing.

At a terminal, journalctl opens its output in less; -e jumps to the end, -n 20 shows the last 20 entries, -r puts the newest first, and -f follows new entries as they arrive until you press Ctrl-C. -g keeps only messages that match a regular expression, and a pattern written in lowercase matches regardless of case:

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -u ssh --since 08:39:49 -g 'invalid user'
Sep 27 08:39:50 web01 sshd-session[83602]: Invalid user admin from 203.0.113.7 port 51225 Sep 27 08:39:50 web01 sshd-session[83602]: Connection closed by invalid user admin 203.0.113.7 port 51225 [preauth] Sep 27 08:39:51 web01 sshd-session[83620]: Invalid user oracle from 203.0.113.7 port 50705 Sep 27 08:39:51 web01 sshd-session[83620]: Connection closed by invalid user oracle 203.0.113.7 port 50705 [preauth]

Priorities: finding the errors

Every entry has a priority, a number from 0 to 7: 0 emerg, 1 alert, 2 crit, 3 err, 4 warning, 5 notice, 6 info and 7 debug. Lower means more urgent, and -p err shows err and everything more urgent (0 to 3). When you do not yet know which service is in trouble, start there:

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -p err --since 08:39:49
Sep 27 08:39:53 web01 systemd[1]: Failed to start ess-journald-backup.service - Nightly backup.

A unit called "Nightly backup" failed to start (the lab created it as a stand-in for a real backup job). The error-level line names the unit but not the reason, so ask the unit for its whole story:

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -u ess-journald-backup --since 08:39:49
Sep 27 08:39:53 web01 systemd[1]: Starting ess-journald-backup.service - Nightly backup... Sep 27 08:39:53 web01 tar[83737]: /usr/bin/tar: Sep 27 08:39:53 web01 tar[83738]: tar (child): /srv/ess-journald-backup/etc.tar.gz: Cannot open: No such file or directory Sep 27 08:39:53 web01 tar[83738]: tar (child): Error is not recoverable: exiting now Sep 27 08:39:53 web01 tar[83737]: Removing leading `/' from member names Sep 27 08:39:53 web01 tar[83737]: /usr/bin/tar: /srv/ess-journald-backup/etc.tar.gz: Cannot write: Broken pipe Sep 27 08:39:53 web01 tar[83737]: /usr/bin/tar: Child returned status 2 Sep 27 08:39:53 web01 tar[83737]: /usr/bin/tar: Error is not recoverable: exiting now Sep 27 08:39:53 web01 systemd[1]: ess-journald-backup.service: Main process exited, code=exited, status=2/INVALIDARGUMENT Sep 27 08:39:53 web01 systemd[1]: ess-journald-backup.service: Failed with result 'exit-code'. Sep 27 08:39:53 web01 systemd[1]: Failed to start ess-journald-backup.service - Nightly backup.

tar could not create its archive because the directory /srv/ess-journald-backup does not exist, and it exited with status 2. Those tar lines did not appear under -p err: systemd logs whatever a service prints at priority info unless the program says otherwise. So use -p err to find the unit, then -u to read why. -b limits any query to the current boot and -b -1 to the one before, which is what you want after a crash and reboot.

From a symptom to the line that explains it
1Something is wrong
a failed job, a report, an alert
2Find the unit
journalctl -p err -b, or systemctl --failed
3Narrow the time
--since / --until around the event
4Read the unit
journalctl -u UNIT, -g PATTERN
5Check the fields
-o verbose: which process, which user
Each step removes lines. A few hundred lines of one unit in a ten-minute window can be read; a whole day of the journal cannot.

The fields behind each line

A journal entry is more than the line journalctl prints. -o verbose shows every field:

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -u ssh --since 08:39:49 -g Accepted -o verbose
Sun 2026-09-27 08:39:53.454056 UTC [s=76d6048ec1064603be1edf8ff8bffe0b;i=111f0;b=60492fd2965b48179fa78e0894522e50;m=314b4f5c;t=65c72e4b675f7;x=25e1331bf1ef343f] … PRIORITY=6 _UID=0 … SYSLOG_IDENTIFIER=sshd-session _COMM=sshd-session _EXE=/usr/lib/openssh/sshd-session … _SYSTEMD_UNIT=ssh.service _CMDLINE="sshd-session: ess-journald-alice [priv]" _BOOT_ID=60492fd2965b48179fa78e0894522e50 … MESSAGE=Accepted publickey for ess-journald-alice from 198.51.100.24 port 41097 ssh2: ED25519 SHA256:m8usWcyK9C+5T9u3kfVfiVPhKbCX+460jr4z9kV8RcA _PID=83656 …

Fields whose names start with an underscore are added by journald itself from what the kernel reports about the process that sent the message, so a program cannot fake them (for a process that exits within milliseconds journald can be too late to read them, and they are then missing): _PID, _UID (0 here, because the privileged sshd-session process runs as root), _COMM (the command name), _EXE, _CMDLINE and _SYSTEMD_UNIT. MESSAGE, PRIORITY and SYSLOG_IDENTIFIER come from the program and say whatever it chose. Any field can be a filter: journalctl _COMM=sudo lists every sudo command, and journalctl _UID=1001 everything one user's processes logged. PIDs are reused after a process exits, so to follow one process add its boot, as in journalctl _PID=83656 _BOOT_ID=60492fd2965b48179fa78e0894522e50.

For a timeline, especially one you will compare with other servers, print full dates with the time zone offset: -o short-iso does that, and --utc converts everything to UTC.

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -u ssh --since 08:39:49 -g 'Accepted|Invalid' -o short-iso --no-hostname
2026-09-27T08:39:50+00:00 sshd-session[83602]: Invalid user admin from 203.0.113.7 port 51225 2026-09-27T08:39:51+00:00 sshd-session[83620]: Invalid user oracle from 203.0.113.7 port 50705 2026-09-27T08:39:53+00:00 sshd-session[83656]: Accepted publickey for ess-journald-alice from 198.51.100.24 port 41097 ssh2: ED25519 SHA256:m8usWcyK9C+5T9u3kfVfiVPhKbCX+460jr4z9kV8RcA

Who may read the logs

The journal's files are in /var/log/journal, owned by the group systemd-journal. The + at the end of the mode means the directory also carries an ACL (access control list), extra permission entries for named users or groups on top of the nine bits, and getfacl prints them:

deploy@web01 · Ubuntu 26.04 LTS
$ ls -ld /var/log/journal
drwxr-sr-x+ 3 root systemd-journal 4096 Sep 26 19:46 /var/log/journal
$ getfacl -p /var/log/journal
# file: /var/log/journal # owner: root # group: systemd-journal # flags: -s- user::rwx group::r-x group:adm:r-x mask::r-x other::r-x default:user::rwx default:group::r-x default:group:adm:r-x default:mask::r-x default:other::r-x

The group:adm:r-x entry lets the adm group in as well, and the default: lines hand the same entries to the files created inside. So members of systemd-journal or adm can read everything, and Ubuntu puts the first administrator account in adm, which is why deploy has read these logs without sudo. On RHEL 10 the same ACL also names wheel (shown below), so every RHEL administrator reads the whole journal without sudo. To see what an ordinary account gets, the lab created ess-journald-bob with sudo useradd -m (any throwaway account you create works the same) and ran commands as him with sudo -u:

deploy@web01 · Ubuntu 26.04 LTS
$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),4(adm),27(sudo)
$ sudo -u ess-journald-bob journalctl -u ssh -n 3
Hint: You are currently not seeing messages from other users and the system. Users in groups 'adm', 'systemd-journal' can see all messages. Pass -q to turn off this notice. No journal files were opened due to insufficient permissions.
$ sudo -u ess-journald-bob tail -n 1 /var/log/auth.log
tail: cannot open '/var/log/auth.log' for reading: Permission denied

ess-journald-bob is in neither group. He may still run journalctl, but it would show only his own processes' messages. It prints a hint, and because he has never logged in and so has no journal file of his own, it reports that it could open no files at all; the text log refuses him outright. The fix is not to loosen file permissions. Use sudo, or give a colleague who reads logs for a living one of the reading groups: sudo usermod -aG systemd-journal NAME for the journal only, or adm for the journal and the text files (the lesson "Reading and editing files safely" showed their permissions). The change takes effect at their next login.

Kept across reboots, and copied to text files

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl --list-boots
IDX BOOT ID FIRST ENTRY LAST ENTRY -2 fe9d6a1664c14b67aa73a56205ff2435 Sat 2026-09-26 19:46:57 UTC Sat 2026-09-26 19:56:28 UTC -1 2a043efbc2dd462eac4deeacce00e4aa Sat 2026-09-26 19:56:31 UTC Sun 2026-09-27 08:26:02 UTC 0 60492fd2965b48179fa78e0894522e50 Sun 2026-09-27 08:26:06 UTC Sun 2026-09-27 08:39:55 UTC
$ journalctl --disk-usage
Archived and active journals take up 125.3M in the file system.

--list-boots lists every boot the journal still has, so this journal survived the machine's reboots, one the evening before and one a few minutes before these commands ran. On Ubuntu 26.04 journald stores its files on disk in /var/log/journal by default: Ubuntu's systemd 259 is built with Storage=persistent as the default. RHEL 10's default is Storage=auto, which writes to disk only if /var/log/journal exists, and RHEL creates it, so the journal is kept there too (this lab machine has booted only once, hence one entry). Rocky's minimal install has no getfacl, so the ACL is read from the systemd rule that sets it:

deploy@rocky10 · Rocky Linux 10.2
$ ls -ld /var/log/journal journalctl --list-boots
drwxr-sr-x+ 3 root systemd-journal 46 Sep 26 19:33 /var/log/journal IDX BOOT ID FIRST ENTRY LAST ENTRY 0 c404a6c12f204df098bfea44b84eeb1b Sat 2026-09-26 19:33:11 UTC Sun 2026-09-27 08:17:53 UTC
$ grep '^a+ /var/log/journal ' /usr/lib/tmpfiles.d/systemd.conf
a+ /var/log/journal - - - - d:group::r-x,d:group:adm:r-x,d:group:wheel:r-x,group::r-x,group:adm:r-x,group:wheel:r-x

Where the directory is missing, on some minimal or container images, the journal lives in /run/log/journal in memory and is lost at every reboot. Check with --list-boots before you need a previous boot's logs. journald limits its own size, by default to 10 percent of the filesystem and at most 4 GiB; --disk-usage shows the current total, and the hardening course sets retention deliberately.

Both distributions also run rsyslog, which receives a copy of every message and writes text files. On Ubuntu a drop-in in /usr/lib/systemd/journald.conf.d sets ForwardToSyslog=yes for it, and rsyslog writes /var/log/syslog (almost everything), /var/log/auth.log (logins, sudo, SSH) and /var/log/kern.log, with full timestamps:

deploy@web01 · Ubuntu 26.04 LTS
$ grep -E 'sshd-session.*(Invalid user|Accepted)' /var/log/auth.log | tail -n 3
2026-09-27T08:39:50.135317+00:00 web01 sshd-session[83602]: Invalid user admin from 203.0.113.7 port 51225 2026-09-27T08:39:51.237934+00:00 web01 sshd-session[83620]: Invalid user oracle from 203.0.113.7 port 50705 2026-09-27T08:39:53.455005+00:00 web01 sshd-session[83656]: Accepted publickey for ess-journald-alice from 198.51.100.24 port 41097 ssh2: ED25519 SHA256:m8usWcyK9C+5T9u3kfVfiVPhKbCX+460jr4z9kV8RcA

RHEL writes /var/log/messages and /var/log/secure instead, readable by root only, and keeps the older timestamp format with no year and no time zone:

deploy@rocky10 · Rocky Linux 10.2
$ ls -l /var/log/secure /var/log/messages
-rw-------. 1 root root 33393908 Sep 27 08:17 /var/log/messages -rw-------. 1 root root 633689 Sep 27 08:17 /var/log/secure
$ sudo grep -E 'Invalid user|COMMAND' /var/log/secure | tail -n 2
Sep 27 08:17:51 rocky10 sshd-session[1151837]: Invalid user admin from 203.0.113.7 port 59677 Sep 27 08:17:51 rocky10 sudo[1151850]: deploy : PWD=/home/deploy ; USER=root ; COMMAND=/bin/systemctl is-active sshd

Text files are handy for grep and for tools that expect them; the journal has the fields. Neither is safe from someone who has root on the machine, who can delete or edit both, which is why the hardening course forwards logs to another host.

Rotation: how old text logs go away

Text logs would grow forever, so logrotate renames them on a schedule and starts new ones. Its main settings are in /etc/logrotate.conf, and each package adds a file to /etc/logrotate.d. This is rsyslog's:

deploy@web01 · Ubuntu 26.04 LTS
$ cat /etc/logrotate.d/rsyslog
/var/log/syslog /var/log/mail.log /var/log/kern.log /var/log/auth.log /var/log/user.log /var/log/cron.log { rotate 4 weekly missingok notifempty compress delaycompress sharedscripts postrotate /usr/lib/rsyslog/rsyslog-rotate endscript }

weekly and rotate 4 keep the current file plus four older ones, so about five weeks of history. After a rotation auth.log starts empty, the previous week becomes auth.log.1, and older copies are compressed to auth.log.2.gz and so on; delaycompress leaves .1 uncompressed for a week because a program may still be finishing writes to it. missingok and notifempty skip missing and empty files, and the postrotate script tells rsyslog to reopen its files. The journal is not rotated by logrotate; journald manages its own files.

deploy@web01 · Ubuntu 26.04 LTS
$ systemctl list-timers logrotate.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES Mon 2026-09-28 00:19:49 UTC 15h Sun 2026-09-27 04:09:19 UTC - logrotate.timer logrotate.service 1 timers listed. Pass --all to see loaded but inactive timers, too.
$ sudo logrotate -d /etc/logrotate.conf 2>&1 | grep -A3 'considering log /var/log/auth.log'
considering log /var/log/auth.log Now: 2026-09-27 08:39 Last rotated at 2026-09-27 04:00 log does not need rotating (log has been rotated at 2026-09-27 04:00, which is less than a week ago)

A systemd timer runs logrotate once a day (timers are the next lesson), and logrotate -d shows what it would decide without changing anything. That dry run is the quickest answer to "why has this log not been rotated?".

Try this

Write your own entry at warning priority with logger -p warning -t ess-try "disk check: /var is 91% full", then find it with journalctl -t ess-try -p warning (-t filters on the identifier) and in /var/log/syslog with grep ess-try. Look at it with journalctl -t ess-try -o verbose and find PRIORITY=4, SYSLOG_IDENTIFIER=ess-try and your own _UID. Then log a second message at -p info and predict which command shows it: journalctl -t ess-try lists both, -p warning only the first.

Takeaway

Narrow before you read: a unit with -u, a window with --since, and -p err or -g for the lines that matter. Check journalctl --list-boots on every server you look after, so you know the logs you will need are being kept.

Quick check
01A backup job failed at 03:10. journalctl -p err --since 03:00 shows only "Failed to start backup.service - Nightly backup." Where is the reason most likely to be?
Incorrect — systemd captures everything the service printed. The reason is logged, just not at error priority.
Incorrect — The service's output is in the journal; syslog has a copy of the same lines.
Correct — -p err finds the failing unit; the unit's own messages are usually at info priority and appear with -u.
Incorrect — debug is the least urgent level, not a hidden one. The messages are at info.
02A new analyst runs journalctl -u ssh and sees a hint that they "are not seeing messages from other users and the system", then "No journal files were opened due to insufficient permissions." What is the least-privilege fix?
Incorrect — That exposes every log to every account on the machine, and journald may reset the permissions anyway.
Correct — The group exists for this; adm also works and adds the text logs in /var/log.
Incorrect — That works, but it grants every root power to someone who only needs to read logs.
Incorrect — journald does not keep a list of accounts. Reading is controlled by file permissions and groups.
03On Ubuntu 26.04, /etc/logrotate.d/rsyslog says weekly and rotate 4. About how far back can /var/log/auth.log and its rotated copies go?
Incorrect — The schedule is weekly, so each rotated file covers about a week, not a day.
Incorrect — rotate 4 means that the oldest copy beyond the fourth is deleted at each rotation.
Incorrect — The old contents are renamed, not discarded, and four renamed copies are kept.
Correct — auth.log, auth.log.1 and auth.log.2.gz to auth.log.4.gz; the next rotation deletes the oldest.

Related