Linux ACL Part 6: Advanced Access Control Lists Guide
Linux ACL Part 6: Advanced Access Control Lists Guide
So far you have learned about access ACLs — the ones that control who can read, write, or execute a file. But Linux ACLs have a second type: the default ACL. A default ACL lives on a directory and acts as a blueprint. Every new file or subdirectory created inside that directory automatically inherits permissions from the default ACL. This is very powerful for shared project directories where you want consistent permissions on all new content without manually running chmod every time.
1. What is a Default ACL?
A default ACL is a special ACL that you set on a directory. It has no effect on who can access the directory itself. Its only job is to control what ACL and permissions are assigned to new files and subdirectories created inside that directory.
Think of it like a permission template. When a process creates a file inside the directory, Linux automatically copies the default ACL and applies it as the new file’s access ACL.
| ACL Type | Set on | Controls | Storage (xattr) |
|---|---|---|---|
| Access ACL | Files & Directories | Who can access this file/dir | system.posix_acl_access |
| Default ACL | Directories only | Permissions of new files inside | system.posix_acl_default |
2. Setting and Viewing Default ACLs
You use the same getfacl and setfacl commands but with the -d flag to work with default ACLs.
# Create a shared project directory
mkdir /projects/alpha
# Set default ACL: owner=rwx, user paulh=rx, group=rx, group:teach=rwx, other=none
setfacl -d -m u::rwx,u:paulh:rx,g::rx,g:teach:rwx,o::- /projects/alpha
# View the default ACL
getfacl -d --omit-header /projects/alpha
Expected output:
user::rwx
user:paulh:r-x
group::r-x
group:teach:rwx
mask::rwx # setfacl auto-generated this
other::---
3. How File Creation Inherits the Default ACL
When any new file or subdirectory is created inside a directory that has a default ACL, Linux performs the following steps:
ACL_USER_OBJ, ACL_MASK, ACL_OTHER entries are ANDed with the mode argument of open().
The entries that get masked (ANDed) against the mode argument in open() / mkdir() are:
- ACL_USER_OBJ — ANDed with owner bits of mode
- ACL_MASK (or ACL_GROUP_OBJ if no mask) — ANDed with group bits of mode
- ACL_OTHER — ANDed with other bits of mode
/* Scenario:
/projects/alpha has this default ACL:
user::rwx
user:paulh:r-x
group::r-x
group:teach:rwx
mask::rwx
other::---
We create a file with mode rwx--x--x (0711)
*/
int fd = open("sub/tfile", O_RDWR | O_CREAT, S_IRWXU | S_IXGRP | S_IXOTH);
/* Linux now builds the access ACL for sub/tfile by:
- Copying the default ACL
- ANDing ACL_USER_OBJ (rwx) with owner bits of 0711 = rwx → rwx
- ANDing ACL_MASK (rwx) with group bits of 0711 = --x → --x
- ANDing ACL_OTHER (---) with other bits of 0711 = --x → ---
*/
The resulting access ACL on the new file:
$ getfacl --omit-header sub/tfile
user::rwx
user:paulh:r-x #effective:--x (r-x ANDed with mask --x = --x)
group::r-x #effective:--x
group:teach:rwx #effective:--x
mask::--x
other::---
4. umask is Bypassed When Default ACL Exists
Normally, when you create a file, the kernel applies your process’s umask to turn off certain permission bits. For example, if umask is 022, the group-write and other-write bits are always cleared.
However, when the parent directory has a default ACL, the umask is NOT applied. Instead, the mode argument you pass to open() is ANDed directly against the inherited ACL entries as shown above.
#!/bin/bash
mkdir /tmp/test_default_acl
cd /tmp/test_default_acl
umask 077 # Strict umask: allow only owner
# Without default ACL:
touch file_no_acl
ls -l file_no_acl
# Expected: -rw------- (umask applied)
# Set a default ACL on the directory
setfacl -d -m u::rw,g::r,o::r /tmp/test_default_acl
# Now create a new file — umask will be IGNORED
touch file_with_acl
ls -l file_with_acl
# Expected: -rw-r--r-- (default ACL applied, not umask)
getfacl --omit-header file_with_acl
5. Default ACL Propagates to Subdirectories
This is one of the most useful properties: when you create a subdirectory inside a directory that has a default ACL, the subdirectory automatically gets:
- The parent’s default ACL as its own access ACL
- The parent’s default ACL as its own default ACL
This means the default ACL ripples down through the entire directory tree automatically as you create subdirectories.
#!/bin/bash
# Setup
mkdir -p /shared/project
setfacl -d -m u::rwx,g::rx,g:devteam:rwx,o::- /shared/project
# Create a nested subdirectory
mkdir /shared/project/src
# Check access ACL of src (should match parent's default ACL)
echo "=== Access ACL of src ==="
getfacl --omit-header /shared/project/src
# Check default ACL of src (should also be propagated)
echo "=== Default ACL of src ==="
getfacl -d --omit-header /shared/project/src
# Create src/main.c — inherits access ACL from src's default ACL
touch /shared/project/src/main.c
echo "=== Access ACL of main.c ==="
getfacl --omit-header /shared/project/src/main.c
6. Removing a Default ACL
You can remove the default ACL from a directory in two ways:
setfacl -k /path/to/dir
acl_delete_def_file(pathname)
# Shell way
setfacl -k /projects/alpha
# Verify it's gone
getfacl -d --omit-header /projects/alpha
# Should now show only minimal ACL or nothing
/* C way */
#include <sys/acl.h>
#include <stdio.h>
int main() {
const char *dir = "/projects/alpha";
if (acl_delete_def_file(dir) == -1) {
perror("acl_delete_def_file");
return 1;
}
printf("Default ACL removed from %s\n", dir);
return 0;
}
/* Compile: gcc remove_default_acl.c -lacl -o remove_default_acl */
7. What Happens When There Is No Default ACL?
If a directory has no default ACL, file creation falls back to the traditional Unix rules:
- New file permissions =
modeargument toopen()masked by the process umask - The new file gets a minimal ACL (just user, group, other) derived from the permission bits
- New subdirectories also get no default ACL
#!/bin/bash
# Case 1: Directory WITHOUT default ACL
mkdir /tmp/no_default
umask 022
touch /tmp/no_default/file1
ls -l /tmp/no_default/file1
# Result: -rw-r--r-- (umask 022 applied to open mode 0666)
# Case 2: Directory WITH default ACL
mkdir /tmp/with_default
setfacl -d -m u::rw,u:alice:rw,g::r,o::- /tmp/with_default
umask 077 # Even with strict umask, it won't matter
touch /tmp/with_default/file2
ls -l /tmp/with_default/file2
getfacl --omit-header /tmp/with_default/file2
# Result: alice gets rw, group gets r, others get nothing
8. Practical Example – Shared Development Directory
#!/bin/bash
# Goal: /codebase owned by root, all devs can read/write new files
# alice is team lead (full access), devteam group gets rw
groupadd devteam
useradd -G devteam alice
useradd -G devteam bob
useradd -G devteam charlie
mkdir /codebase
chown root:devteam /codebase
chmod 2775 /codebase # setgid bit so new files inherit group
# Set default ACL so all new files/dirs are group-writable by default
setfacl -d -m u::rwx,u:alice:rwx,g::rwx,o::rx /codebase
# Verify
echo "=== Default ACL ==="
getfacl -d --omit-header /codebase
# Now create files as different users
su bob -c "touch /codebase/feature.c"
su charlie -c "mkdir /codebase/tests"
echo "=== feature.c ACL ==="
getfacl --omit-header /codebase/feature.c
echo "=== tests/ ACL ==="
getfacl --omit-header /codebase/tests
echo "=== tests/ DEFAULT ACL (propagated) ==="
getfacl -d --omit-header /codebase/tests
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
/* Sets a default ACL on a directory.
Usage: ./set_default_acl <directory> <acl_string>
Example: ./set_default_acl /projects "u::rwx,g::r-x,o::---"
*/
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <dir> <acl_text>\n", argv[0]);
return 1;
}
const char *dirpath = argv[1];
const char *acl_text = argv[2];
/* Parse ACL from text */
acl_t acl = acl_from_text(acl_text);
if (acl == NULL) {
perror("acl_from_text");
return 1;
}
/* Validate ACL */
if (acl_valid(acl) != 0) {
fprintf(stderr, "Invalid ACL string\n");
acl_free(acl);
return 1;
}
/* Apply as default ACL to directory */
if (acl_set_file(dirpath, ACL_TYPE_DEFAULT, acl) == -1) {
perror("acl_set_file (default)");
acl_free(acl);
return 1;
}
printf("Default ACL set on %s\n", dirpath);
acl_free(acl);
return 0;
}
/* Compile: gcc set_default_acl.c -lacl -o set_default_acl */
9. Using ACL_TYPE_DEFAULT in the C API
In the ACL C API, all functions that read or write ACLs accept a type argument. Use ACL_TYPE_DEFAULT to work with the default ACL of a directory.
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
return 1;
}
/* Fetch the DEFAULT ACL */
acl_t acl = acl_get_file(argv[1], ACL_TYPE_DEFAULT);
if (acl == NULL) {
perror("acl_get_file");
return 1;
}
/* Convert to human-readable text */
ssize_t len;
char *text = acl_to_text(acl, &len);
if (text == NULL) {
perror("acl_to_text");
acl_free(acl);
return 1;
}
printf("Default ACL of %s:\n%s\n", argv[1], text);
acl_free(text);
acl_free(acl);
return 0;
}
/* Compile: gcc read_default_acl.c -lacl -o read_default_acl */
acl_get_file(dir, ACL_TYPE_DEFAULT) returns an in-memory ACL with zero entries rather than NULL. You can check acl_entries(acl) == 0 to distinguish this case (note: acl_entries() is a Linux extension).#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
int copy_default_acl(const char *src_dir, const char *dst_dir) {
/* Read source default ACL */
acl_t acl = acl_get_file(src_dir, ACL_TYPE_DEFAULT);
if (acl == NULL) {
perror("acl_get_file (source)");
return -1;
}
/* Write it to destination as default ACL */
if (acl_set_file(dst_dir, ACL_TYPE_DEFAULT, acl) == -1) {
perror("acl_set_file (destination)");
acl_free(acl);
return -1;
}
acl_free(acl);
printf("Default ACL copied from %s to %s\n", src_dir, dst_dir);
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <src_dir> <dst_dir>\n", argv[0]);
return 1;
}
return (copy_default_acl(argv[1], argv[2]) == 0) ? 0 : 1;
}
/* Compile: gcc copy_default_acl.c -lacl -o copy_default_acl */
Interview Questions
mode argument passed to open() or mkdir() is ANDed directly against the corresponding entries of the inherited ACL (ACL_USER_OBJ, ACL_MASK or ACL_GROUP_OBJ, ACL_OTHER). This allows fine-grained control without umask interference.system.posix_acl_default. The access ACL is stored in system.posix_acl_access. You can inspect these with getfattr -n system.posix_acl_default <dir>.setfacl -k <directory>. Using the C API: acl_delete_def_file(pathname). Both remove only the default ACL; the access ACL of the directory is not affected.mode argument of open() with the bits turned off by the process umask. The new file gets a minimal ACL (user, group, other only), and new subdirectories also get no default ACL.acl_get_file(dir, ACL_TYPE_DEFAULT) returns a non-NULL handle even when there is no default ACL — it returns an empty ACL object. You can use the Linux extension acl_entries(acl) to check if the returned ACL has zero entries, which means no default ACL is set.Next: The complete ACL C API — data types, all functions, and a full acl_view.c walkthrough.
