Loading content...
Imagine you're a system administrator at a large enterprise. You have a confidential project directory that should be accessible to the project manager, two lead engineers, the finance director (for budget reviews), and an external auditor (read-only access). You also need to explicitly deny access to a departed employee whose account hasn't been fully removed yet.
With traditional Unix permissions (owner, group, others), this scenario is virtually impossible to implement correctly. You can create a group for the project, but what about the finance director who isn't part of the engineering team? What about granting read-only access to the auditor? What about explicitly denying the departed employee?
Access Control Lists (ACLs) solve this fundamental limitation by allowing you to specify permissions for any number of individual users and groups on a per-object basis. They transform access control from a rigid, three-category model into a flexible, fine-grained permission system that can handle real-world complexity.
By the end of this page, you will understand what ACLs are, why they exist, how they relate to the theoretical access matrix model, and how they fundamentally differ from traditional Unix permission systems. You'll gain the conceptual foundation needed to effectively use and design ACL-based access control systems.
Traditional Unix file permissions, designed in the 1970s, use a simple but limited model based on three categories of users:
For each category, three permission bits control access:
This gives us the familiar rwxrwxrwx notation. While elegantly simple, this model has fundamental limitations that make it inadequate for modern, complex organizations:
| Limitation | Scenario | Why Traditional Permissions Fail |
|---|---|---|
| Single group per file | Engineering file needs access by both DevOps and QA teams | A file can only belong to one group; one team gets access, the other doesn't |
| No per-user control | Grant temporary access to a specific contractor | Either they join an authorized group (affecting all files) or get 'others' access (too broad) |
| No explicit deny | Revoke access from a specific user in an authorized group | Impossible; group membership grants access regardless of individual restrictions |
| Binary decisions only | Auditor needs read, finance needs write, admin needs full | Cannot mix permission levels beyond owner/group/others split |
| No granular inheritance | New files should inherit selective permissions | umask is all-or-nothing; cannot specify complex inheritance rules |
The fundamental problem: Traditional permissions force you to choose between security and functionality. You either create a proliferation of groups (leading to management nightmares) or loosen permissions (creating security risks).
Consider a practical example: a legal department's contract directory.
# Goal: /contracts/ directory permissions# - Legal team (6 people): read/write# - CEO: read-only# - External law firm (2 people): read-only# - Finance director: read-only# - Departed legal assistant: DENY all access# - IT backup service account: read-only # Traditional Unix attempt:$ ls -la /contracts/drwxrwx--- 2 legal_dir legal_team 4096 Jan 15 10:00 contracts/ # Problems:# 1. CEO isn't in legal_team group (adding her puts her in ALL legal resources)# 2. External law firm can't be in legal_team (security policy)# 3. Finance director can't be in legal_team (wrong department)# 4. Departed assistant is still in legal_team (HR hasn't processed yet)# 5. IT backup needs access but shouldn't be in legal_team # "Solutions" all create worse problems:# - Use 'others' permissions: Everyone can access contracts!# - Create contract_readers group: Must maintain in parallel with legal_team# - Multiple symlinks with different permissions: Management nightmareLarge organizations attempting to use only traditional permissions often end up with hundreds or thousands of groups, many containing overlapping members. Managing these groups becomes a full-time job, and the security model becomes so complex that administrators make mistakes, either granting too much access or too little. ACLs eliminate this complexity by allowing per-object, per-user permissions.
An Access Control List (ACL) is a data structure that specifies which subjects (users, groups, or processes) have which permissions on a particular object (file, directory, device, or other resource). Unlike the fixed owner/group/others model, an ACL can contain any number of entries, each defining access rights for a specific subject.
Formal Definition:
An ACL is a list of Access Control Entries (ACEs), where each ACE contains:
Mathematically, if we consider the access matrix model where rows are subjects and columns are objects, an ACL is a column of the access matrix—all the access rights for a single object.
file₁ file₂ file₃ ... fileₙ
┌──────┬──────┬──────┬─────┬──────┐
user₁ │ rw- │ r-- │ --- │ ... │ rwx │
user₂ │ r-- │ rwx │ r-- │ ... │ --- │
user₃ │ --- │ --- │ r-- │ ... │ r-- │
⋮ │ ⋮ │ ⋮ │ ⋮ │ ⋱ │ ⋮ │
groupₘ │ r-- │ --- │ rwx │ ... │ r-- │
└──────┴──────┴──────┴─────┴──────┘
↑
ACL for file₁ = {(user₁, rw-), (user₂, r--), (groupₘ, r--)}
While ACLs represent columns of the access matrix (all subjects for one object), capability lists represent rows (all objects for one subject). Both are valid implementations of the access matrix model, but they have different tradeoffs. ACLs are favored in file systems because access checks occur at the object (file), making it natural to store permissions there. We'll explore capabilities in detail in a separate module.
Let's revisit our legal department scenario and see how ACLs elegantly solve each limitation of traditional permissions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
# Using POSIX ACLs to implement accurate access control: # Set base permissions (owner and owning group)$ chown legal_director:legal_team /contracts/$ chmod 770 /contracts/ # View current ACL (shows traditional permissions mapped to ACL format)$ getfacl /contracts/# file: contracts/# owner: legal_director# group: legal_teamuser::rwxgroup::rwxother::--- # Now add the fine-grained entries: # CEO gets read-only access (individual user entry)$ setfacl -m u:ceo:rx /contracts/ # External law firm partner 1 - read only$ setfacl -m u:lawfirm_partner1:rx /contracts/ # External law firm partner 2 - read only $ setfacl -m u:lawfirm_partner2:rx /contracts/ # Finance director - read only$ setfacl -m u:finance_director:rx /contracts/ # IT backup service - read only$ setfacl -m u:backup_svc:rx /contracts/ # CRITICAL: Explicitly deny departed employee (even though still in legal_team)$ setfacl -m u:departed_assistant:--- /contracts/ # View the complete ACL$ getfacl /contracts/# file: contracts/# owner: legal_director# group: legal_teamuser::rwxuser:ceo:r-xuser:lawfirm_partner1:r-xuser:lawfirm_partner2:r-xuser:finance_director:r-xuser:backup_svc:r-xuser:departed_assistant:---group::rwxmask::rwxother::---Every limitation is addressed:
| Previous Problem | ACL Solution |
|---|---|
| Single group per file | Multiple group entries can be added: setfacl -m g:devops:rx,g:qa:rx file |
| No per-user control | Individual user entries: setfacl -m u:contractor:rx file |
| No explicit deny | Deny entries: setfacl -m u:departed:--- file (overrides group membership) |
| Binary decisions only | Mix permission levels freely for each subject |
| No granular inheritance | Default ACLs specify what new files/directories inherit |
Notice how the departed_assistant gets explicit deny even though they're still in legal_team. In POSIX ACLs, user-specific entries take precedence over group entries. In Windows ACLs, explicit deny entries take precedence over allow entries. This allows immediate access revocation without waiting for group membership changes—a critical capability for security incident response.
Understanding ACL architecture requires examining how ACLs fit into the overall operating system security architecture. ACLs operate within the reference monitor concept—the security kernel component that mediates all access to objects.
Architectural Position:
┌─────────────────────────────────────────────────────────────────┐
│ User Space │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Process A│ │Process B│ │Process C│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ System Call │ │
│ │ Interface │ │
│ └────────┬─────────┘ │
├────────────────────┼────────────────────────────────────────────┤
│ ▼ Kernel Space │
│ ┌──────────────────┐ │
│ │ REFERENCE MONITOR │ │
│ │ ┌─────────────┐ │ │
│ │ │ Access │ │ │
│ │ │ Check │ │ ┌───────────────────┐ │
│ │ └──────┬──────┘ │ │ │ │
│ │ │ │◄────│ ACL Database │ │
│ │ ▼ │ │ (per object) │ │
│ │ ┌─────────────┐ │ └───────────────────┘ │
│ │ │ Decision: │ │ │
│ │ │ ALLOW/DENY │ │ │
│ │ └──────┬──────┘ │ │
│ └─────────┼────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Object (file, │ │
│ │ device, etc.) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Core Components of an ACL System:
When a subject attempts to access an object, the operating system must determine whether to allow or deny the access. This access check algorithm is a critical component of the reference monitor. Different operating systems implement this differently, but the general principles are consistent.
POSIX ACL Evaluation (Linux, Unix):
POSIX ACLs use a first-match algorithm with a specific priority order:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
def check_posix_acl_access(subject, object, requested_permissions): """ POSIX ACL access check algorithm. Returns: ALLOW or DENY Subject has: effective_uid, effective_gid, supplementary_groups Object has: owner_uid, owner_gid, access_acl """ acl = object.access_acl # Step 1: Check if subject is the object owner if subject.effective_uid == object.owner_uid: owner_entry = acl.get_entry(ACL_USER_OBJ) if requested_permissions <= owner_entry.permissions: return ALLOW else: return DENY # Step 2: Check for a named user entry matching the subject user_entry = acl.get_entry(ACL_USER, subject.effective_uid) if user_entry is not None: # Apply mask (if present) to limit effective permissions effective_perms = user_entry.permissions & acl.get_mask() if requested_permissions <= effective_perms: return ALLOW else: return DENY # Step 3: Check group entries (owning group and named groups) matching_group_entries = [] # Check owning group if subject.effective_gid == object.owner_gid or \ object.owner_gid in subject.supplementary_groups: matching_group_entries.append(acl.get_entry(ACL_GROUP_OBJ)) # Check named group entries for group_entry in acl.get_entries(ACL_GROUP): if group_entry.gid == subject.effective_gid or \ group_entry.gid in subject.supplementary_groups: matching_group_entries.append(group_entry) # If any matching group entry grants the requested permission, allow if matching_group_entries: combined_perms = 0 for entry in matching_group_entries: combined_perms |= entry.permissions # Apply mask to combined group permissions effective_perms = combined_perms & acl.get_mask() if requested_permissions <= effective_perms: return ALLOW else: return DENY # Step 4: Fall back to 'other' entry other_entry = acl.get_entry(ACL_OTHER) if requested_permissions <= other_entry.permissions: return ALLOW else: return DENYWindows ACL Evaluation:
Windows uses a different algorithm that evaluates ACEs in order, with explicit deny ACEs taking precedence:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
def check_windows_acl_access(token, object, desired_access): """ Windows discretionary access check algorithm. Token contains: user_sid, group_sids, privileges Object contains: security_descriptor with DACL """ dacl = object.security_descriptor.dacl # Special case: No DACL means full access to everyone if dacl is None: return ALLOW # Empty DACL means no access to anyone (except owner for certain rights) if dacl.is_empty(): return DENY granted_access = 0 remaining_access = desired_access # Evaluate ACEs in order (deny ACEs typically come first) for ace in dacl.entries: # Check if this ACE applies to the token if not ace_applies_to_token(ace, token): continue if ace.type == ACCESS_DENIED_ACE: # If any denied permission overlaps with desired access, deny if ace.access_mask & desired_access: return DENY elif ace.type == ACCESS_ALLOWED_ACE: # Accumulate allowed permissions newly_granted = ace.access_mask & remaining_access granted_access |= newly_granted remaining_access &= ~newly_granted # If all requested permissions are granted, allow if remaining_access == 0: return ALLOW # If we've checked all ACEs and not all permissions are granted if remaining_access != 0: return DENY return ALLOW def ace_applies_to_token(ace, token): """Check if an ACE's SID matches the token's user or any group.""" if ace.sid == token.user_sid: return True for group_sid in token.group_sids: if ace.sid == group_sid: return True return FalseIn Windows, explicit Deny ACEs are conventionally placed before Allow ACEs, and any Deny match immediately terminates with denial. In POSIX ACLs, there's no explicit deny—a user entry with no permissions (---) effectively denies access, but this only works when that entry is evaluated before group entries. Understanding these differences is crucial when designing cross-platform access control.
ACLs must be persistently stored alongside the objects they protect. The storage mechanism varies by file system and operating system, but there are common approaches:
Extended Attributes Approach (POSIX):
Modern Unix/Linux file systems store ACLs as extended attributes (xattrs)—name-value pairs associated with files beyond the traditional inode metadata.
1234567891011121314151617181920212223242526
# Extended attributes used for ACLssystem.posix_acl_access # Access ACL for the objectsystem.posix_acl_default # Default ACL for directories (inheritance) # View raw extended attributes$ getfattr -d -m - /contracts/# file: contracts/system.posix_acl_access=0sAgAAAAEABwD/////AgAHAAQEAAACAA...system.posix_acl_default=0sAgAAAAEABwD/////AgAHAAQEAAA... # The binary format contains:# - ACL version (4 bytes)# - For each entry:# - Tag type (2 bytes): USER_OBJ, USER, GROUP_OBJ, GROUP, MASK, OTHER# - Permissions (2 bytes): rwx bitmask# - ID (4 bytes): UID or GID (0xFFFFFFFF for OBJ types) # File system inode structure (simplified ext4):struct ext4_inode { __le16 i_mode; /* Traditional permissions */ __le16 i_uid; /* Owner UID */ __le16 i_gid; /* Owner GID */ ... __le32 i_file_acl; /* Pointer to extended attribute block */ ...};Security Descriptor Approach (Windows NTFS):
Windows NTFS stores ACLs as part of the $SECURE system file, with pointers from each file's Master File Table (MFT) entry. This approach enables ACL deduplication—identical ACLs are stored once and shared.
| Aspect | POSIX/Linux (ext4, XFS) | Windows (NTFS) |
|---|---|---|
| Storage Location | Extended attributes on each inode | Centralized $SECURE file |
| Deduplication | No (each file has own copy) | Yes (identical ACLs shared) |
| Maximum ACL Size | Depends on xattr limit (typically 64KB) | 64KB per security descriptor |
| Maximum Entries | ~1000-2000 entries typical | ~1800 entries practical limit |
| Inheritance Storage | Default ACL in parent directory | Inheritance flags in each ACE |
| Backup Compatibility | Requires xattr-aware tools | Built into Windows backup APIs |
The different storage approaches have significant performance implications. POSIX xattr storage means every ACL check requires reading per-file attributes, but modifications are independent. NTFS deduplication reduces storage but means the $SECURE file can become a bottleneck on systems with many unique ACLs. We'll explore these tradeoffs in detail in the ACL Performance page.
Understanding when to use ACLs versus traditional permissions requires appreciating the tradeoffs involved. Neither approach is universally better—the choice depends on your specific requirements.
| Criterion | Traditional Permissions | Access Control Lists |
|---|---|---|
| Flexibility | Limited to 3 categories | Unlimited named users/groups |
| Explicit Deny | Not possible | Supported (Windows) or empty perms (POSIX) |
| Inheritance Control | Basic umask only | Fine-grained inheritance rules |
| Administrative Overhead | Lower (simpler model) | Higher (more entries to manage) |
| Storage Cost | 9 bits per file | Variable, can be significant |
| Performance | Single bitmask comparison | ACL walk required |
| Tool Support | Universal (ls, chmod) | Varies (getfacl, setfacl, icacls) |
| Backup/Restore | Standard tools work | Requires ACL-aware tools |
| Portability | POSIX standard | POSIX.1e draft (Linux), proprietary (Windows) |
| Audit Capability | Limited | Full audit ACLs (Windows SACL) |
In practice, the best approach is often hybrid: use traditional permissions for the common case (owner and primary group) and add ACL entries only when specific exceptions are needed. This minimizes ACL overhead while providing flexibility when required. POSIX ACLs are designed to transparently extend traditional permissions—files without ACL entries behave exactly as before.
Access Control Lists represent a fundamental evolution in operating system security, enabling fine-grained access control that traditional permission models cannot achieve. Let's consolidate the key concepts:
What's Next:
With the conceptual foundation established, we'll dive into the specifics of ACL entries in the next page. You'll learn about the different entry types, permission bits, flags, and how to construct ACL entries that accurately express your access control requirements.
You now understand what Access Control Lists are, why they exist, and how they solve the fundamental limitations of traditional Unix permissions. You've seen the architectural components, evaluation algorithms, and storage mechanisms. Next, we'll explore ACL entries in detail, examining the building blocks that make up these powerful permission structures.