Access Control Lists (ACL) in Linux: Complete Guide
Traditional Unix permissions give you three buckets: owner, group, and others. Every file has exactly one owner and one group. This works for simple cases, but falls short when you need different access levels for different people. Access Control Lists (ACLs) solve this by letting you define permissions for any number of specific users and groups on a single file.
ACL support was added to the Linux kernel in version 2.6. The implementation is based on the POSIX.1e draft standard (Draft 17), which was never officially finalized but became the de-facto standard across many UNIX systems.
Key Terms in This Topic
Why Traditional Permissions Are Not Enough
Consider a shared directory on a project server with this requirement:
| User | Role | Needed Access | Possible With chmod? |
|---|---|---|---|
| alice | Owner | Read + Write + Execute | ✔ Yes (owner bits) |
| bob | Reviewer | Read only | ✘ Not individually |
| charlie | Developer | Read + Write | ✘ Not individually |
| others | Public | No access | ✔ Yes (other bits) |
With traditional chmod, bob and charlie would need to be in the same group and get the same permissions. ACLs let you give each user exactly the right access.
Structure of an ACL Entry
An ACL is a list of entries. Each entry has exactly three parts:
| Part | What It Stores | Example | Used For |
|---|---|---|---|
| Tag Type | Category this entry applies to | ACL_USER |
All entries |
| Tag Qualifier | Specific UID or GID | 1007 |
Only ACL_USER & ACL_GROUP |
| Permission Set | r / w / x flags granted | r-x |
All entries |
tag-type:[tag-qualifier]:permissionsExample:
user:bob:r-x or group::r--All Six Tag Types Explained
Present in every ACL exactly once. Defines what the file owner (the user who created or owns the file) can do. Directly maps to the traditional owner permission bits (the leftmost rwx in ls -l).
# In text form — no qualifier needed, just a dash
ACL_USER_OBJ - rwx
# In getfacl short format:
user::rwx
Can have zero or more of these, but at most one per user. Each entry gives specific permissions to one particular user, identified by their UID. This is the power of ACLs — giving custom rights to any user.
# User with UID 1007 gets read-only
ACL_USER 1007 r--
# User 'bob' gets read + execute
user:bob:r-x
# User 'alice' gets full access
user:alice:rwx
Present in every ACL exactly once. Defines permissions for the file’s owning group. When no ACL_MASK entry exists, this maps to the traditional group bits. When ACL_MASK is present, the mask value shows in group bits instead.
ACL_GROUP_OBJ - r-x
# In getfacl format:
group::r-x
Can have zero or more of these, but at most one per group. Gives permissions to a specific group by GID. Very useful for team-based access control.
# Group 'developers' gets read+write
ACL_GROUP developers rw-
# Group ID 103 gets write-only
ACL_GROUP 103 -w-
# In getfacl format:
group:teach:--x
At most one per ACL. Acts as an upper limit on permissions that can be granted by ACL_USER, ACL_GROUP_OBJ, and ACL_GROUP entries. Mandatory if any ACL_USER or ACL_GROUP entries exist. This entry is the key to compatibility with traditional chmod(). Covered in depth in Part 4.
# Even if a user entry says rwx, effective = rwx AND mask
# If mask is rw-, user gets rw- at most
ACL_MASK - rw-
# In getfacl format:
mask::rw-
Present in every ACL exactly once. Applies to any process that does not match any other ACL entry. Maps directly to the traditional “other” permission bits.
ACL_OTHER - r--
# In getfacl format:
other::r--
Full ACL — Visual Layout
Here is what a complete extended ACL looks like for a file:
| Tag Type | Qualifier | Perms | Notes |
|---|---|---|---|
| ACL_USER_OBJ | — | rwx | File owner. Maps to traditional owner bits |
| ACL_USER | 1007 | r– | User 1007 — read only |
| ACL_USER | 1010 | rwx | User 1010 — full access (masked by ACL_MASK) |
| ACL_GROUP_OBJ | — | rwx | File’s owning group (part of group class) |
| ACL_GROUP | 102 | r– | Group 102 — read only |
| ACL_GROUP | 103 | -w- | Group 103 — write only |
| ACL_MASK | — | rw- | Max permissions for group class entries |
| ACL_OTHER | — | r– | Everyone else. Maps to traditional other bits |
Minimal ACL vs Extended ACL
Exactly 3 entries — same as traditional Unix permissions. No ACL_USER, ACL_GROUP, or ACL_MASK entries.
Storage: Stored in regular file permission bits. No extra extended attribute needed.
user::rw-
group::r--
other::r--
When you do ls -l, no + sign appears after permissions.
Has ACL_USER, ACL_GROUP, and ACL_MASK entries in addition to the three base entries.
Storage: Stored in system.posix_acl_access extended attribute on disk.
user::rw-
user:bob:r--
group::r--
mask::r--
other::---
When you do ls -l, a + sign appears after permissions.
How to Enable ACL Support
ACLs need kernel + filesystem support. Here is how to enable them:
# Step 1: Mount filesystem with ACL support
sudo mount -o remount,acl /dev/sda1 /home
# Or add to /etc/fstab permanently:
# /dev/sda1 /home ext4 defaults,acl 0 2
# Step 2: Verify ACL support is enabled
tune2fs -l /dev/sda1 | grep "Default mount options"
# Should show: acl
# Step 3: Check kernel config (ACL must be built in or as module)
grep -i acl /boot/config-$(uname -r)
# CONFIG_EXT4_FS_POSIX_ACL=y
acl mount option.Coding Examples
# Create a fresh file
touch project.c
# Check permissions the traditional way
ls -l project.c
# -rw-r--r-- 1 ravi users 0 Jan 1 10:00 project.c
# No '+' sign = minimal ACL only
# View ACL using getfacl
getfacl project.c
# file: project.c
# owner: ravi
# group: users
user::rw- <-- ACL_USER_OBJ (owner)
group::r-- <-- ACL_GROUP_OBJ
other::r-- <-- ACL_OTHER
# Give bob read-only access
setfacl -m u:bob:r-- project.c
# Give devteam group read+write access
setfacl -m g:devteam:rw- project.c
# View the new ACL
getfacl project.c
# file: project.c
# owner: ravi
# group: users
user::rw-
user:bob:r-- <-- new ACL_USER entry
group::r--
group:devteam:rw- <-- new ACL_GROUP entry
mask::rw- <-- setfacl auto-created this!
other::r--
# Notice: ls -l now shows a '+' sign
ls -l project.c
# -rw-rw-r--+ 1 ravi users 0 Jan 1 10:00 project.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
#include <acl/libacl.h>
/* Returns human-readable tag type name */
const char *tag_to_str(acl_tag_t tag) {
switch (tag) {
case ACL_USER_OBJ: return "ACL_USER_OBJ ";
case ACL_USER: return "ACL_USER ";
case ACL_GROUP_OBJ: return "ACL_GROUP_OBJ";
case ACL_GROUP: return "ACL_GROUP ";
case ACL_MASK: return "ACL_MASK ";
case ACL_OTHER: return "ACL_OTHER ";
default: return "UNKNOWN ";
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s filename\n", argv[0]);
return 1;
}
/* Load ACL from disk into memory */
acl_t acl = acl_get_file(argv[1], ACL_TYPE_ACCESS);
if (!acl) { perror("acl_get_file"); return 1; }
printf("File: %s\n", argv[1]);
printf("%-14s %-10s %s\n", "Tag Type", "Qualifier", "Permissions");
printf("%-14s %-10s %s\n", "--------", "---------", "-----------");
acl_entry_t entry;
acl_tag_t tag;
int id = ACL_FIRST_ENTRY;
/* Loop through all ACL entries */
while (acl_get_entry(acl, id, &entry) == 1) {
id = ACL_NEXT_ENTRY;
acl_get_tag_type(entry, &tag);
printf("%-14s ", tag_to_str(tag));
/* Print qualifier (UID/GID) if applicable */
if (tag == ACL_USER) {
uid_t *uid = acl_get_qualifier(entry);
printf("UID=%-6d ", (int)*uid);
acl_free(uid);
} else if (tag == ACL_GROUP) {
gid_t *gid = acl_get_qualifier(entry);
printf("GID=%-6d ", (int)*gid);
acl_free(gid);
} else {
printf("%-10s ", "-");
}
/* Print permissions */
acl_permset_t perms;
acl_get_permset(entry, &perms);
printf("%c%c%c\n",
acl_get_perm(perms, ACL_READ) ? 'r' : '-',
acl_get_perm(perms, ACL_WRITE) ? 'w' : '-',
acl_get_perm(perms, ACL_EXECUTE) ? 'x' : '-'
);
}
acl_free(acl);
return 0;
}
/* Compile: gcc -o read_acl read_acl.c -lacl
Usage: ./read_acl project.c */
#include <stdio.h>
#include <sys/stat.h>
#include <sys/acl.h>
#include <acl/libacl.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s filename\n", argv[0]);
return 1;
}
acl_t acl = acl_get_file(argv[1], ACL_TYPE_ACCESS);
if (!acl) { perror("acl_get_file"); return 1; }
mode_t mode;
/* acl_equiv_mode returns 0 if ACL == traditional perms (minimal) */
int is_minimal = (acl_equiv_mode(acl, &mode) == 0);
printf("File: %s\n", argv[1]);
printf("ACL type: %s\n", is_minimal ? "MINIMAL (3 entries)" : "EXTENDED");
if (is_minimal) {
printf("Equiv perms: %04o\n", (unsigned)mode & 0777);
printf("No extended attribute needed on disk.\n");
} else {
printf("Stored in: system.posix_acl_access extended attribute\n");
printf("ls -l will show a '+' after permissions.\n");
}
acl_free(acl);
return 0;
}
/* Compile: gcc -o acl_type acl_type.c -lacl */
Interview Questions
Q1. What is an ACL in Linux? How does it differ from traditional file permissions?
Q2. What are the six ACL tag types and what does each represent?
- ACL_USER_OBJ — permissions for the file owner (exactly 1 per ACL)
- ACL_USER — permissions for a specific named user (0 or more; max 1 per user)
- ACL_GROUP_OBJ — permissions for the file’s owning group (exactly 1)
- ACL_GROUP — permissions for a specific named group (0 or more; max 1 per group)
- ACL_MASK — upper bound on group class entry permissions (0 or 1)
- ACL_OTHER — permissions for all other users (exactly 1)
Q3. What is the difference between a minimal ACL and an extended ACL? How does Linux store each?
system.posix_acl_access extended attribute on the filesystem.Q4. What is the “group class” in ACL terminology?
Q5. Which filesystem versions support ACLs in Linux and what is required to enable them?
-o acl mount option (though many modern distros enable this by default). ACL support is also a kernel configuration option under the File Systems menu. The kernel version must be 2.6 or later.Q6. If you run ls -l on a file and see a + after the permissions string (e.g., -rwxr-x--x+), what does that mean?
+ sign indicates that the file has an extended ACL — it has ACL_USER or ACL_GROUP entries beyond the standard three. Use getfacl filename to view the full ACL.Learn exactly how Linux decides if a process can access a file that has an ACL
