File Permission Security: Least Privilege and Auditing
chmod 777 almost always makes a Permission denied error disappear, and it almost never fixes the actual problem: instead of figuring out exactly who needs which operation, it widens the attack surface to every local account on the system. In production, the right question is never "which mode makes this work?" — it is "which process needs to perform which operation on which object?"
This article turns the mechanics from File Permissions, Ownership, Special Permissions, umask, and Root and sudo into an actual security policy. You will apply least privilege to files and directories, connect confidentiality, integrity, and availability risk to mode, ownership, and directory structure; audit world-writable files, writable executables, special bits, and unowned objects with find; read getfacl, namei, findmnt, and MAC logs alongside ls -l; separate policy for code, configuration, secrets, and runtime data; and fix a production permission error with the smallest possible change, verified against the real account, with a working rollback.
A Permission Is a Security Policy, Not Just a Number
Linux's ordinary user/group/other model is called discretionary access control (DAC). "Discretionary" means the object's owner controls its ordinary permissions. The kernel compares a process's identity against the file's owner and group, then checks the relevant r, w, or x bit.
From a security standpoint, every bit answers one of three questions:
- Confidentiality: who can read the content?
- Integrity: who can change the content or a directory entry?
- Availability: who can delete, replace, or block access to something that matters?
A .env file being world-readable is a confidentiality problem. A service executable being writable by the service's own account is an integrity problem: a compromised process could overwrite the code it will run next time. A directory with excessive w+x lets another user remove an important entry, which is an availability problem.
Writing Least Privilege Down Explicitly
Least privilege does not mean "pick the smallest-looking number." A policy has four parts:
- subject: the process or administrator performing the action.
- object: the file, directory, symlink, or other resource.
- action: read, write, execute, create a name, or traverse a path.
- time and context: is this need permanent, or only during deployment?
For example, a web service may always need to read its configuration, but never needs to write its own deployment code. A deployment account may need to create a new release, but should never read a runtime secret. If one shared app group is used for both roles, the mode numbers can look tidy while the policy underneath is weak.
Separate Code, Configuration, and Runtime Data
Managing every file under one ownership and mode is convenient, but it opens unnecessary write paths for a compromised service. A typical server application splits into at least these categories:
| Category | Service's need | Typical manager |
|---|---|---|
| executable and release code | read/execute, no write | root or a deployment account |
| plain configuration | read, no write | root or configuration management |
| secrets | read only by the service that needs it | root, a dedicated service group |
| runtime data | service reads and writes | the service account |
| uploads or cache | service writes; execution usually not needed | the service account |
| logs | written by the application or a logging service, read by an auditor | depends on logging policy |
This table is not a universal list of exact modes. 0640 root:catalog, for example, is a reasonable choice for a secret only if no unrelated person or service belongs to the catalog group. If several unrelated processes share one group, that same mode grants excess read access.
A common, safer layout:
/srv/catalog/current/ root:catalog read/execute for the service
/etc/catalog/app.env root:catalog read-only for the service
/var/lib/catalog/ catalog:catalog read/write for the service
/var/log/catalog/ catalog:adm write, with controlled audit
The exact owner, group, and mode depend on the application's own operating model and distribution policy. The important boundary is that directories the service needs to write are separated from its code and configuration.
Warning
A noexec mount option can restrict directly executing a file from an upload directory, but it should not be treated as sole protection. A readable script can be passed as an argument to an interpreter, and data can be interpreted by a vulnerable program regardless. Ownership, mode, application-level validation, and MAC policy need to work together.
A Safe Lab
The following lab deliberately creates weak modes, only inside the ~/lab/permission-security test tree. It does not touch system directories and does not need sudo. Lines in the code blocks contain no $ or # prompt character.
mkdir -p ~/lab/permission-security/{bin,config,data,uploads}
printf '%s\n' '#!/usr/bin/env bash' 'echo "report ready"' \
> ~/lab/permission-security/bin/run-report.sh
printf '%s\n' 'DB_PASSWORD=test-only-secret' \
> ~/lab/permission-security/config/app.env
printf '%s\n' 'record-001' \
> ~/lab/permission-security/data/records.txt
chmod 0777 ~/lab/permission-security/bin/run-report.sh
chmod 0666 ~/lab/permission-security/config/app.env
chmod 0777 ~/lab/permission-security/uploads
chmod 0644 ~/lab/permission-security/data/records.txt
find ~/lab/permission-security -xdev -printf '%y %m %u:%g %p\n'
A representative, relevant excerpt:
f 777 ali:ali /home/ali/lab/permission-security/bin/run-report.sh
f 666 ali:ali /home/ali/lab/permission-security/config/app.env
d 777 ali:ali /home/ali/lab/permission-security/uploads
f 644 ali:ali /home/ali/lab/permission-security/data/records.txt
Home path and account names vary by system. If run-report.sh, app.env, and uploads show exactly these modes, the lab is ready to audit.
Danger
0777 and 0666 are not recommended for production. They are used here only to demonstrate how audit tools locate a dangerous state, inside this current user's own dedicated lab directory. Do not adapt these commands to /etc, /srv, /var/www, or a real project path.
Risk-Focused Auditing with find
find -perm only checks mode bits. It does not account for an ACL mask, a read-only mount, AppArmor, SELinux, or an NFS server's own decision. So the result is a list of candidates that need review, not a security conclusion by itself.
Files Other Can Write
Finding regular files with the other write bit set:
Expected result:
777 ali:ali /home/ali/lab/permission-security/bin/run-report.sh
666 ali:ali /home/ali/lab/permission-security/config/app.env
-perm -0002 requires the other-write bit to be present and ignores every other bit. A secret at 0666 allows not just reading by everyone, but also overwriting its content.
Writable and Executable Files
If an executable file can be written by group or other, there is a risk that a higher-privileged process running it could execute swapped-in code:
/0111 looks for any of the owner, group, or other execute bits; /0022 looks for any of the group or other write bits. Because the two predicates sit next to each other, a file must satisfy both. The / form is a GNU find extension, present in the GNU Findutils shipped with Ubuntu Server and the RHEL family.
World-Writable Directories Without a Sticky Bit
uploads from the lab shows up here. A world-writable directory without a sticky bit lets any user with access remove or replace another user's directory entries. A shared directory such as /tmp is normally protected by the sticky bit — its mechanism is covered in Special Permissions.
Setuid, Setgid, and Unowned Objects
For a read-only look at a production tree:
app_root='/srv/<app>'
sudo find "$app_root" -xdev -type f -perm /6000 \
-printf 'special %m %u:%g %p\n'
sudo find "$app_root" -xdev \( -nouser -o -nogroup \) \
-printf 'unmapped %m %u:%g %p\n'
sudo is needed to audit restricted directories; it does not mean anything found is automatically dangerous. /6000 matches either setuid or setgid, and a legitimate package-provided privileged executable can appear here too. -nouser/-nogroup finds objects whose UID/GID cannot currently be resolved to a name in the NSS database.
Warning
Do not attach -exec chmod, -delete, or a recursive chown to the audit's output. Removing a special bit can break a system program, and reassigning an unowned number to a new account can expose another tenant's data. Determine the package source, UID/GID mapping, mount, and expected policy first.
Bounding the Audit on a Large Tree
Starting a production audit at / is usually noisy and slow. Scope the target explicitly:
app_root='/srv/<app>'
realpath -- "$app_root"
stat -c '%F %A %U:%G %n' -- "$app_root"
findmnt -R -T "$app_root"
sudo find -P "$app_root" -xdev -maxdepth 3 \
-printf '%y %m %u:%g %p\n'
-P explicitly avoids following symlinks and is written before the starting path in find. -xdev avoids crossing into another mount, and -maxdepth 3 bounds the first pass — deliberately increase the depth if you need to see further. Do not add 2>/dev/null to hide permission errors, or the audit's coverage will look more complete than it actually is.
Layers ls -l Does Not Show
Even when the classic mode looks correct, other layers can still affect the actual decision.
ACLs and the Effective Mask
A + after the permission string in ls -l can indicate an extended ACL:
A representative result:
The named user backup has r--. The ACL's mask::r-- caps the effective permission for named-user, named-group, and the owning-group entries. For example, if an entry reads user:backup:rw- but the mask is r--, backup effectively gets read-only access. getfacl often shows this with an #effective:r-- comment.
A default ACL on a directory affects the starting ACL of new objects created inside it. A single chmod change can accidentally alter the ACL mask, so capture getfacl output before and after a change whenever you see a +.
On Debian/Ubuntu, getfacl comes from the acl package. The RHEL family's ACL tools are usually delivered by an acl package as well.
Setting an ACL: Granting One Extra User Access Without Changing Ownership
Reading an ACL only explains a + you already found. The more common real task is granting one additional account access to an object without changing its owner, group, or handing out a broader mode to everyone. This is exactly the situation the classic owner/group/other model cannot express on its own: "the file is owned by deploy, but backup also needs read access, and nobody else should."
sudo setfacl -m u:backup:r-- /srv/catalog/current/release.tar.gz
getfacl -p /srv/catalog/current/release.tar.gz
# file: srv/catalog/current/release.tar.gz
user::rw-
user:backup:r--
group::r--
mask::r--
other::---
-m modifies the ACL, adding or updating one entry; u:backup:r-- grants the named user backup read-only access, independent of backup's own group membership. The file's owner and primary group are untouched — ls -l still shows the same owner and group, with a trailing + marking the extended ACL.
Danger
setfacl changes access control on a live object; a mask recalculation can unintentionally narrow an existing named-user or named-group entry, and a mistyped user name silently creates a new, unintended grant instead of erroring. Run getfacl before and after every change, keep the "before" output as a rollback (getfacl -R output can be fed straight back with setfacl --restore), and never run setfacl -R against a production tree without first testing the exact same command against a throwaway copy.
Default ACLs: Making New Files Inherit Access Automatically
An ACL set with -m only affects the object it was run against. A directory shared by several accounts needs new files to pick up the same access automatically, which is what a default ACL does:
# file: srv/catalog/shared
user::rwx
group::rwx
other::r-x
default:user::rwx
default:user:backup:r--
default:group::rwx
default:mask::rwx
default:other::r-x
-d marks the entry as a default, applied only to objects created inside this directory afterward — it does not retroactively change files that already exist there, and it does not grant backup any access to the directory itself unless a matching non-default entry is also added. Confirming inheritance:
sudo -u catalog -- touch /srv/catalog/shared/new-release.tar.gz
getfacl -p /srv/catalog/shared/new-release.tar.gz
The new file's ACL should already include user:backup:r--, inherited from the directory's default. This is the standard mechanism behind "everyone on this team can read new files dropped into this shared directory, without a chmod after every single deployment."
Mount Options
ro can block writing, noexec can block running an executable directly, and nosuid restricts setuid/setgid effect. Trying to "fix" a mount policy by widening file mode changes the wrong layer entirely.
AppArmor and SELinux
Ubuntu Server typically layers AppArmor, and the RHEL family typically layers SELinux, as mandatory access control (MAC) on top of DAC. Even when DAC permits an action, MAC policy can still deny it.
Read-only diagnostics on Ubuntu:
On the RHEL family:
ausearch may require elevated privilege to read the audit log. Check exactly which profile/context, process, object, and action a denial refers to.
Fixing a Wrong SELinux Context
A DAC-correct file can still be denied by SELinux if its type does not match what the service's policy expects — a very common cause of "I set the permissions correctly and it's still Permission denied" on RHEL-family systems, especially after moving a file from an unexpected location (moving preserves the context of the source directory, not the destination).
If a file was copied into /var/www/html from, say, a home directory, it commonly ends up labeled user_home_t instead of httpd_sys_content_t, and Apache's policy will not allow reading it even though the DAC mode looks fine. Two ways to fix it:
restorecon resets the file's context back to whatever the system's policy database says it should be for that path — the safe, standard fix, since it does not invent a new rule, only reapplies the existing one.
If the path is new and has no matching rule yet, the policy itself needs a mapping added first, then applied:
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/webapp(/.*)?'
sudo restorecon -Rv /srv/webapp
semanage fcontext -a adds a persistent rule mapping the path pattern to a type, surviving a future relabel; restorecon -Rv then applies it recursively to files that already exist under that path. A one-off chcon changes the label immediately but does not persist across a full relabel or restorecon run, which makes it useful only for a quick, temporary test:
Danger
Do not disable AppArmor, set SELinux to Disabled or a permanent Permissive, or guess a context change just to make Permission denied go away. Check DAC, the path, and the service identity first, then restore the expected MAC policy using the distribution's official tools — restorecon/semanage fcontext for SELinux, profile mode changes for AppArmor. Before any of these, back up the current context (ls -Z output) and confirm the change with ausearch/journalctl showing no further denials for that path.
Changing an AppArmor Profile's Mode
AppArmor profiles run in one of two modes, and aa-status shows which mode each loaded profile is in:
apparmor module is loaded.
50 profiles are loaded.
45 profiles are in enforce mode.
/usr/sbin/nginx
...
5 profiles are in complain mode.
/usr/sbin/tcpdump
...
- enforce: the profile actively blocks anything not explicitly permitted, and logs the denial.
- complain: the profile only logs what would have been denied, without actually blocking it — used while developing or adjusting a profile, so you can see what a new deployment needs before locking it down.
Moving a profile into complain mode while diagnosing a suspected AppArmor-caused failure, without disabling protection system-wide:
sudo aa-complain /usr/sbin/nginx
sudo systemctl restart nginx
journalctl -k --grep='apparmor="ALLOWED"' --since '5 minutes ago'
Every action that would have been denied is now logged as ALLOWED (in complain mode) instead of DENIED, which lets you build the list of missing rules without an outage. Once the profile has been corrected to cover the legitimate behavior, switch back:
Danger
Leaving a profile in complain mode is a silent security regression — the profile stops blocking anything while looking, at a glance, like protection is still active. Treat complain mode as a temporary diagnostic state with a tracked end date, not a permanent fix, and confirm with aa-status that the profile is back in enforce mode once the underlying rule gap is closed.
Capabilities and Already-Open Files
A process holding a capability such as CAP_DAC_OVERRIDE or CAP_DAC_READ_SEARCH can bypass some ordinary permission checks:
If a file is already open, narrowing its mode afterward does not automatically invalidate an existing file descriptor. During an incident, do not stop at chmod — identify processes still holding the object open with lsof <path>. Stopping or restarting a process affects the service, so follow a confirmed incident and rollback procedure.
Bringing the Weak Lab to a Safe State
First back up the current mode, ownership, and ACL:
getfacl -Rp ~/lab/permission-security > ~/lab/permission-security.acl.before
chmod 0600 ~/lab/permission-security.acl.before
If getfacl is missing, installing the acl package requires administrator privilege. Recreating this isolated lab directory can itself serve as rollback; in production, a missing tool is never a reason to change something without a backup.
Lab policy:
- the script is read, written, and executed by its owner; the current group can read and execute it;
- the test secret is read and written only by its owner;
- the data file is written by its owner and read by the group;
- only the owner can enter and write to the uploads directory.
For this specific test tree:
chmod 0750 ~/lab/permission-security/bin
chmod 0750 ~/lab/permission-security/bin/run-report.sh
chmod 0700 ~/lab/permission-security/config
chmod 0600 ~/lab/permission-security/config/app.env
chmod 0750 ~/lab/permission-security/data
chmod 0640 ~/lab/permission-security/data/records.txt
chmod 0700 ~/lab/permission-security/uploads
Do not copy these exact modes onto a real application. They are the result of the policy written just above, not a universal answer.
Re-run the audits:
find ~/lab/permission-security -xdev -type f -perm -0002 \
-printf 'world-writable %m %p\n'
find ~/lab/permission-security -xdev -type f \
-perm /0111 -perm /0022 \
-printf 'writable-executable %m %p\n'
find ~/lab/permission-security -xdev -type d \
-perm -0002 ! -perm -1000 \
-printf 'unsafe-shared-dir %m %p\n'
All three should return nothing. Then a functional check:
~/lab/permission-security/bin/run-report.sh
test -r ~/lab/permission-security/config/app.env
echo $?
If report ready and 0 appear, the needed functionality is preserved. If the result is unexpected:
setfacl --restore=~/lab/permission-security.acl.before
find ~/lab/permission-security -xdev -printf '%y %m %u:%g %p\n'
Practical Scenario: A Service Can Write Its Own Code and Secret
catalog.service runs as the catalog account. An audit found:
-rwxrwxrwx catalog:catalog /srv/catalog/current/bin/catalog
-rw-r--r-- catalog:catalog /etc/catalog/app.env
drwxrwxrwx catalog:catalog /var/lib/catalog/uploads
The service works, but the security boundary is wrong:
- both the service and any other account can replace the executable;
- the secret is readable by every local account, and the service's own ownership lets it change the mode as well;
- the upload directory is world-writable with no sticky bit.
Intended policy: code and secret are managed by root; catalog may only read/execute them; the service writes only to the upload directory.
1. Check the Real Identity and Paths
service_name='catalog.service'
app_binary='/srv/catalog/current/bin/catalog'
secret_file='/etc/catalog/app.env'
upload_dir='/var/lib/catalog/uploads'
systemctl show -p User -p Group "$service_name"
id catalog
namei -l "$app_binary"
namei -l "$secret_file"
findmnt -T "$app_binary" -o TARGET,FSTYPE,OPTIONS
stat -c '%A %a %U(%u):%G(%g) %n' \
"$app_binary" "$secret_file" "$upload_dir"
getfacl -p "$app_binary" "$secret_file" "$upload_dir"
Replace the variables with the real service name and absolute paths. systemctl show does not always prove the currently running process; check with ps -o pid,user,group,comm -C <process_name> if needed.
2. Test the Needed Operations Under the Service Account
Check that the secret can be read without printing its content to the terminal:
sudo -u catalog -- sh -c 'exec 3<"$1"' sh "$secret_file"
echo $?
sudo -u catalog -- test -w "$app_binary"
echo $?
sudo -u catalog -- test -w "$upload_dir"
echo $?
In the current weak state, all three statuses can be 0. The first 0 confirms the needed read; the second confirms the executable is writable; the third confirms the upload directory is writable. Per the intended policy, only the second result should be non-zero: the service must be able to read the secret and write to the upload directory, but must never be able to write to its own executable.
3. Snapshot and Metadata Backup
In production, take a VM/storage snapshot or record configuration management state. In addition:
backup_file=$(sudo mktemp /root/catalog-permissions.before.XXXXXX)
sudo getfacl -p "$app_binary" "$secret_file" "$upload_dir" \
| sudo tee "$backup_file" >/dev/null
sudo chmod 0600 "$backup_file"
printf '%s\n' "$backup_file"
Record the printed path in the change record. This backup only covers the three named objects — if a parent directory or internal files are also being changed, add them to the backup scope first.
4. The Smallest Change
sudo chown root:catalog -- "$app_binary" "$secret_file"
sudo chmod 0750 -- "$app_binary"
sudo chmod 0640 -- "$secret_file"
sudo chown catalog:catalog -- "$upload_dir"
sudo chmod 0750 -- "$upload_dir"
sudo is needed to assign a different owner/group. These commands touch only the three previously verified objects and are not recursive. root:catalog 0750 opens the executable for the service group to read/execute, while write stays with root. root:catalog 0640 gives the service group read access to the secret and closes it to every other account.
5. Verify the Result and the Service
stat -c '%A %a %U:%G %n' \
"$app_binary" "$secret_file" "$upload_dir"
getfacl -p "$app_binary" "$secret_file" "$upload_dir"
sudo -u catalog -- sh -c 'exec 3<"$1"' sh "$secret_file"
echo $?
sudo -u catalog -- test ! -w "$app_binary"
echo $?
sudo -u catalog -- test -w "$upload_dir"
echo $?
All three statuses should now be 0: the secret opens, the binary is not writable, and the upload directory is writable. The service may already have the configuration or executable open from before the change — watch logs and a health check. A permission change alone does not automatically require restarting the service.
6. Rollback
If monitoring or the functional test shows an unexpected result:
sudo setfacl --restore="$backup_file"
stat -c '%A %a %U:%G %n' \
"$app_binary" "$secret_file" "$upload_dir"
setfacl --restore restores the owner, group, mode, and ACL from the backup. If your organization's snapshot or deployment rollback is more reliable, use that path instead.
Making Production Auditing Useful
A one-off find run is not a security control. A working audit has these properties:
- the expected state is written down: which path should have which owner/group and permitted operations is known in advance;
- coverage is bounded: mounts, symlinks, and deliberate exceptions are explicitly identified;
- the result is compared: a new run's differences from the previously approved state are isolated;
- there is an owner: it is clear who reviews a finding and when it gets fixed;
- rollback has been tested: restoring ACLs, ownership, and special attributes has actually been verified;
- it is tied to deployment: a permission drift does not silently reappear on the next release.
Instead of repeatedly fixing modes by hand, fix the source in the package, image, deployment manifest, or configuration management. Otherwise a hotfix applied directly on the server disappears on the next deploy.
Common Mistakes
Using 777 as a Quick Fix for Availability
The error disappears temporarily, but a write and execute path opens for every local account. Find the process identity, the parent directory, ACLs, and MAC cause instead, and grant only the missing operation.
Looking Only at a File's Own Mode
Excess w+x on a parent directory can let another user replace a file's name entirely. Check the whole path with namei -l, ACLs with getfacl, and the mount with findmnt.
Giving a Service the Entire Project Tree
Runtime writes do not require handing release code and secrets to the service's own owner. Separate writable data into its own directory, such as /var/lib/<service>.
Labeling Every Audit Result a Vulnerability
/tmp being a sticky, world-writable directory, or a package's setuid executable, can both be entirely legitimate. Evaluate a finding by its purpose, owner, package origin, and any additional protective layer.
Treating find -perm as a Real Access Test
-perm only compares inode mode bits. ACLs, MAC, a read-only mount, and NFS mapping can each produce a different actual decision. Get the final proof from a real, side-effect-free operation run under the specific service account.
Disabling MAC to Hide a DAC Error
An AppArmor or SELinux denial can indicate that policy and placement genuinely do not match. Instead of disabling protection globally, check the subject, object, action, and expected label/profile in the log.
Fixing Only Existing Files
Today's chmod does not decide tomorrow's new files' mode. Creation policy depends on umask, default ACLs, a setgid directory, and the deployment tool. Fix the creation source too, or drift returns.
Assuming a Backup Preserves Mode
Some copy or archive methods do not preserve ACLs, ownership, extended attributes, or file capabilities. When testing a restore, check metadata alongside content.
Exercises
- Run the world-writable file, writable-executable, and sticky-bit-free world-writable directory audits against the lab's weak state. Tie each result to a confidentiality, integrity, or availability risk.
- Compare
ls -l,stat, andgetfacloutput forapp.env. Note which tool answers questions about mode, numeric UID/GID, and ACL respectively. - Write your own lab policy first, then compute the required modes from it. Do not copy ready-made numbers without a policy behind them.
- Compare
find -perm /0022andfind -perm -0022across three test files with modes0620,0602, and0622. Prove the "any bit" versus "all bits" difference with each result. - For a hypothetical service, map out code, secret, runtime data, and upload directories. For each, specify owner, group, required operations, an audit command, and a rollback source.
Verification criterion: every answer should make the subject, object, and required action explicit; do not use "the error went away" as proof of security.
When you finish the lab, check the target first:
If the result shows only the lab tree:
Warning
rm -rI deletes the test tree, and rm -i deletes the ACL backup, with no undo mechanism. Proceed only after checking the read-only listing above and saving any results you want to keep elsewhere. Do not adapt these commands to a production path.
What This Module Adds Up To
Across this module, permissions moved from a static number to a managed policy: User and Group Accounts established the UID/GID identities everything else depends on; File Permissions covered how the kernel picks one class and checks one bit; Ownership covered where "owner" and "group" actually come from; Special Permissions covered how an executable's identity or a shared directory's inheritance can change; umask covered how a brand-new object's starting permission is decided; and Root and sudo covered how elevated authority is scoped rather than granted wholesale. This article tied all of it into an auditable security policy, driven by subject, object, and action rather than by a mode number alone.
The next module turns to how running programs use these same identities — what a process is, how its resources are tracked, and how to observe and control it while it runs.
References
- Linux man-pages:
path_resolution(7): https://man7.org/linux/man-pages/man7/path_resolution.7.html - Linux man-pages:
inode(7): https://man7.org/linux/man-pages/man7/inode.7.html - Linux man-pages:
acl(5): https://man7.org/linux/man-pages/man5/acl.5.html - Linux man-pages:
capabilities(7): https://man7.org/linux/man-pages/man7/capabilities.7.html - GNU Findutils manual: mode bits: https://www.gnu.org/software/findutils/manual/html_node/find_html/Mode-Bits.html
- GNU Findutils manual: symbolic links: https://www.gnu.org/software/findutils/manual/html_node/find_html/Symbolic-Links.html
- Linux kernel documentation:
/proc/sys/fs: https://www.kernel.org/doc/html/latest/admin-guide/sysctl/fs.html - Ubuntu Server documentation: AppArmor: https://ubuntu.com/server/docs/how-to/security/apparmor/
- Red Hat Enterprise Linux 9: getting started with SELinux: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_selinux/getting-started-with-selinux_using-selinux
- Red Hat Enterprise Linux 9: managing SELinux file contexts (
semanage fcontext,restorecon,chcon): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_selinux/mngin-selinux-file-context_using-selinux - Ubuntu manual page:
aa-status(8),aa-complain(8),aa-enforce(8): https://manpages.ubuntu.com/manpages/noble/man8/aa-status.8.html - Linux man-pages:
setfacl(1),getfacl(1): https://man7.org/linux/man-pages/man1/setfacl.1.html - POSIX.1-2024: file access permissions: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap04.html