I enforced least-privilege file access using the standard user/group/other model, special bits, and POSIX ACLs for finer control. Explicit, auditable modes and ACLs let multiple users share directories without over-granting access.

Objective & Context

Permissions are the first access-control layer on every Linux file. This lab covers octal modes, ownership, setuid/setgid/sticky bits, umask defaults, and ACLs for cases the three-class model cannot express, aligning to NIST AC-6 least privilege.

Learning Objectives

  • Read and set octal permission modes with chmod for files and directories.
  • Change file ownership and group ownership with chown and chgrp.
  • Apply setuid, setgid, and sticky bits and explain what each one does.
  • Predict default permissions from umask values.
  • Grant per-user access beyond the owner/group/other model using POSIX ACLs.

Environment & Prerequisites

Prerequisites

  • Linux with coreutils and the acl package: sudo apt install -y acl or sudo dnf install -y acl.
  • Root or sudo access, since changing ownership requires elevation.
  • Two or more test user accounts and a shared directory to experiment on.
  • Completion of the file management lab is recommended.

Step-by-Step Execution

1. Set explicit octal modes

chmod 750 /srv/app && chown app:web /srv/app

Expected output:

(no output on success; verify with: stat -c "%a %U:%G %n" /srv/app)
750 app:web /srv/app

What this teaches: Octal 750 means rwxr-x---: full control for the owner, read/execute for the group, nothing for others. Directories need the execute bit for traversal.

2. Grant a user access via ACL

setfacl -m u:deploy:rx /srv/app && getfacl /srv/app

Expected output:

# file: /srv/app
# owner: root
# group: web
user::rwx
user:deploy:r-x
group::r-x
mask::r-x
other::---

What this teaches: setfacl adds an access control entry for one user without changing the base mode; getfacl reveals the effective permissions, including the mask entry that caps every named user and group entry. If you later grant u:deploy:rwx but the mask stays r-x, deploy still gets only r-x, which is the number one reason ACL grants appear not to work.

3. Set a setgid directory for shared group ownership

chmod 2775 /srv/shared

Expected output:

(no output on success; verify with: stat -c "%a %n" /srv/shared)
2775 /srv/shared

What this teaches: The leading 2 is the setgid bit: new files in the directory inherit the directory's group instead of the creator's primary group, which keeps shared trees consistently owned.

How It Works

Every Linux file carries three permission triples: one for the owning user, one for the owning group, and one for everyone else. Each triple is three bits (read, write, execute), conventionally written as one octal digit, so 750 is a compact way of saying rwxr-x---. The kernel checks them in order and stops at the first class that matches the requesting process.

The special bits extend this model. setuid (4) on an executable runs the program as the file's owner, setgid (2) runs it as the file's group or forces group inheritance on directories, and the sticky bit (1) on a directory like /tmp lets users delete only their own files. umask then subtracts from the default 666/777 at creation time, which is why new files are 644 and new directories 755 under the common 022 umask.

POSIX ACLs exist because the three-class model cannot express rules like 'give deploy read access but nobody else outside the group'. An ACL attaches named user and group entries to the file, and getfacl shows a mask entry that caps the effective rights of all named entries. Checking the mask is the step most people skip when an ACL 'does not work'.

Validation & Testing

As each test user, attempt allowed and disallowed actions and confirm the kernel enforces the expected result. Pass criteria: owner/group/other behave per mode, the ACL grants exactly the named user, and setgid propagates group ownership to new files.

Troubleshooting

SymptomLikely causeFix
ACL grants access but the user is still deniedThe ACL mask entry caps effective permissions below the grantCheck getfacl output and raise the mask with setfacl -m m::rx
chmod 777 seemed necessary to make it workOwnership or group membership is wrong, not the modeFix with chown/chgrp and use 750/640 instead of opening to everyone
New files ignore the directory groupsetgid bit is missing on the shared directoryApply chmod g+s so new files inherit the directory group
Script fails with permission denied despite +xThe filesystem is mounted noexec, or the shebang interpreter lacks rightsRemount without noexec or move the script to an executable path
umask change had no effect on existing filesumask only applies at file creation timeAdjust existing files explicitly with chmod
setuid on a shell script does nothingThe kernel ignores setuid on interpreted scripts for safetyCompile a helper binary or use sudo rules instead
Advanced: Troubleshooting
  • Unexpected access: an ACL may override base bits; inspect with getfacl.
  • New files wrong group: set the setgid bit on the parent directory.
  • Too-open defaults: tighten umask (027) for service accounts.

Key Results

  • Enforced least privilege with explicit octal modes across service dirs.
  • Granted per-user access via ACLs without widening group membership.
  • Standardized shared directories with setgid for consistent ownership.
  • Hardened default creation modes with a stricter umask.

Key Takeaways

  • Octal modes encode three triples; the kernel stops at the first class that matches.
  • Directories need the execute bit for traversal, not just listing.
  • setgid directories keep shared trees under one group automatically.
  • umask only shapes newly created files; it never changes existing ones.
  • When ACLs misbehave, check the mask entry first.

Practice Challenges

Challenge 1: Secure a web root

Goal: set /srv/app so the owner has full control, group web can read and traverse, and others get nothing, then verify with stat.

Hint: one octal digit per class.

Challenge 2: Debug a denied deploy

Goal: a deploy user gets permission denied on a group-readable file; use namei -l and getfacl to find whether the blocker is the path, the group, or the mask.

Hint: every parent directory needs the execute bit.