Why this is worth reading
You already know a code change can look safe in diff form and still cause damage the first time it runs unattended. A systemd timer makes that risk worse because the unit does not run while you are watching; it runs from a calendar expression and may include boot-time catch-up. Static review of the .service and .timer files tells you what the author intended. It does not tell you what the binary changes, which paths are writable, whether network is disabled, or whether the schedule semantics match your maintenance window.
This article gives you a five-field runtime contract and a three-run replay harness for a candidate AI-generated timer. You reject the unit if the replay effect differs from the contract, even when the unit text looks reasonable.
For the example, I generated a log-cleanup unit pair with MonkeyCode's free model access and ran the audit on a container using its free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow remains useful if you delete those sentences and use any model output or a hand-written unit.
The candidate pair
Here is the input you treat as untrusted.
# /etc/systemd/system/log-cleanup.service
[Unit]
Description=Rotate old application logs
After=network-online.target
[Service]
Type=oneshot
User=appuser
EnvironmentFile=/etc/default/log-cleanup
ExecStart=/usr/local/bin/log-cleanup.sh --retention ${RETENTION_DAYS}
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log/app /var/lib/app
# /etc/systemd/system/log-cleanup.timer
[Unit]
Description=Weekly log cleanup
[Timer]
OnCalendar=Sun *-*-* 03:17:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
At first glance the unit is restrained. It drops privileges, uses a strict filesystem sandbox, and writes only to /var/log/app and /var/lib/app. But three problems are invisible: the command may require network despite the local log name, Persistent=true will fire missed jobs immediately after boot, and the ReadWritePaths list does not tell you whether /usr/local/bin/log-cleanup.sh is the real script or a shell wrapper that writes elsewhere.
The five fields you record before enabling the unit
Run the command, not the policy. For any candidate timer, fill in this table before you let systemctl enable --now happen.
| Field | Question | Minimum evidence |
|---|---|---|
| Identity | Which unit, user, group, and environment file are active? | systemctl show log-cleanup.service -p User -p Group -p EnvironmentFiles |
| Execution | What is the exact command and argument vector? | systemctl show log-cleanup.service -p ExecStart |
| Filesystem intent | Which paths are expected to be read and written? | Explicit ReadWritePaths plus the script's own --dry-run output |
| Network and isolation | Does the job need network, new privileges, or device access? |
systemd-analyze security log-cleanup.service plus a firewall or PrivateNetwork=true decision |
| Schedule | What is the normalized calendar and persistence behavior? | systemd-analyze calendar "Sun *-*-* 03:17:00" |
You write the contract as a small YAML file before you run the replay.
identity:
unit: log-cleanup.service
user: appuser
exec:
command: /usr/local/bin/log-cleanup.sh --retention ${RETENTION_DAYS}
files:
writes: [/var/log/app, /var/lib/app]
reads: [/var/log/app]
network:
required: false
policy: PrivateNetwork=true
schedule:
on_calendar: "Sun *-*-* 03:17:00"
persistent: false
randomized_delay_sec: 60
This contract is the acceptance test. If any subsequent replay writes outside the declared paths, network becomes reachable, or schedule semantics differ, you reject the unit.
Replay the command three times before you enable the timer
You do not need to wait until Sunday. Strip the timer and run the service's ExecStart in a disposable root directory with the same isolation properties. The script below runs the command three times from an identical seed state and records file inventories and hashes.
#!/usr/bin/env bash
set -euo pipefail
UNIT=${1:?usage: timer_replay.sh log-cleanup}
WORK=/var/tmp/${UNIT}-audit
STATE=${WORK}/seed
OUT=${WORK}/out
mkdir -p ${STATE} ${OUT}
seed_state() {
rm -rf ${STATE}/rootfs
mkdir -p ${STATE}/rootfs/var/log/app \
${STATE}/rootfs/var/lib/app \
${STATE}/rootfs/usr/local/bin
echo seed-document > ${STATE}/rootfs/var/log/app/app.log
echo seed-document > ${STATE}/rootfs/var/lib/app/state.db
cp /usr/local/bin/log-cleanup.sh ${STATE}/rootfs/usr/local/bin/log-cleanup.sh
chown -R 65534:65534 ${STATE}/rootfs/var/log/app ${STATE}/rootfs/var/lib/app
}
seed_state
for run in 1 2 3; do
runroot=${WORK}/run-${run}
rm -rf ${runroot}
mkdir -p ${runroot}
cp -a ${STATE}/rootfs/. ${runroot}/rootfs
timeout 30 systemd-run --wait \
--unit=${UNIT}-audit-${run} \
--property=Type=oneshot \
--property=RootDirectory=${runroot}/rootfs \
--property=User=nobody \
--property=NoNewPrivileges=true \
--property=PrivateNetwork=true \
--property=PrivateTmp=true \
--property=ProtectSystem=strict \
--property=ReadWritePaths='/var/log/app /var/lib/app' \
/usr/local/bin/log-cleanup.sh --retention 7 \
|| { printf 'run %s failed\n' ${run} >&2; continue; }
(cd ${runroot}/rootfs && find . -xdev -type f -printf '%P %s\n' | sort) \
> ${OUT}/run-${run}.files
(cd ${runroot}/rootfs && find . -xdev -type f -print0 | sort -z | xargs -0 sha256sum) \
> ${OUT}/run-${run}.hashes
done
echo '--- file inventory, run 1 ---'
cat ${OUT}/run-1.files
echo '--- diff run 1 vs run 2 ---'
diff -u ${OUT}/run-1.hashes ${OUT}/run-2.hashes || true
Replace /usr/local/bin/log-cleanup.sh with the command under test and adjust the user ID or seed files for your candidate. The output you want is a deterministic filesystem effect, not a clean log. If the three runs produce different file sets or the diff shows writes under /var/www, /etc, or /root, reject the unit before you attach the timer.
A unit can be syntactically valid and still fail the contract
You should also confirm the schedule, because the timer part is where valid but wrong units cause operational pain.
systemd-analyze verify /etc/systemd/system/log-cleanup.service /etc/systemd/system/log-cleanup.timer
systemd-analyze calendar "Sun *-*-* 03:17:00"
If Persistent=true is present and you do not want a missed 03:17 cleanup to run immediately during a Monday morning boot storm, set it to false. The contract YAML should record the normalized trigger, not the author's raw string, so a reviewer does not have to recalculate ambiguity like *-*-* by hand.
Limits and who should not use this
This approach is not a sandbox escape test and it is not a substitute for reading the script. A replay with PrivateNetwork=true will not catch a unit that requires network in production; if the unit needs network, you must either mirror that exact setting or put it behind an allowlisted proxy before you trust it. Three deterministic runs do not catch a time bomb that only triggers after N runs, on a specific date, or when the host has a particular kernel then environment. The harness also resets filesystem state between runs, so it is wrong for services that intentionally mutate persistent state across executions.
Skip this if your candidate timer is a simple package-provided unit that you have already reviewed upstream. Keep this if you are accepting generated maintenance jobs, one-shot scripts, or anything that will run automatically at root-adjacent privilege on a server that matters.
Copy the contract first. The timer can stay disabled until the replay effect matches the five fields you wrote down.
Top comments (0)