ou deploy a pod. It crashloops. You check the logs and find the most generic error in computing:
Permission denied
Then comes the ritual: someone suggests chmod 777, someone suggests running as root, the pod comes up, everyone moves on. It worked. But nobody knows why — and three months later the same error shows up somewhere else.
Linux permissions are among the easiest topics to explain and the most poorly learned in our field. Not because they're hard, but because almost everyone memorized 755 and 644 without ever understanding where those numbers come from.
Let's fix that.
- Linux doesn't know your name — it knows your number
This is the idea that unlocks everything else.
When ls -l shows roger, that's decoration. The kernel has no idea what "roger" is. It knows a number: the UID. Same for groups — the GID.
bash
$ id
uid=1000(roger) gid=1000(roger) groups=1000(roger),27(sudo),999(docker)
The number-to-name mapping lives in plain text files:
bash
$ grep roger /etc/passwd
roger❌1000:1000:Roger Oliveira:/home/roger:/bin/bash
name:password:UID:GID:comment:home:shell
File Holds
/etc/passwd user, UID, primary GID, home, shell
/etc/group groups and their members
/etc/shadow password hashes (root-only)
Why this matters for platform work: a container has its own /etc/passwd, but shares the kernel — and therefore the UIDs — with the host. If the container process runs as UID 1000 and the file on the volume belongs to UID 2000, no username on earth will fix it. It's number against number.
That's why chmod 777 "works": it gives up on the check instead of resolving the mismatch.
- The three digits, finally explained
Every file has one owner (a UID) and one group (a GID). Permissions are defined for three classes:
Class Who
user (u) the file owner
group (g) members of the file's group
others (o) everyone else
And three permissions per class:
Letter Value On a file On a directory
r 4 read contents list names inside
w 2 modify contents create/delete/rename inside
x 1 execute traverse
-rw-r--r-- → 6 4 4 → 644
│ │ │ └── others: r--
│ │ └───── group: r--
│ └──────── user: rw-
└────────── type (- file, d directory, l link)
No memorization needed: add 4+2+1 within each block of three.
Common patterns:
644 — owner writes, everyone reads. Config files.
600 — owner only. Private keys and secrets.
755 — owner writes, everyone executes. Binaries and directories.
777 — everyone does everything. This isn't a permission, it's the absence of one.
The directory x trap
This one catches good engineers. On a directory, x doesn't mean "execute" — it means you may pass through.
A directory with r but no x lets you see filenames but open none of them. A directory with x but no r lets you open a file if you know its exact name, but not list the contents.
And it must hold across the entire path. If /data has no x for you, it doesn't matter that /data/app/config.yml is 777 — you'll never reach it.
The command that solves this in five seconds:
bash
$ namei -l /data/app/config.yml
f: /data/app/config.yml
drwxr-xr-x root root /
drwxr-x--- root infra data ← here
drwxr-xr-x app app app
-rw-r--r-- app app config.yml
Found it. data only allows entry for root and members of infra.
namei -l should be the first command of any permission investigation. It walks the whole path, layer by layer.
- Changing ownership and mode bash chmod 640 secret.env # octal chmod u+x deploy.sh # symbolic chmod g-w app.conf chmod o= secret.env # zero out "others"
chown app:platform config.yml
chown -R app:platform /opt/app
chgrp platform config.yml
Recursion without collateral damage — capital X applies execute only to directories:
bash
chmod -R u=rwX,go=rX /opt/app
chmod -R 755 would mark every data file as executable. Don't.
- The three bits nobody explains Bit Octal Shows as Effect setuid 4000 -rwsr-xr-x runs with the file owner's privileges setgid 2000 drwxr-sr-x new files inherit the directory's group sticky 1000 drwxrwxrwt only the file's owner can delete it
setuid is how passwd lets an ordinary user modify a root-owned file. It's also the first thing an attacker looks for:
bash
find / -perm -4000 -type f 2>/dev/null
setgid on a directory is genuinely useful — it's the right way to build a shared directory:
bash
chgrp platform /srv/shared
chmod 2775 /srv/shared
Every file created there inherits the team's group. This is exactly what Kubernetes fsGroup does under the hood.
Sticky bit is why /tmp is 1777 and not 777. Without it, any user could delete any other process's temp files.
- umask — why your file is born 644
umask is a mask that removes bits from the creation default.
bash
$ umask
0022
umask File (666−) Directory (777−)
022 644 755
027 640 750
077 600 700
If a pipeline-generated file comes out with the wrong mode, the cause is almost always the umask of the process that created it.
- Where this hits Kubernetes yaml securityContext: runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 runAsNonRoot: true readOnlyRootFilesystem: true
Now that you understand the model:
runAsUser sets the process UID. It does not need to exist in the image's /etc/passwd — the kernel only wants the number. That's why you see whoami: cannot find name for user ID 1000 in a container with no matching user: nothing is broken, there's just no number-to-name translation.
fsGroup makes the kubelet apply that GID to mounted volumes and turn on setgid on the directory — exactly the mechanism from section 4. Without it, a volume owned by a different UID simply isn't writable.
runAsNonRoot: true makes the kubelet refuse to start a container that would run as UID 0. Cheap and effective.
And the classic trap: fsGroup does not work on NFS. NFS resolves permissions server-side, not client-side. If the export maps to a different UID, no securityContext will save you — the fix belongs on the NFS side.
- The investigation checklist
Next time you hit Permission denied, follow this order instead of guessing:
bash
id # 1. who am I really (UID/GID, not name)
ls -ln file # 2. who owns it — -n shows numbers, not names
namei -l /full/path # 3. does the whole path allow traversal?
stat file # 4. detailed mode, including special bits
getfacl file # 5. is an ACL overriding the classic model?
Use ls -ln (with n) instead of ls -l when debugging containers. Seeing the number is what reveals the mismatch — the name hides it.
If all five come back clean and it still denies, look one layer up — SELinux or AppArmor:
bash
getenforce
ausearch -m avc -ts recent
Exercise
Five minutes, on a throwaway box or container:
bash
mkdir -p /tmp/lab && cd /tmp/lab
touch public.txt private.txt
chmod 644 public.txt
chmod 600 private.txt
mkdir shared && chmod 2775 shared
ls -ln
stat shared | head -5
umask
Then answer without looking:
Why can't a user in the same group read private.txt?
What does the 2 in 2775 change about shared?
With umask 0022, what mode does a new directory get?
If all three come easily, you understand the model — you didn't memorize the numbers.
This is part #4 of the Linux from Zero track — free, open material for people building a solid foundation before taking on platform work and Kubernetes.
Full repository: kubernetes-do-zero-ptbr
Top comments (1)
I appreciate your clear breakdown of Linux permissions; it's a critical area where many developers struggle, often leading to those frustrating "permission denied" errors. Your point about the UID/GID mapping being crucial for containerized environments is particularly insightful, as it highlights the need for a solid understanding of these concepts in today’s infrastructure. I’d suggest incorporating a few practical examples of common permission pitfalls in real-world scenarios, which can further solidify understanding. If you’re looking for help with any part of this project or further exploration of these concepts, I’d be glad to discuss a paid collaboration.