Replay
A running agent joins the present. Reading what already happened is a separate, deliberate act — and one you want, in two situations that look different and are the same mechanism:
- before arming a rule, to see what it would have done to real logs;
- after an attack has begun, to act on the part of it that happened before anyone was watching.
A live tail joins the present
Starting the agent reads from the end of each file it watches. It does not resume a position it saved before it stopped, and this is deliberate: you stopped it for a reason, the file may have rotated meanwhile, and replaying a day of ordinary traffic as if it had all just happened would have any windowed rule ban whoever appears in it.
A file that appears after the agent started is read whole — it is all present. So is the successor of a rotation.
The one hand-off that does exist lives inside a single run: when
replay_on_start reads the past at boot, the live tails pick up exactly
where the reading stopped — same process, same life, never a position
persisted across a restart.
The rule that makes it useful
A replay does not simply ask "is this event recent". It asks:
would the sanction this evidence earns still be in force now?
Window and ban duration, not window alone. An attack five hours old that earns a six-hour ban still earns it; the same attack earning five minutes earns nothing. That is what lets someone arrive late and still stop who is behind it, and it is what a tool that only checks recency cannot do.
Events are dated by the line
For this to mean anything, each event must carry its own time. journald records it; a file needs its parser to declare where it is:
time_field: time
time_format: nginx # nginx, apache, syslog, rfc3339, unix, or a Go layout
Without it, every line of a week-old file claims to have happened now, and a ten-minute window swallows a month. A dry run counts the lines it had to skip for that reason and says so.
Three ways to run one
See, without touching anything
shieldlist-agent -dry-run 24h
shieldlist-agent -dry-run 0 -rule wp-bruteforce # 0 = everything
Reads, computes, prints, enforces nothing, writes nothing. Safe on a production machine at any hour.
read 48210 lines from 2 files
recognised 1204
covering 2026-08-12T22:29:16Z to 2026-08-13T21:30:16Z
would ban 3
too old to matter 11
rule_ssh-bruteforce 3
2026-08-13T21:30:16Z ssh-bruteforce 203.0.113.9 would ban until 2026-08-14T03:30:16Z
| Aug 13 21:29:16 host sshd[1]: Failed password for root from 203.0.113.9 port 54122 ssh2
At startup
replay_on_start: 6h
The agent reads that much of its logs' past before joining the present, and enforces what still applies. Off unless you ask for it. This is the tidiest way to say "catch up on what you missed": restart it.
It happens inside the agent's own process, which is why it needs nothing special — it already holds the store and the kernel's sets.
On demand
shield replay -last 8h
shield replay # everything
Asks the running agent to do the same thing now. The agent does the reading, for the same reason: nobody else may write its store or its tables behind its back.
A replay over a large history takes minutes, and shield waits for the
answer:
the last 8h read 1204882 lines, acted on 37, 219 too old to matter
What it does not do
- journald replay works, but a journal that has been vacuumed no longer holds what it forgot.
- A replay judges each crossing on its own evidence. It does not reconstruct an offender's escalation history and apply the fourth rung of a ladder — it applies what the evidence in front of it earns.
- Nothing is written to your log files. A replay reads them and nothing else.
Bounded, and beside the live watch
Reading the past runs beside the live watch, never instead of it: the agent keeps detecting while it judges, and acts on the verdicts when they land. What a replay holds is bounded by the agent's memory budget; if the past outgrows it, the newest evidence is judged — what can still be in force — and the answer says how many older hits were dropped, never silently.