Reported symptoms
At 08:20 the finance team reports that nothing on the shared drive will open.
Every name has gained a .locked suffix.
The share lives on rbdr-fileserver, on the office VLAN. The offsite copy lives
on rbdr-mirror-host at the second site, which pulls the share every morning at
06:00 with rsync -a --delete over SSH into /srv/rbdr-mirror/finance.
The operator who opens the incident at 08:26 expects this to be short. The offsite copy is on separate hardware, separate power and a separate site, and it synchronised at 06:04 this morning. The monitoring tile has been green every day for eleven months.
At 08:41 they list the destination. It holds orders.csv.locked and 3,179 other
encrypted names, and no earlier version of anything.
Evidence provided
The propagation behaviour was not inferred after the fact. It was measured beforehand on the same rsync build the job runs, and the capture’s third night is this incident in miniature: the source is encrypted in place, the scheduled run executes, and the destination is made to match.
$ rsync -a --delete src/ mirror/--- source after the attack ---
src/orders.csv.locked
src/reports/q3.txt.locked
--- mirror after that run ---
mirror/orders.csv.locked
mirror/reports/q3.txt.lockedWhether the mirrored ledger is still business data the capture settles by reading its first bytes rather than by asking the job.
$ od -c mirror/orders.csv.locked--- is the mirrored ledger still readable business data? ---
0000000 S a l t e d _ _ < 370 351 357 256 N J t
0000020 363 225 5 016 V r 034 . \0 220 274 ] 345 237 033 &
plaintext copies of the ledger remaining anywhere: 0The capture’s closing sentence is the one to carry into the incident: the mirror did exactly what it was configured to do, on schedule, with exit code 0, and it did it to the only other copy of the data.
The second piece of evidence is this morning’s job log. Nothing in it is wrong.
$ tail -12 /var/log/rbdr-mirror/sync-2026-08-27.logNumber of files: 3,214 (reg: 3,180, dir: 34)
Number of created files: 3,180 (reg: 3,180)
Number of deleted files: 3,180 (reg: 3,180)
Number of regular files transferred: 3,180
Total transferred file size: 2,041,118,720 bytes
sent 121,946 bytes received 2,043,551,802 bytes 4,431,721.19 bytes/sec
total size is 2,041,118,720 speedup is 1.00
started 06:04:11 finished 06:11:52
rsync exit status: 0Illustrative output
A normal run for this share moves eight to forty files and about 60 MiB. The monitor consumed one line of that log: the exit status.
The third piece is a negative — what the destination does not contain — together with two timestamps that separate the source’s clock from the mirror host’s.
$ stat -c '%n Modify:%y Change:%z' /srv/rbdr-mirror/finance/orders.csv.lockedorders.csv.locked Modify:2026-08-27 03:47:12 +0000 Change:2026-08-27 06:07:41 +0000Illustrative output
/srv/rbdr-mirror/ contains one directory for this share. There is no
--backup-dir target, no filesystem snapshot and no versioned object store
behind it. The firewall flow log for the night records no session from the
office VLAN to rbdr-mirror-host; the only session between the two hosts is the
mirror host’s own outbound SSH connection at 06:04:09.
Work the evidence before reading on
- The run created 3,180 files and deleted 3,180 files. Which line of the job log would a monitor have had to read to see that, and which line did it actually read?
- The destination modification times fall inside the attack window and the change times fall inside the synchronisation window. What does each of those two facts establish on its own?
- The measured run ended at exit 0. What would rsync have had to observe in order to exit anything else?
- If the job had run hourly instead of daily, what would have been different at 08:41?
Root cause
The destination was defined as a copy of the present
rsync -a --delete does not copy events. It makes a destination match a source:
entries present only at the destination are removed, and entries whose name or
content differs are replaced. Under that definition an accidental deletion, a
truncation and an in-place encryption are all differences to be resolved, and
resolving them is the job working correctly.
Because the destination retained no prior version, resolving the difference consumed the only surviving copy of the pre-attack state. Nine hours of exposure became total loss at 06:11:52.
The monitor watched the job result, not the change
The check exported two series: the exit status of the last run, and the age of the last successful run. Both stayed correct, because the run genuinely succeeded. Neither is a function of what the run did.
That the job succeeded does not mean the data can be restored — the two are independent, and this run is the proof. The numbers that were functions of what happened — files created, files deleted, bytes transferred — were written into the log by rsync and read by nobody. Each moved by three orders of magnitude at 06:04.
The defects compose. History without monitoring makes the incident survivable and slow; monitoring without history makes it visible and still unrecoverable. Neither leaves one recovery point, overwritten on schedule.
The hypothesis the evidence eliminates
The incident was first handled as a second intrusion — the attacker reached the mirror host too — which is the reasonable reading of an encrypted offsite copy, and it cost forty minutes.
The timestamps rule it out. rsync -a preserves modification times from the
source, so the destination files carry the source’s encryption times of 03:41 to
03:58. Their inode change times are 06:04 to 06:11, set by the local kernel when
the file was written and settable by no remote process. The destination bytes
were written by the synchronisation, not by anything acting on the mirror host
at 03:47.
Reachability confirms it. The mirror runs as a pull, so the mirror host opens the connection; the office VLAN cannot open a session to it at all, and the flow log for the night shows none.
Resolution
Stop the timer on the mirror host before anything else; the next scheduled run will re-apply the same transformation to whatever is restored beside it. Take a read-only copy of the encrypted destination — it is the best surviving record of what the source held at 03:58.
Then locate a copy the mirror never touched. Here that was the quarterly export in a separate repository, eleven days old, plus locally cached spreadsheets on four finance workstations. Recovery was assembled from those and re-keyed transactions; the loss window was eleven days, not nine hours.
Rebuild before restoring into it. Add --backup --backup-dir= pointing at a
per-run directory, which rsync(1) documents as storing every displaced or
deleted file on the receiving side instead of discarding it. Add
--max-delete=200, which rsync(1) documents as skipping all further deletions
once the limit is exceeded, warning, and exiting with error code 25 — on 3,180
deletions it stops the run.
Verification
Encrypt a file in a scratch tree on the source, let the scheduled run execute, and read the pre-change bytes back out of the run backup directory at the destination; their checksum must match the value recorded beforehand.
Delete more files than the configured ceiling, run the job, and confirm it reports skipped deletions with a non-zero status and that the destination still holds them.
Confirm the job now exports files created, files deleted and bytes transferred as separate series, that the deletion series alerts against its own recent history rather than a fixed threshold, and that firing it pages somebody. Trip it deliberately to prove it.
Confirm from the firewall that the office VLAN still cannot reach the mirror host, and that the mirror host’s key on the source is restricted to the read-only transfer command.
Prevention
Name the failure classes the copy answers, and the ones it does not. This mirror answered device, host and site loss. It was obliged to reproduce deletion, corruption and encryption.
Keep one copy the source cannot modify. Pull-based transfer protects the destination host. Only history at the destination, or a repository under a retention lock the backup credential cannot shorten, protects the destination data.
Monitor change volume, not outcome. Exit status and last-success age are liveness checks for the schedule. Files created, files deleted, bytes transferred and destination object count are the series that move before the loss becomes permanent.
Put a ceiling on destructive change and let it fail closed. One option converts an unattended catastrophic run into an alert.
Retain longer than the detection latency. Ransomware is found hours to weeks after it lands; a destination that covers one day covers no realistic detection window.