Chapter 1: Permissions
Interview Questions
This chapter answers to the following questions:
- Scenario: SSH Key Rejected — “Unprotected Private Key”
- Scenario: Nginx Returns 403 Forbidden
- Scenario: Docker Bind Mount — Permission Denied Writing /app/data
Linux File Permissions
Every file and directory in Linux has an associated set of permissions that control who can read, write, or execute it. Understanding this system is fundamental to Linux administration and almost always comes up in technical interviews.
The Permission Model
Linux uses a discretionary access control (DAC) model. Each file has three permission classes:
| Class | Applies to |
|---|---|
| User (u) | The file’s owner |
| Group (g) | Members of the file’s group |
| Other (o) | Everyone else |
Each class has three permission bits:
| Permission | Symbol | Octal | On a file | On a directory |
|---|---|---|---|---|
| Read | r | 4 | View file contents | List directory contents (ls) |
| Write | w | 2 | Modify file contents | Create, delete, rename files inside |
| Execute | x | 1 | Run as a program | Enter the directory (cd) |

Reading the Permission String
Run ls -l to see permissions:
-rwxr-xr-- 1 alice devs 4096 May 30 10:00 script.sh
Break down the first field, -rwxr-xr--:
- rwx r-x r--
│ │ │ └── Other: read only
│ │ └────── Group: read + execute
│ └────────── User: read + write + execute
└──────────── File type: - (regular file)
File type characters:
| Symbol | Type |
|---|---|
- | Regular file |
d | Directory |
l | Symbolic link |
c | Character device |
b | Block device |
p | Named pipe (FIFO) |
s | Socket |
Octal (Numeric) Notation
Each permission class is represented as a 3-bit binary number, summed into a single octal digit:
rwx = 4+2+1 = 7
r-x = 4+0+1 = 5
r-- = 4+0+0 = 4
So -rwxr-xr-- = 754.
Common permission values:
| Octal | Binary | Symbolic | Meaning |
|---|---|---|---|
777 | 111 111 111 | rwxrwxrwx | Full access for everyone |
755 | 111 101 101 | rwxr-xr-x | Owner full; others read/exec |
644 | 110 100 100 | rw-r--r-- | Owner read/write; others read |
600 | 110 000 000 | rw------- | Owner read/write only |
700 | 111 000 000 | rwx------ | Owner full, no one else |
Changing Permissions: chmod
Symbolic mode — readable and expressive:
chmod u+x script.sh # add execute for owner
chmod g-w file.txt # remove write from group
chmod o=r file.txt # set other to read-only exactly
chmod a+r file.txt # add read for all (a = ugo)
chmod u+x,g-w file.txt # multiple changes at once
Numeric mode — precise and scriptable:
chmod 755 script.sh # rwxr-xr-x
chmod 644 config.conf # rw-r--r--
chmod 600 ~/.ssh/id_rsa # rw------- (required by SSH)
Recursive:
chmod -R 755 /var/www/html
💡 Interview tip: Prefer numeric mode in scripts for clarity and predictability; prefer symbolic mode when doing targeted, additive changes interactively.
Changing Ownership: chown and chgrp
chown alice file.txt # change owner to alice
chown alice:devs file.txt # change owner and group
chown :devs file.txt # change group only
chgrp devs file.txt # change group (equivalent)
chown -R alice:devs /srv/app # recursive
Only the root user (or a process with CAP_CHOWN capability) can change a file’s owner. A regular user can only change the group of a file they own, and only to a group they belong to.
Special Permission Bits
Beyond rwx, there are three special bits that modify execution behavior:
setuid (SUID) — octal 4000
When set on an executable file, the process runs with the file owner’s privileges instead of the invoking user’s.
chmod u+s /usr/bin/passwd # symbolic
chmod 4755 /usr/bin/passwd # numeric
In ls -l output, the owner execute bit shows s (or S if execute is not set):
-rwsr-xr-x root root /usr/bin/passwd
passwdis the classic example: a normal user needs to write to/etc/shadow(owned by root), so the binary runs as root via SUID.
On a directory, SUID has no standard effect on Linux.
setgid (SGID) — octal 2000
On an executable file, the process runs with the file’s group privileges.
On a directory, new files created inside inherit the directory’s group instead of the creator’s primary group — essential for shared project directories.
chmod g+s /srv/shared # symbolic
chmod 2775 /srv/shared # numeric
In ls -l, the group execute bit shows s:
drwxrwsr-x alice devs /srv/shared
Sticky Bit — octal 1000
On a directory, only the file’s owner, the directory’s owner, or root can delete or rename files within it — even if others have write permission on the directory.
chmod +t /tmp # symbolic
chmod 1777 /tmp # numeric
In ls -l, the other execute bit shows t:
drwxrwxrwt root root /tmp
/tmpis the canonical example: world-writable but protected so users can’t delete each other’s files.
Summary of special bits:
| Bit | Octal | On file | On directory |
|---|---|---|---|
| SUID | 4000 | Run as file owner | No standard effect |
| SGID | 2000 | Run as file group | New files inherit directory group |
| Sticky | 1000 | No standard effect | Only owner can delete/rename files |

The umask
The umask defines which permission bits are removed from the default when a new file or directory is created.
Default creation modes:
- Files:
0666(no execute by default) - Directories:
0777
With a umask of 0022:
- Files created as:
0666 - 0022 = 0644(rw-r--r--) - Directories created as:
0777 - 0022 = 0755(rwxr-xr-x)
umask # display current umask
umask 0027 # set new umask (files: 0640, dirs: 0750)
umask -S # display in symbolic form (u=rwx,g=rx,o=)
The umask is subtracted bitwise, not arithmetically. Think of it as a mask of bits to clear, not a number to subtract.
Access Control Lists (ACLs)
Standard Unix permissions allow only one owner and one group per file. ACLs extend this by allowing per-user and per-group rules.
# View ACLs
getfacl file.txt
# Grant bob read+write
setfacl -m u:bob:rw file.txt
# Grant the ops group read-only
setfacl -m g:ops:r file.txt
# Remove bob's ACL entry
setfacl -x u:bob file.txt
# Remove all ACL entries
setfacl -b file.txt
When ACLs are present, ls -l shows a + after the permission string:
-rw-rw-r--+ alice devs file.txt
Scenario: SSH Key Rejected — “Unprotected Private Key”
The Situation
You have generated an SSH key pair and are trying to connect to a remote server:
ssh -i ~/.ssh/id_rsa alice@remote-server.example.com
Instead of connecting, you receive this error:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/alice/.ssh/id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/home/alice/.ssh/id_rsa": bad permissions
alice@remote-server.example.com: Permission denied (publickey).

Diagnosis
SSH refuses to use any private key file that other users can read. The error message tells you exactly what is wrong: the key file at ~/.ssh/id_rsa has permissions 0644 (rw-r--r--), meaning the group and other classes can read it.
This is a deliberate security enforcement built into the SSH client. A private key readable by others is considered compromised — any user on the same system could copy it and impersonate you.
Check the current permissions to confirm:
ls -la ~/.ssh/
You might see something like:
drwxr-xr-x alice alice .ssh/
-rw-r--r-- alice alice id_rsa ← too open (0644)
-rw-r--r-- alice alice id_rsa.pub
Solution
Restrict the private key so only the owner can read it:
chmod 600 ~/.ssh/id_rsa
This sets permissions to rw-------: only the owner can read or write the file. Group and other have zero access, satisfying SSH’s requirement.
While you’re there, fix the .ssh directory itself if needed:
chmod 700 ~/.ssh
Then retry:
ssh -i ~/.ssh/id_rsa alice@remote-server.example.com
Why SSH Enforces This
SSH operates on the principle that a private key is a secret credential. If the file is world-readable or group-readable, any process or user sharing the system could read your key and use it to authenticate as you to any server that trusts it.
Unlike a password (which is verified by the remote server), a private key never leaves your machine — the client uses it to sign a challenge. There is no server-side rate limiting or lockout to protect against a stolen key. Once someone has a copy, they have permanent access until you revoke the public key on every remote server. SSH therefore refuses to proceed rather than silently use a key that may already be compromised.
Reference: Correct .ssh Permissions
| Path | Permission | Reason |
|---|---|---|
~/.ssh/ | 700 | Only owner can enter or list the directory |
~/.ssh/id_rsa | 600 | Private key — owner read/write only |
~/.ssh/id_ed25519 | 600 | Same rule applies to all private key formats |
~/.ssh/id_rsa.pub | 644 | Public key — safe to be world-readable |
~/.ssh/authorized_keys | 600 | SSHd rejects looser permissions on this file too |
~/.ssh/known_hosts | 644 | Readable by owner; group/other read is acceptable |
~/.ssh/config | 600 | May contain IdentityFile paths and proxy settings |
Apply all of these at once with:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa ~/.ssh/id_ed25519 ~/.ssh/authorized_keys ~/.ssh/config
chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/id_ed25519.pub ~/.ssh/known_hosts
💡 Interview tip: If you are asked why SSH authentication fails even when the key pair is correct, permissions on
~/.ssh/authorized_keyson the remote server are the second most common culprit after the private key itself.SSHdwill silently ignoreauthorized_keysif it is group-writable or world-writable.
Scenario: Nginx Returns 403 Forbidden
The Situation
You have configured Nginx to serve a static site. The config looks correct and the HTML file is present on disk, but every request returns a 403 Forbidden error:
server {
listen 80;
server_name example.com;
root /home/alice/mysite;
index index.html;
}
curl -I http://example.com/
# HTTP/1.1 403 Forbidden

Diagnosis
A 403 means Nginx can find the location but is denied permission to read it. Because the file visibly exists, the instinct is to check the file itself — but the real cause is almost always further up the directory tree.
Nginx runs as a system user (typically www-data on Debian/Ubuntu, nginx on RHEL/CentOS). To serve /home/alice/mysite/index.html, that user must be able to:
- Execute (
x) every directory in the path —/home/,/home/alice/,/home/alice/mysite/ - Read (
r) the file itself —index.html
Failing either check at any level produces a 403. The file’s own permissions are fine to check, but the home directory is the most common culprit.
Check what Nginx is actually blocked on by inspecting the error log:
tail -f /var/log/nginx/error.log
You will see a line like:
[error] 1234#0: *1 open() "/home/alice/mysite/index.html" failed (13: Permission denied)
Now trace the directory permissions from the root down:
ls -la /home/
ls -la /home/alice/
ls -la /home/alice/mysite/
ls -la /home/alice/mysite/index.html
A typical home directory looks like this by default:
drwx------ alice alice /home/alice/
700 — only the owner can enter. www-data hits a wall at this directory and never reaches mysite/.
Solution
You need to grant the execute bit on each directory in the path to the user Nginx runs as. There are two approaches:
Option 1 — Add world-execute to the blocking directory (simplest)
chmod o+x /home/alice
This allows any user — including www-data — to traverse into /home/alice. It does not grant read access (they cannot ls the home directory), only traversal.
chmod o+x /home/alice/mysite
chmod o+r /home/alice/mysite/index.html # if the file itself is also restricted
Option 2 — Add Nginx’s user to the owner’s group (more controlled)
# Add www-data to alice's group
usermod -aG alice www-data
# Grant group execute on the directories
chmod g+x /home/alice
chmod g+x /home/alice/mysite
# Grant group read on the files
chmod -R g+r /home/alice/mysite
Changes to group membership take effect on the next login/session for that user. Reload Nginx after any permission or group change:
systemctl reload nginx
Option 3 — Move the web root outside the home directory (best practice)
Serving files from inside a user’s home directory is the root cause of this class of problem. The standard solution is to host web content under a dedicated directory:
sudo mkdir -p /var/www/mysite
sudo cp -r /home/alice/mysite/* /var/www/mysite/
sudo chown -R www-data:www-data /var/www/mysite
sudo chmod -R 755 /var/www/mysite
Then update the Nginx config:
root /var/www/mysite;
Why Execute on Directories Matters
The read (r) and execute (x) bits mean very different things on directories:
| Bit | On a directory |
|---|---|
r | List the contents of the directory (ls) |
x | Traverse into the directory — required to access anything inside it |
A process that lacks x on a directory cannot open, stat, or read any file within it, even if it knows the exact filename. This is why a 403 can occur even when the file itself has world-read permission: the kernel rejects the path traversal before it ever reaches the file.
Reference: Standard Nginx Web Root Permissions
| Path | Owner | Permission | Reason |
|---|---|---|---|
/var/www/ | root:root | 755 | Base directory, traversable by all |
/var/www/mysite/ | www-data:www-data | 755 | Nginx user owns the web root |
/var/www/mysite/*.html | www-data:www-data | 644 | Nginx reads files; no execute needed |
/var/www/mysite/uploads/ | www-data:www-data | 755 | If Nginx writes here, owner must match |
💡 Interview tip: When debugging a 403, always check the Nginx error log first — it names the exact path and errno. Then walk the directory tree from
/down to the file checking for missingxbits on each directory, not just the file itself.
Scenario: Docker Bind Mount — Permission Denied Writing /app/data
The Situation
You have a Dockerized application that writes output files to /app/data. You use a bind mount so the files are accessible on the host:
docker run -v /home/alice/data:/app/data myapp
The container starts, but as soon as the application tries to write a file it crashes with:
PermissionError: [Errno 13] Permission denied: '/app/data/output.csv'
Or, from inside the container:
docker exec -it myapp_container touch /app/data/test
# touch: cannot touch '/app/data/test': Permission denied
The directory /home/alice/data clearly exists on the host and Alice can write to it.

Diagnosis
The key insight is that bind mounts expose the host filesystem directly into the container with no UID translation. The kernel enforces the same ownership and permission rules inside the container as it does on the host — the only thing that changes is the path.
Check what user the container process is running as:
docker exec -it myapp_container id
# uid=1001(appuser) gid=1001(appuser)
Now check who owns the host directory:
ls -la /home/alice/
# drwxr-xr-x alice alice data/
# (alice has uid=1000)
The container process runs as UID 1001 (appuser). The host directory is owned by UID 1000 (alice) with permissions 755 — group and other can only read and traverse, not write. UID 1001 falls into the “other” class and has no write permission.
This is the core mismatch:
Host directory owner: UID 1000 (alice)
Container process: UID 1001 (appuser)
Directory permission: 755 → other = r-x → no write
You can verify this without even entering the container:
# Find out the UID the container process runs as
docker inspect myapp_container --format '{{.Config.User}}'
# Or check the Dockerfile
grep -i user Dockerfile
Solution
There are four approaches, ordered from quick fix to best practice.
Option 1 — chown the host directory to match the container UID
sudo chown -R 1001:1001 /home/alice/data
The container process (UID 1001) now owns the directory and can write freely. The trade-off: the directory on the host is owned by a UID that may not correspond to a named user on the host system.
Option 2 — Run the container as the host user
Pass --user to make the container process run as the current host user:
docker run --user $(id -u):$(id -g) -v /home/alice/data:/app/data myapp
$(id -u) expands to Alice’s UID (1000) at runtime. The container process now has the same UID as the host directory owner and can write to it. This requires the application inside the container to be able to run as an arbitrary UID (it must not rely on a named user or /etc/passwd entry).
Option 3 — Set the UID in the Dockerfile
If you control the image, align the container user’s UID with the expected host UID at build time:
FROM python:3.12-slim
# Create appuser with UID 1000 to match the host
RUN groupadd -g 1000 appuser && \
useradd -u 1000 -g appuser -s /bin/sh appuser
WORKDIR /app
COPY . .
USER appuser
# Host directory already owned by UID 1000 (alice) — no chown needed
docker run -v /home/alice/data:/app/data myapp
This is the most portable solution: the image is self-documenting about which UID it expects, and the host directory needs no special preparation beyond normal ownership.
Option 4 — Use a named volume instead of a bind mount
If the application only needs to persist data (not share it with a specific host path), use a Docker-managed named volume:
docker run -v myapp_data:/app/data myapp
Docker creates the volume and sets ownership to the container’s user automatically. There is no host UID involved. Access the data from the host via:
docker run --rm -v myapp_data:/data alpine ls /data
Why UIDs Are Just Numbers
Linux identifies file owners by UID integer, not by username. Usernames are resolved from /etc/passwd only for display. Inside a container, /etc/passwd is the container’s own file, entirely separate from the host’s.
# On the host
id alice
# uid=1000(alice) gid=1000(alice)
# Inside the container — same UID, different name
docker exec myapp_container id 1000
# uid=1000(appuser) gid=1000(appuser) ← different username, same UID
When the container process (UID 1001) tries to write to a directory owned by UID 1000, the kernel sees two different UIDs and applies the “other” permission bits — regardless of what names appear in either /etc/passwd. The container boundary is irrelevant to the kernel’s permission check; only the UID numbers matter.
Reference: Diagnosing Bind Mount Permission Errors
# 1. Find the UID the container runs as
docker inspect <container> --format '{{.Config.User}}'
docker exec <container> id
# 2. Check host directory ownership
ls -lan /host/path # -n shows numeric UIDs, bypassing name resolution
# 3. Compare — if UIDs differ, that is the problem
# owner UID != process UID → "other" bits apply
# 4. Check what the kernel sees at mount time
docker exec <container> ls -lan /app/data
💡 Interview tip: Always use
ls -ln(numeric UIDs) when debugging cross-container or cross-host permission problems. Username display hides the actual mismatch — two users namedappuseron different systems may have completely different UIDs, while two users with different names but the same UID have identical access rights.