DEV Community

Cover image for Linux from Zero #4 — Users, Groups and Permissions for Platform Engineers
Roger Oliveira
Roger Oliveira

Posted on

Linux from Zero #4 — Users, Groups and Permissions for Platform Engineers

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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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)

Collapse
 
topstar_ai profile image
Luis Cruz

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.