You will provision server accounts with role-based groups, scoped sudo rules, password aging, and lockout on failed logins. By the end, access is least-privilege and every privileged action is auditable.

Learning Objectives

  • Create role groups and assign users to them.
  • Write a least-privilege sudoers drop-in safely.
  • Apply password aging and account lockout.
  • Time: ~2 hours · Difficulty: Intermediate · Prereqs: a server with root/sudo.

Architecture Overview

Environment Setup

You will need: shadow-utils, sudo, and PAM faillock (default on RHEL via the authselect with-faillock profile; on Ubuntu/Debian add the pam_faillock lines to /etc/pam.d/common-auth yourself, since it is not enabled by default there).

Before you begin: keep a second root session open while editing sudoers.

Prerequisites

  • Linux with shadow-utils, sudo, and faillock installed (libpam-modules on Debian/Ubuntu, pam_faillock on RHEL); confirm pam_faillock is wired into the auth stack before relying on lockout.
  • Root or sudo access for identity changes.
  • A service account name for the lab (deploy used here).
  • The fundamentals user admin lab for passwd/shadow/group basics.

Step-by-Step Execution

01
Create a role group and user

Groups let you grant access by role rather than per user, which scales cleanly.

sudo groupadd ops && sudo useradd -m -s /bin/bash -G ops deploy && sudo passwd deploy && id deploy
[ROOT REQUIRED] Creates the ops group and a member user.

Expected output:

(passwd prompts twice for the new password)
uid=1002(deploy) gid=1002(deploy) groups=1002(deploy),1003(ops)

What this teaches: Role groups (ops) decouple who from what: users join the group, and permissions attach to the group, so onboarding is one usermod.

02
Grant a scoped sudo rule with visudo

Editing through visudo validates syntax so a typo cannot lock everyone out of sudo.

echo '%ops ALL=(root) /usr/bin/systemctl restart nginx, /usr/bin/journalctl' | sudo EDITOR='tee' visudo -f /etc/sudoers.d/ops && sudo chmod 0440 /etc/sudoers.d/ops && sudo visudo -cf /etc/sudoers.d/ops

Expected output:

/etc/sudoers.d/ops: parsed OK

What this teaches: sudoers.d drop-ins keep custom rules out of the main sudoers file; EDITOR='tee' with visudo -f validates syntax before the file goes live. The explicit chmod 0440 matters because sudo silently ignores drop-ins with wrong permissions; visudo -cf then confirms with parsed OK. Test the grant with sudo -l -U deploy before relying on it.

03
Apply password aging and lockout

Expected output:

# chage -M 90 -W 7 deploy && faillock --user deploy
deploy: When Type Source Valid (no failed logins)

What this teaches: chage sets rotation policy while faillock tracks failures; together they implement aging plus brute-force lockout at the PAM layer. Tune the deny, unlock_time, and fail_interval thresholds in /etc/security/faillock.conf to match your policy, and confirm lockout actually triggers before depending on it.

How It Works

Server identity management scales through indirection: users belong to role groups, sudo rules reference the groups, and policy (aging, lockout) attaches to accounts. Adding a team member becomes group membership plus a key, and removing one is a single deletion. Per-user bespoke rules are the anti-pattern this replaces.

/etc/sudoers.d/ exists so packages and admins stop editing one shared file: each drop-in is validated by visudo independently, and naming them numerically (10-ops) controls evaluation order. The %group syntax grants to every member, which is why the role-group plus sudoers.d combination is the standard server pattern.

faillock closes the brute-force loop that password aging leaves open. PAM's faillock module counts failures per user and temporarily denies authentication after the threshold, with automatic unlock after the configured interval. chage -l and faillock --user each audit one half of the policy.

Progress So Far

Testing & Validation

sudo -l -U deploy && chage -l deploy

You should see only the two allowed sudo commands and the 90-day aging policy. If so, least-privilege access is enforced.

Troubleshooting

SymptomLikely causeFix
sudoers.d file ignoredWrong permissions or missing #includedir directiveFiles must be 0440 and not end in ~; confirm #includedir /etc/sudoers.d in sudoers
Rule order gives wider access than intendedLater entries can override earlier ones; last match winsName drop-ins so specific rules sort after general ones
faillock not triggeringpam_faillock missing from the auth stackCheck /etc/pam.d/common-auth (Debian) or system-auth (RHEL) includes it
Locked-out admin with no consolefaillock applied to the only sudoerRecover via console/single-user and faillock --user admin --reset
Group sudo rule not applyingUser added to group but session predates itLog out and back in; verify with id
NOPASSWD scope creepBroad NOPASSWD lines defeat password re-authScope NOPASSWD to specific binaries, keep the rest passworded
Troubleshooting
  • Broken sudoers locks out sudo: use the open root session and run visudo -c to find the error.
  • Group change not effective: the user must log out and back in.
  • Account locked: reset with faillock --user deploy --reset.

Extension Ideas

  • Centralize accounts with LDAP or FreeIPA for many servers.
  • Pair with SSH Hardening for key-only access.
  • Ship sudo logs to your SIEM from Log Management.

Key Results

  • Granted access by role group rather than per-user edits.
  • Limited sudo to 2 explicit commands, fully audited.
  • Enforced 90-day password aging with lockout on failures.
  • Confirmed effective privileges with sudo -l.

Key Takeaways

  • Role groups plus sudoers.d drop-ins scale; bespoke per-user rules do not.
  • Validate every sudo change with visudo; syntax errors lock out elevation.
  • chage handles aging; faillock handles brute-force lockout.
  • Last match wins in sudoers: order drop-ins deliberately.
  • Group membership applies at next login.

Practice Challenges

Challenge 1: Onboarding script

Goal: write a script that creates a user, adds them to ops, sets 90-day aging, and prints a verification summary with id and chage -l.

Hint: make it idempotent with id checks.

Challenge 2: Lockout drill

Goal: configure faillock for 3 attempts / 10-minute unlock on a test account, trigger it, verify denial, then reset.

Hint: faillock --user test --reset clears it.