Linux ACL Part 6: Advanced Access Control Lists Guide

 

Linux ACL Part 6: Advanced Access Control Lists Guide

Linux ACL Part 6: Advanced Access Control Lists Guide

📂
Default ACL
🧬
Inheritance
🔧
setfacl -d
📄
open() / mkdir()

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.

Key Concepts in This Post
default ACL access ACL setfacl -d getfacl -d setfacl -k ACL inheritance umask interaction open() with ACL mkdir() with ACL system.posix_acl_default ACL_MASK in inheritance acl_delete_def_file()

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
Key point: Default ACLs only exist on directories. Setting a default ACL on a file has no meaning and will be rejected by the kernel.

2. Setting and Viewing Default ACLs

You use the same getfacl and setfacl commands but with the -d flag to work with default ACLs.

Example 1 – Create a directory and set a default ACL
# 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::---
Note: setfacl automatically creates an ACL_MASK entry when you have ACL_USER or ACL_GROUP entries — same behaviour as with access ACLs.

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:

Directory has a Default ACL
New FILE created
Inherits default ACL as access ACL.
ACL_USER_OBJ, ACL_MASK, ACL_OTHER entries are ANDed with the mode argument of open().
New SUBDIRECTORY created
Inherits default ACL as its access ACL AND also as its own default ACL (propagation).
umask is ignored when a parent directory has a default ACL

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
Example 2 – See how open() applies the mode mask
/* 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.

Example 3 – Demonstrate umask bypass
#!/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.

Example 4 – Propagation demo
#!/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

📁 /shared/project ← default ACL set here
📁 src/ inherits access ACL + default ACL
📄 main.c — inherits access ACL from src’s default
📁 utils/ also inherits + propagates further

6. Removing a Default ACL

You can remove the default ACL from a directory in two ways:

Shell command

setfacl -k /path/to/dir

Removes the default ACL. The access ACL is untouched.
C API

acl_delete_def_file(pathname)

Removes the default ACL of the directory at pathname.
Example 5 – Remove default ACL
# 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 = mode argument to open() 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
Example 6 – Traditional vs default ACL behaviour
#!/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

Example 7 – Set up a shared project directory for a dev team
#!/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
Example 8 – C program to set default ACL programmatically
#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.

Example 9 – Read and print 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 */
Tip: If a directory has no 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).
Example 10 – Copy default ACL from one directory to another
#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

Q1. What is the difference between an access ACL and a default ACL?
An access ACL controls who can access a specific file or directory. A default ACL is set only on directories and acts as a template for the ACL that new files and subdirectories created inside that directory will automatically receive. The default ACL does not affect access to the directory itself.
Q2. How does the process umask interact with default ACLs during file creation?
When a parent directory has a default ACL, the process umask is bypassed. Instead, the 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.
Q3. How does a default ACL propagate through a directory tree?
When a new subdirectory is created inside a directory that has a default ACL, the subdirectory automatically inherits both an access ACL and a default ACL from the parent’s default ACL. This means the default ACL propagates recursively downward as you create subdirectories, ensuring consistent permissions across the entire tree.
Q4. Which extended attribute stores the default ACL on disk?
The default ACL is stored in the extended attribute named 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>.
Q5. How do you remove the default ACL from a directory?
Using the shell: 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.
Q6. What happens if a directory has NO default ACL when a new file is created inside it?
File creation falls back to traditional Unix rules: the permissions of the new file are set to the 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.
Q7. In the C API, how do you distinguish between “no default ACL” and “default ACL with zero entries”?
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.

Continue Learning Linux ACLs

Next: The complete ACL C API — data types, all functions, and a full acl_view.c walkthrough.

Part 7: ACL C API → ← Part 5: getfacl & setfacl

Leave a Reply

Your email address will not be published. Required fields are marked *