A named-user ACL can still display rw- while the process behind that username can no longer open the file for writing. That is not contradictory output. On Linux extended access ACLs, a named user’s stored entry is only one part of the decision: the shared ACL mask limits the effective permissions of named users, the owning group entry and named groups. When that mask narrows, a stored rw- can become effective r--. The Linux acl(5) access algorithm documents that intersection, and it also documents the critical mode-bit correspondence: when an ACL mask exists, the file’s group-class mode bits correspond to that mask. A later chmod can therefore change effective named-user access without deleting the named-user entry.
The operational question is broader than “how do I make the writer work again?” The safe question is: what effective access does every relevant principal receive after the proposed repair? A writer-only-looking fix can recalculate the shared mask and awaken a second user’s latent write bit.
This playbook builds one disposable, owned Linux fixture around that failure. It records the environment, proves access through unprivileged opens, reproduces the chmod regression, demonstrates a dangerous repair branch, then restores the intended three-principal policy. The command traces below are a reproducible laboratory design and mathematically/documentationally expected results; they are not represented as observations from a target VM in this article.
Define the file-access policy before changing permissions
Treat permissions as a policy with positive and negative requirements, not as a sequence of successful administration commands. For this fixture, the regular file is owned by root:root; none of the three test identities is the owner, and none should have discretionary-access-control bypass capability. The intended access policy is simple enough to audit completely:
Principal | Read | Append | Acceptance meaning |
writer | allow | allow | application can consume and update the file |
reader | allow | deny | read-only identity remains read-only |
outsider | deny | deny | unrelated identity receives no file access |
The baseline access ACL is user::rw-, writer rw-, reader r--, group::---, mask::rw-, other::---. The mask is not an extra principal. It is the maximum effective rights for named users, the owning-group entry and named groups; owner and other are outside that mask. That distinction is explicit in Linux’s ACL documentation.
This narrow experiment sits inside broader system-administration responsibilities, but the acceptance criterion here is deliberately smaller: preserve exactly the file-access contract above, on the same inode, under the same relevant security conditions. Refonte’s broader administration page discusses access management and troubleshooting as part of the role; it does not establish the specialist ACL result used here.
Three signals must remain separate. Stored ACL text says what entries exist. Effective ACL rights account for the mask. Real operations determine whether the designated process can open the object in its actual path and security context. A root setfacl returning zero proves only that the ACL-changing operation succeeded. It does not prove the writer can append, and it says nothing about whether the reader was accidentally granted write.
The decision rule is therefore whole-policy: approve only when every intended allow succeeds, every intended deny fails, identity and file evidence still match, and no other security layer is being bypassed to force the result.
Build an isolated ACL-capable Linux baseline
Use a disposable VM you own, with a local filesystem on which the installed ACL tools can create and read an extended access ACL. Do not point this procedure at production data, NFS, SMB/CIFS, an overlay filesystem, or a pre-existing account. The setup below uses collision-checked names and refuses to reuse the lab path. It also sets a restrictive setup umask and gives the lab directory execute/search permission without directory listing or creation rights to the test users.
The exercise complements hands-on administrator foundations, but it is intentionally a controlled permission experiment rather than a career or certification exercise.
Run this setup as root only on the disposable VM:
#!/usr/bin/env bash
set -euo pipefail
umask 077
REV='ACL-LAB-2026-09-24-r1'
LAB='/var/tmp/refonte-acl-mask-20260924-r1'
FILE="$LAB/fixture.txt"
EVID="$LAB/evidence"
WRITER='aclw_260924'; WGROUP='aclwg_260924'
READER='aclr_260924'; RGROUP='aclrg_260924'
OUTSIDER='aclo_260924'; OGROUP='aclog_260924'
[[ $EUID -eq 0 ]] || { echo 'setup must run as root' >&2; exit 1; }
[[ ! -e "$LAB" && ! -L "$LAB" ]] || {
echo "REFUSE: lab path already exists: $LAB" >&2; exit 1;
}
for n in "$WRITER" "$READER" "$OUTSIDER"; do
getent passwd "$n" >/dev/null && {
echo "REFUSE: pre-existing user: $n" >&2; exit 1;
}
done
for g in "$WGROUP" "$RGROUP" "$OGROUP"; do
getent group "$g" >/dev/null && {
echo "REFUSE: pre-existing group: $g" >&2; exit 1;
}
done
for c in getfacl setfacl runuser findmnt namei stat getent useradd groupadd; do
command -v "$c" >/dev/null || { echo "missing command: $c" >&2; exit 1; }
done
mkdir -m 0711 "$LAB"
mkdir -m 0700 "$EVID"
printf '%s\n' 'inert-fixture-v1' > "$FILE"
chown root:root "$FILE"
chmod 0600 "$FILE"
FSTYPE=$(findmnt -T "$FILE" -n -o FSTYPE)
case "$FSTYPE" in
nfs*|cifs|smb3|9p|virtiofs|overlay|fuse.*)
echo "REFUSE: use a supported local ACL filesystem; found $FSTYPE" >&2
exit 1 ;;
esac
findmnt -T "$FILE" -n -o OPTIONS | grep -qw ro && {
echo 'REFUSE: filesystem is read-only' >&2; exit 1;
}
groupadd "$WGROUP"; useradd -M -N -g "$WGROUP" -s /usr/sbin/nologin "$WRITER"
groupadd "$RGROUP"; useradd -M -N -g "$RGROUP" -s /usr/sbin/nologin "$READER"
groupadd "$OGROUP"; useradd -M -N -g "$OGROUP" -s /usr/sbin/nologin "$OUTSIDER"
WUID=$(id -u "$WRITER"); RUID=$(id -u "$READER")
setfacl -b -- "$FILE"
setfacl -m "u::rw-,u:${WUID}:rw-,u:${RUID}:r--,g::---,m::rw-,o::---" -- "$FILE"
printf '%s\n' "$REV" > "$EVID/fixture-revision"
getfacl -p -n -- "$FILE" > "$EVID/acl.baseline.restore"
getfacl -p -n -e -- "$FILE" > "$EVID/acl.baseline.display"
stat -Lc '%d:%i' -- "$FILE" > "$EVID/device-inode.baseline"
sha256sum "$EVID/acl.baseline.restore" > "$EVID/acl.baseline.restore.sha256"The script deliberately does not delete colliding users, groups or files. A collision is a stop condition, because silently taking over a pre-existing identity would destroy the evidentiary value of the lab and could damage a machine you do not intend to alter. This scope is Linux local-filesystem access ACLs only; NFSv4 ACLs, SMB/Windows ACL semantics and remote-server authorization are different models and are outside the acceptance claim.
The directory modes are intentional. 0711 on the lab path supplies search/traversal to the test identities but not directory listing or file creation, while the root-only 0700 evidence directory prevents the probe users from rewriting the acceptance record. The regular file begins root-owned so the named-user branch, rather than the owner branch, is exercised. These are preconditions to the known-answer calculation: changing them changes which access-control path may decide the operation.
Capture the runtime manifest immediately after setup. The exact values are evidence to retain, not values to invent in an article:
{
echo "fixture_revision=$REV"
echo "setup_umask=$(umask)"
uname -a
cat /etc/os-release
getfacl --version
setfacl --version
runuser --version | head -n1
stat --version | head -n1
findmnt --version | head -n1
command -v dpkg-query >/dev/null && dpkg-query -W acl || true
command -v rpm >/dev/null && rpm -q acl || true
findmnt -T "$FILE" -o SOURCE,TARGET,FSTYPE,OPTIONS
stat -Lc 'mode=%A(%a) uid=%u gid=%g dev=%d inode=%i' -- "$FILE"
namei -l -- "$FILE"
id "$WRITER"; id "$READER"; id "$OUTSIDER"
lsattr -d -- "$FILE" 2>/dev/null || true
cat /sys/kernel/security/lsm 2>/dev/null || true
command -v getenforce >/dev/null && getenforce || true
command -v aa-status >/dev/null && aa-status --enabled 2>/dev/null || true
} | tee "$EVID/runtime-manifest.txt"Record versions rather than saying “latest.” The documentation used for this playbook was rechecked on September 24, 2026, but an access date is not an environment lock. The Linux man pages describe implementation behavior; POSIX.1e Draft 17 is an abandoned draft, not a current ratified POSIX ACL standard, although Linux documents its ACL implementation against that model.
The manifest is also the boundary between a reproducible experiment and a copied command sequence. If another administrator obtains a different filesystem type, acl utility version, kernel, LSM state, UID mapping or mount option, that difference belongs in the comparison. Do not silently rewrite the expected trace to fit an unexpected result. Preserve the mismatch and determine whether it changes the access algorithm, tool behavior or merely the presentation before accepting the repair.
Capture the initial three-principal access matrix
Before introducing the defect, prove that the known-answer fixture behaves as designed. The getfacl(1) manual documents the ACL display and effective-rights comments; -p preserves an absolute path, -n avoids converting identifiers back to names, and the effective comment exposes entries limited by the mask.
Use functions that make the designated unprivileged shell open the file. Unique append markers create a small deterministic content ledger without copying real application data:
probe_read() {
local phase=$1 user=$2 file=$3 rc
if runuser -u "$user" -- sh -c 'IFS= read -r < "$1"' sh "$file"; then
printf '%s,%s,read,ALLOW,rc=0\n' "$phase" "$user"
else
rc=$?
printf '%s,%s,read,DENY,rc=%s\n' "$phase" "$user" "$rc"
fi
}
probeappend() {
local phase=$1 user=$2 file=$3 marker=$4 rc
if runuser -u "$user" -- sh -c \
'printf "%s\n" "$2" >> "$1"' sh "$file" "$marker"; then
printf '%s,%s,append,ALLOW,rc=0\n' "$phase" "$user"
else
rc=$?
printf '%s,%s,append,DENY,rc=%s\n' "$phase" "$user" "$rc"
fi
}
for u in "$WRITER" "$READER" "$OUTSIDER"; do
runuser -u "$u" -- sh -c \
'printf "user=%s uid=%s gid=%s groups=%s CapEff=%s\n" \
"$(id -un)" "$(id -u)" "$(id -g)" "$(id -G)" \
"$(awk "/^CapEff:/ {print \$2}" /proc/self/status)"'
done | tee "$EVID/credentials.txt"
grep -Eq 'CapEff=0+$' "$EVID/credentials.txt" || {
echo 'HOLD: a probe identity has effective capabilities' >&2; exit 1;
}
{
probe_read baseline "$WRITER" "$FILE"
probe_append baseline "$WRITER" "$FILE" 'P1-writer-append'
probe_read baseline "$READER" "$FILE"
probe_append baseline "$READER" "$FILE" 'P1-reader-MUST-NOT-append'
probe_read baseline "$OUTSIDER" "$FILE"
probe_append baseline "$OUTSIDER" "$FILE" 'P1-outsider-MUST-NOT-append'
} | tee "$EVID/probes.baseline.csv"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.baseline.after-probes"
stat -Lc 'dev=%d inode=%i mode=%A(%a) size=%s' -- "$FILE" \
| tee "$EVID/stat.baseline.after-probes"Expected result, not claimed observed output: writer read and append are ALLOW; reader read is ALLOW and append is DENY; outsider read and append are DENY. The expected result follows directly from the configured ACL plus the documented named-user/mask access algorithm.
Make the unprivileged process open the file
The placement of shell redirection is a security property of the test. This is valid:
runuser -u "$WRITER" -- sh -c \
'printf "%s\n" probe >> "$1"' sh "$FILE"The >> "$1" is interpreted after runuser starts the unprivileged shell, so that shell attempts the destination open. By contrast, a root shell command such as runuser ... printf ... >> "$FILE" would let root perform the redirection before privilege is dropped; a successful write would then say nothing about application access.
Also capture id and effective capability state from inside each probe process. File permission decisions depend on process credentials, not merely the account name in a ticket. The ACL algorithm compares the effective user ID for owner/named-user checks and effective plus supplementary group IDs for group checks. A root success, a privileged helper, or a process with relevant DAC-bypass capability is therefore not an acceptable substitute for this non root file access probe.
Remove group write and observe the named user
Now introduce the exact misleading condition. Preserve the baseline ACL and file identity, then remove group-class write through chmod:
cp -- "$EVID/acl.baseline.display" "$EVID/acl.before-chmod"
stat -Lc 'dev=%d inode=%i mode=%A(%a)' -- "$FILE" \
> "$EVID/stat.before-chmod"
chmod g-w -- "$FILE"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.after-chmod"
stat -Lc 'dev=%d inode=%i mode=%A(%a)' -- "$FILE" \
| tee "$EVID/stat.after-chmod"
{
probe_read chmod_case "$WRITER" "$FILE"
probe_append chmod_case "$WRITER" "$FILE" 'P2-writer-MUST-FAIL'
} | tee "$EVID/probes.chmod-case.csv"With the extended ACL still present, chmod g-w operates on the group-class mode bits. Linux documents that when an ACL_MASK entry exists, those group bits correspond to the ACL mask, and modifications to corresponding file mode bits modify the associated ACL entries. That is the core of chmod changes ACL mask and, more precisely, the chmod group bits ACL_MASK interaction in this fixture.
The expected ACL pattern is:
# expected pattern; numeric IDs replace the placeholders
user::rw-
user:<writer-uid>:rw- #effective:r--
user:<reader-uid>:r--
group::---
mask::r--
other::---The writer’s stored entry has not lost its w. The shared mask has. Therefore the expected operation result is writer read ALLOW, writer append DENY. Keep this evidence pair together: “unchanged named entry” without the mask is incomplete; “permission denied” without the credentials and object identity is equally incomplete.
A compact phase ledger makes the causal change visible. Baseline has writer rw-, reader r--, mask rw-, and the intended operation matrix. After chmod g-w, the stored named-user rows are unchanged, the mask is r--, the mode’s group class reflects r--, and writer append flips from allow to deny. Device/inode should remain constant. If the inode changes, the comparison is no longer “same file, different mask” and must be held rather than narrated as proof of the ACL mechanism.
Read the effective-rights comment, not just the entry
For a matching named user, the documented check requires the requested permission to exist in both the matching ACL_USER entry and the ACL_MASK. The Linux ACL mask effective permissions calculation for the writer is therefore a set intersection:
stored writer rw- ∩ mask r-- = effective writer r--.That is why getfacl can print user:<uid>:rw- #effective:r--. Its effective-rights comment is not cosmetic; it is the visible explanation that the stored entry contains permissions excluded by the mask. The getfacl documentation explicitly says the mask limits groups and named users, while owner and other are unaffected.
Do not generalize this intersection to every ACL class. The owner entry is checked first and is not restricted by the mask. other is reached only when no owner, named-user or matching-group branch applies, and it is also outside the mask. The phrase POSIX ACL named user mask is useful shorthand, but the actual shared class is larger than named users: the owning group and named groups are masked too.
Trace why group mode bits changed the ACL result
The important reconciliation is between three representations of the same policy state. First, the extended access ACL has an ACL_MASK. Second, Linux maps the file’s group permission bits to that mask whenever the mask exists. Third, chmod g-w modifies those group mode bits, so the corresponding mask loses write. The named writer entry can remain rw- because chmod did not need to erase it to reduce effective group-class rights. This behavior is directly documented by acl(5), rather than inferred from a particular application.
This is why a mode-only inspection can mislead in both directions. ls -l may show group r--, but on an extended ACL that field reflects the mask rather than simply the owning group’s stored ACL entry. Conversely, seeing user:writer:rw- in getfacl does not prove effective write because the mask may be narrower. The trustworthy interpretation is the ACL entry, its effective comment, and an unprivileged open reconciled together.
The standards wording also deserves precision. Linux calls these POSIX ACLs and documents conformance to POSIX.1e Draft 17, but the acl(5) standards section says that 1003.1e work was abandoned. Therefore this playbook treats the Linux man page as implementation documentation for Linux behavior; it does not present an abandoned draft as a current normative POSIX standard. getfacl and setfacl are client utilities that expose and edit that Linux ACL model.
Check the assumptions around the access algorithm
Before attributing a denial to the mask, assert the branch of the access algorithm you think is running. The writer must not own the file. Record the file UID/GID and the process effective UID, primary GID and supplementary groups. Confirm CapEff is zero for the probes. If the process is unexpectedly privileged or the identity maps differently than expected, hold the diagnosis rather than editing ACLs.
Path resolution is an independent gate. The Linux path_resolution(7) manual states that lack of search permission on a nonfinal directory component produces EACCES. This fixture uses a traversable 0711 lab directory so parent directory traversal permissions do not contaminate the known-answer test. namei -l "$FILE" is retained as evidence precisely because a perfect file ACL cannot compensate for an unsearchable ancestor.
Finally, keep the principal classes distinct. A matching named-user entry is evaluated before group entries in the documented algorithm. Membership in an additional group does not merge permissions into a matched named-user entry. For this experiment, the writer’s effective rights are its named entry intersected with the mask, not a union with unrelated group permissions.
Test a targeted repair with collateral access
A safe playbook must test the repair that looks obvious and fails least privilege. Return metadata to the known baseline, then create a controlled named user latent write permission: give the reader a stored rw- entry while explicitly keeping the mask at r--. This makes both writer and reader effectively read-only even though both stored entries contain write.
sha256sum -c "$EVID/acl.baseline.restore.sha256"
[[ "$(stat -Lc '%d:%i' -- "$FILE")" == \
"$(cat "$EVID/device-inode.baseline")" ]] || {
echo 'HOLD: fixture identity changed' >&2; exit 1;
}
setfacl --restore="$EVID/acl.baseline.restore"
WUID=$(id -u "$WRITER"); RUID=$(id -u "$READER")
setfacl -m "u:${RUID}:rw-,m::r--" -- "$FILE"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.latent-reader-write"
{
probe_append latent "$WRITER" "$FILE" 'P3-writer-denied-before-repair'
probe_append latent "$READER" "$FILE" 'P3-reader-denied-before-repair'
} | tee "$EVID/probes.latent.csv"Expected result: both append attempts are denied because each stored rw- entry is intersected with mask::r--. This negative branch is synthetic fixture design, not a claim that production systems naturally acquire the same stale entry.
Now try the apparently targeted writer repair:
setfacl --test -m "u:${WUID}:rw-" -- "$FILE" \
| tee "$EVID/proposal.targeted-writer.test"
setfacl -m "u:${WUID}:rw-" -- "$FILE"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.after-targeted-writer"
{
probe_append targeted "$WRITER" "$FILE" 'P4-writer-append'
probe_append targeted "$READER" "$FILE" 'P4-reader-COLLATERAL-WRITE'
} | tee "$EVID/probes.targeted.csv"The setfacl(1) manual documents the dangerous mechanism: unless -n/--no-mask is specified, setfacl recalculates the ACL mask by default, except when a mask entry is explicitly supplied; that calculated mask is the union of the owning group and named user/group permissions. Therefore the expected result of this branch is mask::rw-: the writer regains append and the reader’s already stored w becomes effective. That is the failure mode behind a confusing setfacl write permission denied incident turning into a privilege regression through setfacl automatic mask recalculation.
A zero exit status from the repair is not acceptance. In this branch, it would mean the ACL edit succeeded while policy failed.
The negative branch is useful because it separates recovery of one capability from correctness of the policy. Before the edit, writer and reader each have stored rw- but effective r--; after the writer-focused edit, neither named entry needs to change, yet a recalculated rw- mask makes write effective for both. The reader’s newly successful append is therefore not a side effect of changing the reader row. It is collateral access caused by changing the shared ceiling. That distinction is the reason a reviewer must inspect all mask-governed entries before approving a writer repair.
In a real incident, do not manufacture a latent permission on production just to demonstrate the point. This branch exists only in the disposable known-answer fixture. Production evidence should be read as found, preserved, and compared with the approved policy; the lab supplies the mechanism needed to interpret it.
Repair the full effective policy explicitly
Repair the policy, not the symptom. The intended state is writer stored/effective rw-, reader stored/effective r--, owning group ---, shared mask rw-, and other ---. Review those entries together before applying anything:
[[ "$(stat -Lc '%d:%i' -- "$FILE")" == \
"$(cat "$EVID/device-inode.baseline")" ]] || {
echo 'HOLD: fixture identity changed' >&2; exit 1;
}
setfacl --test \
-m "u:${WUID}:rw-,u:${RUID}:r--,g::---,m::rw-,o::---" \
-- "$FILE" | tee "$EVID/proposal.whole-policy.test"
setfacl \
-m "u:${WUID}:rw-,u:${RUID}:r--,g::---,m::rw-,o::---" \
-- "$FILE"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.repaired"setfacl --test lists the ACL that would result instead of changing the file, so it is the right place to inspect whether a proposal touches more policy than its operator intended. The actual application still requires a subsequent unprivileged access matrix; a dry-run ACL rendering cannot prove path traversal, security-module behavior or the ability of a real process to open the file.
The repair deliberately leaves user::rw- and other::--- unchanged. It also corrects the reader’s stored entry rather than relying on the mask to hide excess rights. That makes the policy less fragile: a later legitimate expansion of the shared mask will not expose a reader write bit that the policy never intended.
Treat an explicit mask as shared policy
mask::rw- must be reviewed as a grant ceiling for an entire ACL class, not as “writer write.” Linux defines it as the maximum rights for named users, the owning-group entry and named groups. An explicit mask is safe here only because every entry affected by it has been reviewed: writer rw-, reader r--, and owning group ---.
setfacl -n is useful when an operator intentionally wants to preserve an existing mask, but it is not a universal fix. In the broken state where the writer has stored rw- behind mask::r--, using -n while rewriting only the writer leaves the restrictive mask in place; writer append remains denied. Conversely, allowing automatic recalculation can widen other entries. The correct choice follows the reviewed whole policy, not a preference for one switch. The client utility’s documented default and -n behavior make that tradeoff explicit.
Repeat the access matrix after the repair
Acceptance now returns to the same six operations used at baseline. Do not substitute an ACL diff for this step, and do not test only the writer that raised the incident.
CURRENT_ID=$(stat -Lc '%d:%i' -- "$FILE")
BASE_ID=$(cat "$EVID/device-inode.baseline")
[[ "$CURRENT_ID" == "$BASE_ID" ]] || {
echo "HOLD: expected $BASE_ID, got $CURRENT_ID" >&2; exit 1;
}
{
probe_read repaired "$WRITER" "$FILE"
probe_append repaired "$WRITER" "$FILE" 'P5-writer-append'
probe_read repaired "$READER" "$FILE"
probe_append repaired "$READER" "$FILE" 'P5-reader-MUST-NOT-append'
probe_read repaired "$OUTSIDER" "$FILE"
probe_append repaired "$OUTSIDER" "$FILE" 'P5-outsider-MUST-NOT-append'
} | tee "$EVID/probes.repaired.csv"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.repaired.final"
stat -Lc 'dev=%d inode=%i mode=%A(%a) uid=%u gid=%g size=%s' -- "$FILE" \
| tee "$EVID/stat.repaired.final"
namei -l -- "$FILE" | tee "$EVID/namei.repaired.final"The acceptance matrix is the baseline matrix: writer read/append allow; reader read allow and append deny; outsider read/append deny. Those are mathematically expected results from the reviewed ACL under the fixture preconditions, not reported VM observations. The exact process exit status belongs in retained evidence because a prose statement such as “permission test passed” is too weak for Linux permission repair validation.
Reconcile three ledgers before approval. The ACL ledger records the stored entries, mask and effective comments. The object ledger records absolute path, device/inode, owner/group and mode. The operation ledger records principal, action, phase, exit status and marker. A changed device/inode means you may be testing a replacement file even if the pathname is identical; stop rather than transferring conclusions to a different object.
The negative tests are first-class acceptance criteria. Writer recovery with reader append also succeeding is a failed repair, not “mostly fixed.” Likewise, outsider denial alone does not prove reader least privilege. The objective is equality between intended policy and the complete tested matrix.
Separate ACL-mask failures from other denials
An EACCES or “Permission denied” message is not an ACL-mask diagnosis by itself. The kernel resolves the pathname before it can apply the final object’s access ACL; missing search permission on an ancestor can deny traversal. path_resolution(7) documents that requirement. Keep the parent controlled in this lab, and in a real investigation capture the path rather than widening the file ACL to compensate for an ancestor problem.
Stop this mask repair when any of these conditions appears: the mount is read-only; device/inode differs from the object under review; immutable or append-only inode attributes change the operation semantics; the process identity or supplementary groups are not what the policy owner approved; effective capabilities are unexpected; or an LSM such as SELinux or AppArmor is independently denying the operation. These are separate controls and should be diagnosed on their own terms. Do not disable security enforcement merely to obtain a green filesystem probe.
Operationally, findmnt -T "$FILE", stat, namei -l, lsattr, process credentials, and the system’s LSM status form the minimum context around the ACL evidence. The goal resembles disciplined monitoring and diagnostic evidence: preserve enough state to explain a result, without treating an observability guide as proof of this ACL mechanism.
There is also a filesystem support stop condition. getfacl documents that on a filesystem without ACL support it can fall back to displaying traditional mode permissions, while setfacl documents degraded behavior when an ACL cannot be represented. The controlled exercise therefore requires successful creation and retrieval of the named-user ACL, not merely the presence of the commands in $PATH.
Use an elimination order that preserves evidence. First prove you are looking at the intended object: absolute path, device/inode and ownership. Next prove the process identity and lack of unexpected privilege. Then prove every directory component is searchable. Next record the mount and inode attributes. Check the LSM rather than suppressing it. Only after those conditions are understood should the ACL entry/mask calculation carry the diagnosis. This ordering prevents an administrator from “repairing” the ACL when the actual denial belongs to a different control plane.
The converse matters too. If traversal, mount state and security layers are clean and the writer is a matching named user with stored rw- behind mask::r--, then the ACL math predicts no write. In that bounded situation, widening unrelated directory or other permissions is neither necessary nor defensible; the repair belongs in the reviewed ACL policy.
Retain the decisive evidence without sensitive contents
A permission ticket rarely needs the application’s business data. Retain metadata and inert probe identifiers instead: fixture revision; OS/kernel; ACL package and tool versions; mount source/target/type/options; setup umask; numeric UID/GID and supplementary groups; CapEff; LSM status; parent traversal; getfacl -p -n -e; mode/owner; device/inode; file attributes; proposal dry-run; operation and exit status; and hashes of rollback artifacts.
The fixture’s markers such as P5-writer-append are deliberately content-neutral. Failed append markers should not appear in the file; successful writer markers should. Do not solve evidentiary uncertainty by copying a production secret into a test ticket.
This evidence scope also limits the conclusion. Passing this canary proves the tested principals on this object under the captured conditions. It does not establish that every path in an application, every container namespace, every host, or every future file has the same policy.
Make permission automation prove the outcome
Automation can make an ACL edit reproducible; it cannot turn command completion into policy proof. A configuration-management task can report success while the resulting mask is too narrow for the writer or broad enough to activate a reader’s latent permission. The negative branch above is a concrete example: setfacl can succeed exactly as designed while least privilege fails because automatic mask recalculation changes shared effective rights.
That is the boundary to preserve when connecting this playbook to administration automation with Ansible. Refonte’s automation page discusses repeatability and playbooks; the specialist acceptance contract here is different: after the change engine runs, independently read the actual ACL and execute the same multi-principal unprivileged probes.
A least privilege ACL regression test should therefore encode expected allows and expected denies. The automation job should fail acceptance if any one of the six results differs, if the inode/device is not the reviewed object, or if the final ACL differs from the approved policy. Save the dry-run proposal and the resulting evidence next to the change identifier.
Do not “stabilize” the job with chmod 777, a recursive chmod, removal of the ACL, or disabled SELinux/AppArmor enforcement. Those approaches erase the policy being validated. Likewise, do not infer correctness from idempotence: repeatedly converging to the same wrong ACL is still wrong.
Prepare a scoped metadata rollback
Rollback must be prepared before the risky edit. The baseline setup already saved getfacl -p -n output and a checksum. getfacl output is designed so that it can be consumed by setfacl, and setfacl --restore can restore a permission backup; the manual also warns that owner/group comments and special-bit flag comments in the input affect what is restored. Review the exact file, identities and metadata represented by the backup before applying it.
For this one-file lab:
sha256sum -c "$EVID/acl.baseline.restore.sha256"
[[ "$(stat -Lc '%d:%i' -- "$FILE")" == \
"$(cat "$EVID/device-inode.baseline")" ]] || {
echo 'HOLD: rollback target is not the baseline object' >&2; exit 1;
}
getent passwd "$WRITER" "$READER" "$OUTSIDER" >/dev/null || {
echo 'HOLD: test identity mapping changed' >&2; exit 1;
}
setfacl --test --restore="$EVID/acl.baseline.restore" \
| tee "$EVID/rollback.test"
# Apply only after reviewing the exact proposal and path.
setfacl --restore="$EVID/acl.baseline.restore"
getfacl -p -n -e -- "$FILE" | tee "$EVID/acl.after-rollback"--test is documented as non-changing test mode, and it is one of the limited options that may be combined with --restore. That makes it valuable ACL restore rollback evidence, but it does not make restore universally safe. A recursive or stale backup can describe more paths than intended; this playbook uses a single controlled absolute path and refuses a changed inode.
Most importantly, ACL rollback is metadata rollback, not content rollback. It cannot remove lines that a successful probe already appended, reconstruct data changed by an application, or reverse some other process’s write. In this disposable fixture the probe markers are harmless. In production, content recovery is a separate backup/application-recovery decision with its own owner and evidence.
The restore file itself deserves change control. getfacl can include owner, group and flags comments, and setfacl --restore documents that it attempts to restore owner/group from those comments and sets special bits according to flags information. Do not treat a visually familiar ACL stanza as proof that the complete restore payload is harmless. Review the full retained file, checksum it, verify its absolute target and numeric identities, run --test, then apply it only while the fixture identity still matches. This is why the playbook keeps a purpose-built single-file backup instead of reaching for a recursive restore.
After the lab evidence is exported, prefer destroying the disposable VM according to your lab lifecycle rather than improvising account deletion commands that might be copied into a less isolated environment.
Roll out the correction with least-privilege evidence
A production analogue should be smaller than the incident pressure encourages. Change only the reviewed object or narrowly defined object set; retain before/after ACLs and identity evidence; and make the policy owner accountable for the desired writer, reader and outsider outcomes. The operations owner executes the bounded change and canary. The security owner reviews least-privilege implications and negative tests. Rollback is approved in advance, not invented after collateral access appears.
That change discipline aligns with the general principles in DevOps change and delivery practice, while the ACL-specific acceptance test remains the one defined here. Do not widen this article into a deployment framework: the relevant rule is simply that a permission change is not complete until the post-change object and principal matrix match the reviewed contract.
Use a canary before extending the correction. On the canary, verify exact UID/GID mappings, parent traversal, same intended filesystem semantics, security-layer conditions, stored ACL, effective mask, device/inode and all positive/negative operations. Only then may an operations owner consider a broader rollout using an independently reviewed inventory. A single canary is evidence for that canary, not a production-wide guarantee.
Stop when the identity or policy is uncertain
Hold rather than improvise when a numeric UID resolves differently across hosts, a named ACL entry has no policy owner, an inherited or pre-existing entry has not been reviewed, or an unexplained denial remains after the ACL math says access should be allowed. Also hold if the installed tool’s behavior materially disagrees with the documentation used to design the change.
The response to uncertainty is not to add write bits until the application works. Resolve the identity mapping, path, mount, attribute or LSM question first. A green probe obtained by broadening rights outside the approved policy is a security regression, not successful recovery.
Choose approve, repair, hold or restore
The final decision is a policy decision supported by evidence. It is not “chmod worked,” “setfacl returned zero,” or “the application stopped logging an error.” Use the complete matrix below and assign an accountable owner.
Evidence state | Decision | Required action | Accountable owner |
Writer read/append allow; reader read allow and append deny; outsider denied both; ACL matches reviewed entries/mask; same device/inode; context evidence complete | APPROVE | Accept the canary and retain evidence/change record | Operations owner + security/policy owner sign-off |
Writer entry says rw-, mask is r--, writer append denies, other controls match the fixture assumptions | REPAIR | Review all group-class entries and apply the explicit whole-policy ACL; rerun all principals | Operations owner, with policy owner approving intended rights |
Writer-focused setfacl widens mask and reader append succeeds | RESTORE, THEN REPAIR | Restore reviewed baseline metadata on the same object; replace latent reader w with r--; set explicit reviewed mask; retest | Operations owner executes; security owner reviews regression |
Parent traversal denies, mount is read-only, immutable/append-only state is relevant, or an LSM explains denial | HOLD | Diagnose the independent control; do not widen ACL or disable enforcement | Owner of the blocking control, coordinated by operations |
Device/inode changed, path resolves unexpectedly, identity/group mapping changed, or probe has unexpected capabilities | HOLD | Re-establish object and principal identity before any repair | Operations owner; identity/security owner as applicable |
Named ACLs cannot be created/retrieved as required on the filesystem | HOLD | Move the experiment to a supported local ACL-capable fixture or choose a mechanism appropriate to that filesystem | Platform/storage owner |
Documentation, installed version and actual trace disagree materially | HOLD | Preserve the mismatch, identify version-specific behavior, and re-review the proposal | Platform owner + security reviewer |
An attempted change causes a policy regression and the exact reviewed baseline restore still targets the same object/identities | RESTORE | Apply scoped metadata restore, then rerun the baseline matrix; handle content recovery separately | Operations owner under rollback authority |
Writer works but one intended deny is untested or evidence is missing | HOLD | Complete negative probes and evidence; do not infer least privilege | Change owner |
Two details prevent the matrix from becoming a checkbox exercise. First, an intended denial is as important as an intended allow. The reader’s failed append is the evidence that distinguishes the correct repair from the dangerous automatic-mask branch. Second, every decision is scoped to the tested object and environment. A correct ACL_MASK calculation cannot rule out an untested directory, mount namespace, container identity translation or security module elsewhere.
For the known-answer fixture, the mathematical policy after the approved repair is unambiguous. Writer: rw- ∩ rw- = rw-. Reader: r-- ∩ rw- = r--. Owning group: --- ∩ rw- = ---. Outsider reaches other::--- and receives no read or write. The owner remains governed by user::rw- outside the mask. Those results follow the documented ACL algorithm, while the actual operation ledger is what must confirm them on the chosen VM.
Approval should therefore require both operations and security/policy sign-off on the same evidence bundle. Operations owns recoverability and exact execution; the policy/security owner confirms that the resulting privileges, including denials, are intended. If either cannot explain why the reader remains unable to append after writer recovery, the correct state is hold.
The evidence bundle should be internally reconcilable without trusting the operator’s memory. The runtime manifest identifies the platform and tools; the baseline restore file anchors the starting metadata; pre/post ACL captures show stored and effective rights; stat anchors the object; credential records anchor the principals; dry-run output anchors the proposed mutation; and probe CSV files anchor real opens by those principals. A reviewer should be able to walk from policy to ACL math to operation result and find no unexplained transition. Missing evidence does not automatically mean the permissions are wrong, but it does mean the requested approval has not been demonstrated.
Finally, distinguish restoration from approval. Restoring the baseline can be the right immediate risk-control action after a collateral grant, yet it merely returns metadata to a previously captured state. The incident is not closed until the baseline itself is still authorized, the matrix is rerun, any content effects are handled separately, and the intended forward repair has its own review. That keeps rollback from becoming an unexamined permanent configuration.
Build permission verification into administration practice
The durable lesson is not “never use chmod with ACLs” or “always use setfacl -n.” It is that extended ACLs make the group-class mode bits and the shared mask part of one policy, and any repair must be evaluated through effective rights plus real unprivileged operations. Linux documents both the group-bit/mask correspondence and the named-user/mask intersection; getfacl exposes the effective difference, while setfacl can recalculate the shared mask.
Institutionalize the evidence pattern: define the principal matrix first, capture numeric identities and object identity, preserve a rollback artifact, dry-run the ACL proposal, probe every intended allow and deny as the real unprivileged identities, and hold on unexplained differences. That turns permission repair from “make the error disappear” into a bounded least-privilege acceptance decision.
For administrators strengthening the broader foundations around that workflow, Refonte Learning’s System Administration page currently lists Linux/Windows administration, security, troubleshooting, command-line work and backup/recovery among its areas, with a detailed program specification of six months at 10–12 hours per week and an admission prerequisite of working toward a bachelor’s or higher-level degree. The page should be checked directly before enrollment because program details can change, and this article does not claim that the specific POSIX ACL mask laboratory above is a taught module. The operational standard remains independent: approve the whole effective policy, not merely the user whose write error started the investigation.
