Back to posts
Post

Prevent /etc/passwd and /etc/shadow Write Attempts with auditd Rules

Use auditctl and augenrules to monitor and block unauthorized writes to critical password files in real time via systemd integration.

Siber Güvenlikauditdsystemdlog monitoringfile integrity

I’ve seen too many admins rely solely on file permissions to protect /etc/passwd and /etc/shadow. While 644 and 600 are essential, they don’t stop a compromised service or misconfigured script from trying to write to these files. That’s where auditd comes in — not just for logging, but for active prevention when paired with systemd.

Why auditd Rules Beat Just Watching Logs

The audit subsystem doesn’t just record events — it can trigger actions. By setting a watch on /etc/passwd and /etc/shadow with auditctl -w, you generate an audit record every time someone opens these files for writing. But logging alone isn’t enough if you’re asleep or your SIEM is delayed. The real power comes when you bind that audit event to a systemd service that can kill the offending process or alert you instantly.

Setting Up the Watch with augenrules

Instead of running auditctl commands directly (which don’t survive reboots), I use augenrules to make rules persistent. Create a file in /etc/audit/rules.d/:

# /etc/audit/rules.d/99-protect-passwd.rules
-w /etc/passwd -p wa -k passwd_write
-w /etc/shadow -p wa -k shadow_write

The -p wa flag means watch for write and attribute changes. The -k tag lets us filter these events later. After saving, reload the rules:

augenrules --load
systemctl restart auditd

Verify it’s active with:

auditctl -l | grep -E "passwd|shadow"

You should see both watches listed.

Binding audit Events to systemd for Real-Time Response

Now comes the integration part. auditd can execute a script when it sees a matching event via the auditd rules’ systemd action. But since auditd doesn’t natively call systemd units directly, I use a small daemon script that watches the audit log and triggers a systemd service.

First, create a response script:

# /usr/local/bin/block-passwd-write.sh
#!/bin/bash
# Triggers when auditd logs a write to /etc/passwd or /etc/shadow
PID=$(ausearch -k passwd_write -k shadow_write -i --raw | aureport -f -i | tail -n 1 | awk '{print $2}')
if [ -n "$PID" ]; then
    logger -t audit-response "Blocking PID $PID for attempting to write to /etc/passwd or /etc/shadow"
    kill -9 $PID
    systemctl start alert-passwd-breach.service  # optional: trigger alerting
fi

Make it executable:

chmod +x /usr/local/bin/block-passwd-write.sh

Then create a systemd service to run this script on demand:

# /etc/systemd/system/audit-passwd-response.service
[Unit]
Description=Response to unauthorized /etc/passwd or /etc/shadow write
After=auditd.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/block-passwd-write.sh

[Install]
WantedBy=multi-user.target

Now, configure auditd to invoke this service when a matching event occurs. Edit /etc/audit/auditd.conf and set:

admin_space_left_action = halt
# Not ideal, but we want immediate response — better approach below

Actually, a cleaner way is to use auditd’s exec action in rules — but that’s limited. Instead, I rely on a separate log watcher. So create a second service that tails the audit log:

# /etc/systemd/system/audit-watchdog.service
[Unit]
Description=Watch audit log for critical file writes and trigger response
After=auditd.service

[Service]
Type=simple
ExecStart=/usr/local/bin/audit-log-watcher.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

With a watcher script like:

# /usr/local/bin/audit-log-watcher.sh
#!/bin/bash
while true; do
    if ausearch -k passwd_write -k shadow_write -i --raw | grep -q .; then
        /usr/local/bin/block-passwd-write.sh
        # Clear the buffer to avoid repeated triggers
        ausearch -k passwd_write -k shadow_write -i --raw > /dev/null
    fi
    sleep 2

done

Enable and start both services:

systemctl daemon-reload
systemctl enable audit-watchdog.service
systemctl start audit-watchdog.service

Testing the Setup Safely

Never test on a production auth file directly. Instead, create a copy:

cp /etc/passwd /tmp/test-passwd
chmod 644 /tmp/test-passwd
# Temporarily watch the test file
 auditctl -w /tmp/test-passwd -p wa -k test-passwd

Then try to write to it:

echo "test:x:1001:1001::/home/test:/bin/sh" >> /tmp/test-passwd

Check if the watcher caught it:

journalctl -u audit-watchdog.service -f

You should see the script trigger, log the PID, and kill the process.

Why This Beats Just Using inotify

I’ve used inotifywait before for file watching, but it has limitations: it can miss events under heavy load, doesn’t scale well to many files, and lacks the kernel-level auditing integrity of auditd. auditd runs in kernel space, so it catches syscalls even if the process tries to evade detection. Combined with systemd, it gives you a reliable, tamper-resistant response layer.

Note: Avoid False Positives

Legitimate tools like pwck, usermod, or backup scripts may trigger this. To reduce noise:

  • Whitelist known PIDs or comm values in your watcher script
  • Use -k tags to separate critical vs. routine watches
  • Test your backup and user management workflows first

Tip: Combine with alerting

Instead of just killing the process, have the response service send a Slack or Telegram alert via curl, or trigger a PagerDuty event. I covered alerting from systemd in a previous post — see my guide on [systemd Slice ile Bellek ve IO Sınırlaması](https://furkanikkan.com/urun/systemd-slice-ile-bellek-ve-io-sinirlamasi-kaynak-izolasyonu-rehberi-88) for how to structure responsive services.

This approach turns passive logging into active defense. It won’t stop a determined rootkit, but it will catch and stop casual misuse, misconfigurations, or compromised services trying to tamper with your auth files — and that’s worth the setup time.


Cover image: slee144 · CC0 (Openverse / kamu malı) · https://www.flickr.com/photos/197519814@N08/52656069487