You will query journald, forward logs to a central rsyslog server, and rotate files so disks never fill. By the end you will have searchable, retained, centralized logs, the foundation for monitoring and the SOC labs.

Learning Objectives

  • Filter and follow logs with journalctl.
  • Forward logs to a central server over rsyslog.
  • Rotate and retain logs with logrotate.
  • Time: ~3 hours · Difficulty: Intermediate · Prereqs: a server (and optionally a second host as collector).

Architecture Overview

Environment Setup

You will need: systemd-journald (default), rsyslog, and logrotate.

Before you begin: if forwarding, have a reachable collector listening on TCP/514.

Prerequisites

  • A systemd-based Linux host with journald and rsyslog installed.
  • Root or sudo access for rsyslog config and logrotate.
  • A central log server address for the forwarding step (or a second VM).
  • A service generating logs (ssh used in the examples).

Step-by-Step Execution

01
Query logs precisely with journalctl

Targeted filters turn a flood of entries into the few that matter for triage.

journalctl -p info -S "2026-06-17 00:00:00" -u ssh --no-pager
Shows SSH messages at info priority or higher since midnight. Failed SSH logins are recorded at info priority, so -p err would hide exactly the lines you are hunting.

Expected output:

Jun 17 09:02:11 srv01 sshd[2101]: Failed password for invalid user admin from 203.0.113.45
Jun 17 09:02:14 srv01 sshd[2101]: Failed password for invalid user admin from 203.0.113.45

What this teaches: -p filters by priority (err and higher would miss these auth lines, which are info), -S sets the start time, -u scopes to one unit: three filters turn a flood into the exact lines triage needs. Match the priority to what the service actually logs.

02
Forward logs to a central server

Centralized logs survive a compromised host and enable cross-host correlation.

echo '*.* @@192.168.1.60:514' | sudo tee /etc/rsyslog.d/90-forward.conf && sudo systemctl restart rsyslog

Expected output:

*.* @@192.168.1.60:514
(rsyslog restarted; verify with: systemctl is-active rsyslog)

What this teaches: A one-line rsyslog rule forwards everything over TCP (@@) to the collector; centralized logs survive a compromised or dead host.

The collector (192.168.1.60) must actually be listening, or the forwarded logs go nowhere. Enable TCP reception on the collector:

ON THE COLLECTOR: LISTEN ON TCP 514

printf 'module(load="imtcp")\ninput(type="imtcp" port="514")\n' | sudo tee /etc/rsyslog.d/10-listen.conf && sudo systemctl restart rsyslog && sudo ss -tlnp | grep ':514'
[ROOT REQUIRED] Run on 192.168.1.60; also open TCP 514 in the collector firewall.

Verify end to end: run logger -t labtest "forward check" on the client, then search for labtest in the collector logs.

Security note: bare TCP syslog is unencrypted and unauthenticated. On any untrusted network, use TLS (imtcp with certificates) or RELP with TLS instead of plain @@host:514.

03
Rotate logs with logrotate

Expected output:

# logrotate -d /etc/logrotate.d/rsyslog
rotating pattern: /var/log/syslog weekly (4 rotations) considering log /var/log/syslog log does not need rotating

What this teaches: logrotate -d dry-runs the config, showing which pattern matches and whether rotation is due, without touching any files.

How It Works

Modern Linux logging has two layers. journald captures stdout/stderr of every systemd service into a structured, indexed binary journal with fields like _SYSTEMD_UNIT and PRIORITY; journalctl queries that index, which is why filtering by unit, time, and priority is instant. rsyslog handles the traditional text-file side (/var/log/syslog) and simple network forwarding. how journal entries reach rsyslog depends on the distribution and local configuration: either journald forwards them to the local syslog socket (ForwardToSyslog=yes in journald.conf), or rsyslog pulls them from the journal directly with its imjournal input module. Upstream systemd defaults ForwardToSyslog to no, precisely because rsyslog and syslog-ng normally read the journal themselves, so check /etc/systemd/journald.conf and whether rsyslog loads imjournal to know which path your host uses. journald also ships its own network transport via systemd-journal-remote and systemd-journal-upload; rsyslog forwarding remains the simplest cross-platform path.

Centralized logging is a security control, not just convenience: an attacker who roots a host can tamper with local logs, but already-forwarded entries on the collector are out of reach. Forwarding over TCP (or TLS with certificates) plus local buffering covers the gap between event and delivery.

logrotate bounds disk usage with declarative policy: rotate weekly, keep 4, compress the old ones, and notify the daemon (postrotate reload) so it reopens files. Without rotation, one chatty service eventually fills the disk and takes the host down with it.

journald retention is separate from logrotate: write SystemMaxUse=500M under [Journal] in /etc/systemd/journald.conf.d/10-limits.conf, apply it with sudo systemctl restart systemd-journald, and trim on demand with sudo journalctl --vacuum-size=500M (vacuum needs root). logrotate never touches the binary journal.

Progress So Far

Testing & Validation

logger "test-event-tyf" && journalctl -n 1 | grep test-event-tyf

You should see the test event in the local journal (and on the collector if forwarding). If so, your logging pipeline is working.

Troubleshooting

SymptomLikely causeFix
journalctl shows no logs for a unitService logs to a file instead of stdout, or Storage=noneCheck journald.conf Storage setting and the unit's StandardOutput
Forwarded logs never arriveFirewall, wrong port, or TCP vs UDP mismatchTest with logger and tcpdump; @@ is TCP, @ is UDP
logrotate: error: state fileConcurrent runs or corrupt status fileCheck /var/lib/logrotate/status and ensure one daily cron run
Logs stop after rotationDaemon still writes to the renamed file handleAdd postrotate with systemctl reload/kill -HUP so it reopens logs
Journal consumes too much diskSystemMaxUse unset on a busy hostWrite SystemMaxUse=500M under [Journal] in /etc/systemd/journald.conf.d/10-limits.conf, run sudo systemctl restart systemd-journald, then trim with sudo journalctl --vacuum-size=500M
Timestamps confusing across hostsMixed timezones in text logsStandardize on UTC via timedatectl and let the viewer localize
Troubleshooting
  • No logs on collector: confirm the collector listens on 514 and firewall allows it.
  • Journal too large: cap with SystemMaxUse= in journald.conf.
  • logrotate not running: it is triggered by a daily timer; test with logrotate -f.

Extension Ideas

  • Feed centralized logs into the Automated SOC Pipeline.
  • Add structured logging (JSON) for easier parsing.
  • Forward over TLS (RELP) for confidentiality and reliability.

Key Results

  • Queried logs precisely by priority, time, and unit.
  • Centralized logs to a collector for cross-host correlation.
  • Bounded disk usage with rotation and retention.
  • Verified the pipeline end to end with a test event.

Key Takeaways

  • journalctl filters (unit, priority, since) make triage instant.
  • Forward logs centrally; local logs are tamperable evidence.
  • logrotate policy bounds disk: frequency, count, compression, postrotate.
  • Dry-run with -d before trusting a new rotation config.
  • UTC everywhere; localize at display time.
  • Structured JSON logs query far better than free text once you scale.
  • Rate limits such as RateLimitIntervalSec stop a noisy service from filling the disk.

Practice Challenges

Challenge 1: Breach timeline

Goal: using journalctl only, build a minute-by-minute timeline of SSH logins and failures for one day, exported to a file.

Hint: -o short-iso plus grep and sort.

Challenge 2: Forward and verify

Goal: forward a test host's auth logs to a collector, generate login events, and prove they arrived within seconds.

Hint: logger -p authpriv.info creates test events.