Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

How I check what changed in sudo, SSH keys and SUID files on a Linux server

An access review always asks the same thing: "Who can get root on this box, and did that change since last quarter?" The first half is easy. The second half means reading /etc/sudoers.d, authorized_keys files and old ti

An access review always asks the same thing: "Who can get root on this box, and did that change since last quarter?"

The first half is easy. The second half means reading /etc/sudoers.d, authorized_keys files and old tickets, and I did not want to do that by hand every time. So I wrote a small tool called permdrift. This post is about the problem, the commands I use, and where the tool falls short.

What I actually want to know

Three places keep coming up when I look for quiet privilege changes:

  1. sudo. Someone adds a NOPASSWD rule "just for the migration" and it stays for two years.
  2. SSH keys. A new line shows up in ~/.ssh/authorized_keys. Nobody remembers who owns it.
  3. SUID files. A binary gets the setuid bit and now runs as its owner.

You can check each with a one-liner. find / -perm -4000 -type f works. So does grep -r NOPASSWD /etc/sudoers*. The trouble is comparing today with last month. A one-liner gives you a pile of output and no memory.

The approach

permdrift takes a snapshot of this state into a JSON file. Later you take another one and diff them. It is one static Go binary, standard library only, and it never changes anything on the host.

sudo permdrift snapshot -o base.json

Run it as root. As a normal user it cannot read /etc/sudoers or other people's ~/.ssh, and the snapshot will say so with warnings. Do not compare a root snapshot with a non-root one, you will get a lot of fake "added" lines.

Snapshots never contain key material or password hashes. For keys it stores the type, the SHA256 fingerprint, the comment and the options.

A real run

I do not want to paste output from a customer server, so this is a throwaway fake root built by the demo script in the repo (scripts/demo-root.sh). The output format is the same as on a real host. Here is the baseline:

$ permdrift snapshot --root /tmp/art-root -o base.json
snapshot written to base.json: 3 users, 4 groups, 1 ssh keys, 4 sudoers lines, 1 SUID/SGID files, 0 world-writable, 0 warnings

Then I changed three things. I added a sudoers drop-in for bob, gave bob an SSH key, and set the SUID bit on /usr/bin/editor. Second snapshot, then the diff:

$ permdrift diff base.json now.json
DRIFT DETECTED: 4 change(s): High 1, Medium 2, Low 1 | added 4, removed 0, changed 0

[HIGH  ] added   sudoers        NOPASSWD + ALL=(ALL) sudo rule added for bob in /etc/sudoers.d/90-ops: bob ALL=(ALL) NOPASSWD: ALL
[MEDIUM] added   ssh_key        SSH key SHA256:mdjRRFt76vijB/211jtzs1JjGFJR6LVIbEqfsGAoPR4 added for user bob (ssh-ed25519, comment "bob@workstation")
[MEDIUM] added   suid           /usr/bin/editor is now SUID (owner alice, mode 4755; new file or mode change)
[LOW   ] added   file_perm      sensitive file /etc/sudoers.d/90-ops added (mode 0440, alice:alice)

(I cut the two header lines at the top for width. The rest is exactly what it printed.)

The exit code is 1 when there is drift and 0 when there is none, so it works in cron or CI. If you only care about the loud stuff:

$ permdrift diff base.json now.json --min-severity high

That prints only the NOPASSWD line. Diffing a snapshot against itself prints "No drift detected." and exits 0.

The severity levels are my opinion. A key added for a user in the sudo group is High. A key for a normal user is Medium. If you disagree, there is an ignore file for patterns you do not want to see.

The SOC 2 access review use case

This is the main reason I wrote it. For an access review you need to show what access exists and that changes were noticed. The workflow I have in mind:

  1. Snapshot every host on a schedule and copy the JSON off the host.
  2. At review time, diff the first snapshot of the quarter against the last.
  3. Attach the diff to the review ticket. Every High line either maps to an approved change or becomes a finding.

I want to be clear about one thing. This helps you gather evidence. It does not make you compliant, and your auditor decides what counts as evidence, not me.

Limits, and the tools you should look at

The list of things permdrift does not do is longer than the list it does.

  • It is not a monitor. There is no daemon and no alerting. It compares two snapshots you took.
  • The baseline is not tamper-evident. An attacker with root can edit the host and the JSON file. Keep baselines somewhere else.
  • It only looks at permissions and access: users, groups, sudoers, authorized_keys, SUID/SGID files, a few sshd settings. No packages, services, cron or network.
  • Sudoers parsing is line-based. No alias expansion. LDAP and SSSD users are not seen.
  • Linux only.

If you need more, use something else:

  • statedrift is also a single Go binary, but it covers much more of the host and keeps a hash-chained history. If you want drift detection with integrity guarantees, start there.
  • osquery lets you query users, sudoers and SUID files with SQL. It is great at fleet scale, but you need more setup to get history and diffs.
  • Wazuh is a full HIDS with agents, rules and dashboards. It does continuous monitoring, which I do not.
  • AIDE watches files for changes in content and permissions. It is mature. But it tells you a file changed, not that a NOPASSWD rule appeared.

I see permdrift as something that sits next to AIDE, not a replacement for it. It answers a narrower question in plainer words.

Try it

It is v0.1.0, so expect rough edges. Static binaries for amd64 and arm64 are on the releases page, and go install github.com/permdrift/permdrift/cmd/permdrift@latest works too.

Repo: https://github.com/permdrift/permdrift

I would like to hear how you do this check today, and where the severity ratings are wrong for you. Issues are open.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.