sysadmin · May 29, 2026 · 13 min read

Password aging and scoped sudo.
chage, sudoers.d, and visudo.

A compliance audit wants password expiry policies on four accounts and passwordless sudo for the ops user, scoped to three commands. Done with chage and a validated sudoers drop-in.

// what we’re getting into
  1. The ticket
  2. Concept review. chage and sudoers syntax.
  3. Apply the password policies in bulk
  4. Force a password change on next login
  5. System-wide defaults in login.defs
  6. Build the sudoers.d drop-in
  7. Test ops1’s access
  8. Exam questions
  9. Final checklist: confirm everything works

The ticket: secadmin has two compliance items. Password aging on dev1, dev2, ops1, and dbadmin1: expire every 90 days, at least 7 days between changes, 14 days warning, and force dev1 to change at next login. And scoped sudo so ops1 can run systemctl, dnf, and firewall-cmd with no password, but nothing else. Use /etc/sudoers.d/ so it is reviewable, and validate the syntax before saving, because a sudoers lockout last quarter was a nightmare.

The real problem: a bad sudoers file can lock everyone out of privilege escalation. The whole point of this ticket is doing it the safe way, with validation.

What we are doing: password aging with chage, and a safe, scoped sudoers drop-in that is syntax-checked before it goes live.

The ticket

bash
for u in dev1 dev2 ops1 dbadmin1; do id $u; done
ls /etc/sudoers.d/

Concept review. chage and sudoers syntax.

bash
man chage
man sudoers
chage flags
-l  list current aging          -M  max days before a change is required
-m  min days between changes    -W  warning days before expiry
-I  inactive days after expiry  -E  account expiry date (YYYY-MM-DD or -1)
-d 0  force a change on next login
sudoers syntax
WHO   WHERE=(AS_WHOM)   [TAG:]   WHAT
# tags: NOPASSWD:  PASSWD:  NOEXEC:  EXEC:
ops1 ALL=(ALL) NOPASSWD: /usr/bin/systemctl, /usr/bin/dnf

Apply the password policies in bulk

Four accounts get the same policy, so loop over them. That is the real sysadmin move, and it is faster than four separate commands.

bash
# see the current state
for u in dev1 dev2 ops1 dbadmin1; do echo "=== $u ==="; sudo chage -l $u; done

# apply 90 / 7 / 14 to all four
for u in dev1 dev2 ops1 dbadmin1; do
    sudo chage -M 90 -m 7 -W 14 $u
done

# verify
for u in dev1 dev2 ops1 dbadmin1; do
    echo "=== $u ==="; sudo chage -l $u | grep -E "Maximum|Minimum|Warning"
done

Force a password change on next login

bash
sudo chage -d 0 dev1
sudo chage -l dev1    # 'password must be changed'

-d 0 sets the last-change date to the epoch, so the system treats the password as immediately expired and prompts at next login.

System-wide defaults in login.defs

chage changes existing users. To set the policy for every future user, edit /etc/login.defs. This does not touch existing accounts.

bash
grep -E "PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE" /etc/login.defs

sudo cp /etc/login.defs /etc/login.defs.bak
sudo sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS\t90/' /etc/login.defs
sudo sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS\t7/' /etc/login.defs
sudo sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE\t14/' /etc/login.defs
grep -E "PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE" /etc/login.defs

Build the sudoers.d drop-in

Never edit /etc/sudoers directly with a plain editor. Write a drop-in to /etc/sudoers.d/, set its mode to 440, and validate with visudo -c before trusting it.

bash
sudo bash -c 'cat > /etc/sudoers.d/ops1-scoped << "EOF"
# scoped sudo for ops1: services, packages, firewall
ops1 ALL=(ALL) NOPASSWD: /usr/bin/systemctl, /usr/bin/dnf, /usr/bin/firewall-cmd
EOF'

sudo chmod 440 /etc/sudoers.d/ops1-scoped   # sudoers ignores it otherwise
sudo visudo -c    # must report: parsed OK
warning: If visudo -c reports an error, fix it before you log out. A broken file in /etc/sudoers.d/ can disable sudo for everyone. The 440 permission and the syntax check are the two things standing between you and last quarter’s lockout.

Test ops1’s access

bash
sudo -l -U ops1    # what can ops1 run

sudo -u ops1 sudo systemctl status sshd     # allowed, no password
sudo -u ops1 sudo firewall-cmd --list-all   # allowed
sudo -u ops1 sudo cat /etc/shadow           # not in scope, refused

Exam questions

Write the command first.

Q1. Configure alice so her password expires every 60 days, with a 7-day minimum between changes and a 10-day warning.

Q2. Force bob to change his password at next login, without changing his other aging settings.

Q3. Create a sudoers drop-in giving auditor1 passwordless cat, grep, and tail. Validate the syntax.

Answers.

bash
# A1
sudo chage -M 60 -m 7 -W 10 alice
# A2
sudo chage -d 0 bob
# A3
sudo bash -c 'cat > /etc/sudoers.d/auditor1 << EOF
auditor1 ALL=(ALL) NOPASSWD: /usr/bin/cat, /usr/bin/grep, /usr/bin/tail
EOF'
sudo chmod 440 /etc/sudoers.d/auditor1
sudo visudo -c

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. all four users on the 90/7/14 policy
for u in dev1 dev2 ops1 dbadmin1; do sudo chage -l $u | grep Maximum; done

# 2. dev1 forced to change at next login
sudo chage -l dev1 | head -1

# 3. the drop-in exists at mode 440 and parses
ls -l /etc/sudoers.d/ops1-scoped; sudo visudo -c

# 4. ops1 can run the scoped commands but not others
# 5. all three exam commands written from scratch
# 6. tracker entry checked off

Reply to secadmin: all four accounts are on the 90/7/14 policy, dev1 changes at next login, and ops1 has passwordless sudo scoped to systemctl, dnf, and firewall-cmd via /etc/sudoers.d/ops1-scoped. visudo -c is clean.

next post
Partitioning a new disk for the database tier