Operations 44 min read

Linux Least Privilege: Mastering sudo, Groups, and ACLs for Secure Access Control

This comprehensive guide demonstrates how to implement least-privilege access in Linux using sudo, user groups, and ACLs, with practical examples covering identity verification, permission layers, service sandboxing, testing strategies, and account offboarding.

Ops Community
Ops Community
Ops Community
Linux Least Privilege: Mastering sudo, Groups, and ACLs for Secure Access Control

Clarify Identity, Session, and Resource Ownership

Start by verifying the actual identity of the current session using id and getent passwd / getent group to query the system's configured identity sources (local files or directory services). Numeric IDs are what the filesystem stores; group names are just labels. Use who and ps -eo user,group,pid,ppid,args to list active sessions and processes — locking an account does not terminate existing sessions or processes. Check the full path permissions with namei -l /path/to/file because a missing execute bit on any parent directory blocks access. Examine numeric ownership and mode with stat -c '%n uid=%u gid=%g mode=%a type=%F' /path and note that ACLs can make the traditional group permission bit reflect the ACL mask rather than the actual group rights. Finally, inspect mount options with findmnt -T /path -o TARGET,SOURCE,FSTYPE,OPTIONS since noexec or read-only mounts can override permission bits.

Define a Task Matrix Before Granting Access

Create a task matrix that separates duties (e.g., read logs, write releases, reload service) and explicitly lists forbidden actions (edit unit files, arbitrary root shell, read secrets without need). This prevents backward reasoning from "which group to add" and enables positive and negative testing later.

Create Accounts and Groups: Separate People from Services

Create role-based groups ( app-readers, app-writers, app-operators, app-editors) with groupadd. Create human accounts ( alice, bob) with useradd --create-home --shell /bin/bash and a system service account ( appsvc) with

useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin

. Add users to supplementary groups using usermod -aG to preserve existing memberships. Verify group membership in a new login session ( sudo -iu alice id) because existing sessions retain old credentials. Set account expiration with chage -E 2026-12-31 alice and document the purpose in the comment field (

usermod --comment 'Application runtime account; owner=platform' appsvc

).

Traditional Permission Bits First, Then ACLs

Create a shared directory with setgid so new files inherit the group:

sudo install -d -o root -g app-writers -m 2770 /srv/app/releases

. For existing directories, change group ownership and setgid non-recursively to avoid affecting unrelated files:

sudo chgrp app-writers /srv/app/releases && sudo chmod g+s /srv/app/releases

. Set a restrictive umask ( umask 0027) for new objects. Use conditional execute ( chmod -R g+rX) to add execute only to directories and already-executable files. Apply the sticky bit ( chmod 1777) on world-writable drop-box directories to prevent users from deleting each other's files. Audit world-writable files with sudo find /srv/app -xdev -perm -0002 -printf '%m %u %g %p\n' before fixing.

ACLs for Multi-Role Access: Understand Mask and Inheritance

View ACLs with getfacl -p /path and back them up ( sudo getfacl -R -p /srv/app > app-acl-before.txt). Grant directory traversal and file read to a group: sudo setfacl -m g:app-readers:rx /srv/app/releases. Give a specific user read access to a single file: sudo setfacl -m u:bob:r /srv/app/logs/app.log. Set default ACLs for inheritance:

sudo setfacl -m d:u::rwx,d:g::rwx,d:g:app-readers:rx,d:g:app-writers:rwx,d:m::rwx,d:o::--- /srv/app/releases

. The mask limits effective permissions for named users, named groups, and the owning group; reduce it with sudo setfacl -m m::rx /srv/app/releases and verify with getfacl. When expanding the mask, first lower undesired entries. Apply ACLs recursively to existing trees by separating directories and files:

sudo find /srv/app/logs -xdev -type d -exec setfacl -m g:app-readers:rx {} +

and

sudo find /srv/app/logs -xdev -type f -exec setfacl -m g:app-readers:r {} +

. Remove a specific entry with sudo setfacl -x u:bob /srv/app/logs/app.log.

sudo: Allow Only Explicit Actions and Audit Dependencies

Check a user's effective sudo rights with sudo -l -U alice (covers all included files). Review administrative groups ( getent group sudo, getent group wheel) and sudoers includes ( sudo grep -nE '^%|includedir|include' /etc/sudoers). Edit sudoers with visudo -f /etc/sudoers.d/app-operators. Write tight rules:

%app-operators ALL=(root) /usr/bin/systemctl reload app.service

. Prefer a wrapper script that validates arguments and cleans the environment:

#!/usr/bin/env bash; set -euo pipefail; [[ $# -eq 1 && "$1" == reload ]] || { echo 'usage: app-control reload' >&2; exit 64; }; exec /usr/bin/env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin /usr/bin/systemctl reload app.service

. Install it securely:

sudo install -o root -g root -m 0755 app-control /usr/local/sbin/app-control

and verify the path chain with namei -l /usr/local/sbin/app-control. Define a command alias in sudoers:

Cmnd_Alias APP_RELOAD = /usr/local/sbin/app-control reload; %app-operators ALL=(root) APP_RELOAD

. Validate the service unit's full configuration ( systemctl cat app.service,

systemctl show app.service -p User,Group,ExecStart,ExecReload,EnvironmentFiles

) and ensure the executable and its dependencies are not writable by the authorized user ( namei -l /etc/systemd/system/app.service, namei -l /usr/local/sbin/app-control). Use sudoedit only for vetted low-risk parameter files. Check the sudo environment with

sudo /usr/bin/env | awk -F= '$1~/^(PATH|HOME|USER|LOGNAME|SHELL)$/ {print}'

. Clear authentication cache for testing with sudo -k.

Service Self-Hardening: Sandbox the Runtime

In the systemd unit, set a dedicated user/group, supplementary groups, and umask:

[Service]; User=appsvc; Group=appsvc; SupplementaryGroups=app-readers; UMask=0027

. Apply sandboxing:

NoNewPrivileges=yes; ProtectSystem=strict; ProtectHome=yes; ReadWritePaths=/srv/app/data /srv/app/logs

. Grant only required capabilities:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE; AmbientCapabilities=CAP_NET_BIND_SERVICE

. Scan for file capabilities ( sudo getcap -r /srv/app) and setuid/setgid binaries (

sudo find /srv/app -xdev -type f \( -perm -4000 -o -perm -2000 \) -printf '%m %u %g %p
'

). Check SELinux status ( getenforce) and labels ( ls -Zd /srv/app /srv/app/data /srv/app/logs); review recent denials with sudo ausearch -m AVC,USER_AVC -ts recent. For AppArmor, run sudo aa-status and check kernel logs (

sudo journalctl -k --since '-30 min' --no-pager | grep -i apparmor

).

Validate with Allow and Deny Tests

Test positive access: sudo -u bob head -n 1 /srv/app/logs/acceptance.log. Test negative access: attempt to create a file as the reader and expect failure. Verify writer creates files with correct group and mode:

sudo -u alice touch /srv/app/releases/acceptance-file; stat -c '%u %g %a %n' /srv/app/releases/acceptance-file; getfacl -p /srv/app/releases/acceptance-file

. Preview ACL changes with

sudo setfacl --test -m g:app-readers:rx,m::rwx /srv/app/releases

. Restore from backup only after reviewing diffs: sudo setfacl --restore=app-acl-before.txt. Test sudo rule matching without execution: sudo -l -U alice /usr/local/sbin/app-control reload and verify denial of unwanted commands: sudo -l -U alice /bin/bash. Monitor sudo logs: sudo journalctl _COMM=sudo --since '-1 hour' --no-pager.

Handle Copies, Deployments, and Containers

Preserve ACLs and xattrs in backups: sudo rsync -aHAX --numeric-ids /src/ /dst/ and sudo tar --acls --xattrs -cpf archive.tar -C /src dir. Moving files ( mv) may retain source permissions instead of inheriting target default ACLs — test the actual deployment pipeline. Configure logrotate to create new files with the right ownership and group:

/srv/app/logs/app.log { daily; rotate 7; create 0640 appsvc app-readers }

and preview with sudo logrotate --debug /etc/logrotate.d/app. In containers, inspect user namespace mapping:

docker inspect container --format '{{json .Config.User}} {{json .HostConfig.UsernsMode}}'

. Check the running process's actual credentials: sudo awk '/^(Uid|Gid|Groups|CapEff):/' /proc/PID/status. List open files held by the service user: sudo lsof -u appsvc — permission changes do not revoke already-open descriptors.

Offboarding and Auditing: Remove All Access Vectors

Remove group membership: sudo gpasswd -d alice app-writers (plan session termination). Lock the password: sudo usermod -L alice; sudo passwd -S alice — but also revoke SSH keys, certificates, directory service entries, sudoers rules, and ACLs. Find files owned by the user's UID: sudo find /srv/app -xdev -uid 1001 -printf '%u %g %m %p\n'. Review cron jobs and systemd timers:

sudo crontab -u alice -l; systemctl list-timers --all --no-pager

. Search for leftover ACL entries: sudo getfacl -R -p /srv/app | grep -n 'user:alice:'. Record the final authorization state in a structured document covering allowed/denied tasks, dependency review, positive/negative test results, and expiration review.

Three Illustrative Cases

Case 1 — Log Reading: Adding a troubleshooter to a system log group grants access to all service logs. Instead, isolate application logs in a dedicated directory, use a role group with default ACLs, verify traversal rights on each parent directory, and sanitize sensitive fields at log generation time. Case 2 — Service Restart: A fixed systemctl restart sudo rule seems narrow, but if the service runs as root and its start script lives in a publisher-writable directory, the publisher can replace the script and execute arbitrary code as root. Fix by moving the privileged start chain under admin control, running the service as a low-privilege user, and separating writable artifacts from privileged scripts. Case 3 — ACL Mask Side Effect: Adding a read entry for a user causes the tool to recalculate the mask, inadvertently restoring write access for a group that had a write entry but was previously masked. Always review the full ACL and effective permissions after any change; lower undesired entries before raising the mask, then test with representative identities.

Reference Materials

acl manual

setfacl manual

sudoers manual

useradd manual

systemd.exec documentation

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Access ControlLinuxuser managementACLpermissionsAppArmorSELinuxsystemdsudoleast privilege
Ops Community
Written by

Ops Community

A leading IT operations community where professionals share and grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.