Skip to main content

Command Palette

Search for a command to run...

The Files Behind the Commands

Updated
16 min read

What the filesystem is really telling you, if you know where to look


Most people learn Linux by memorizing commands. Then there is the version where you start reading the files those commands are secretly reading for you. This post is about the second version.

Everything here came from live exploration of a running Linux system — not documentation, not man pages. Just opening files and thinking about what they mean. Some of what I found was expected. A lot of it wasn't.


Table of Contents

  1. /proc is not a real folder

  2. nsswitch.conf controls DNS order

  3. The routing table lives in /proc

  4. /proc/self — the mirror

  5. sysctl.d hardens the kernel

  6. passwd is public, shadow is not

  7. /dev/null and /dev/zero

  8. cgroups and resource accounting

  9. SUID bits and privilege surface

  10. Namespaces inside /proc/self/ns


1. /proc is not a real folder. It's a window into the kernel.

Path: /proc

Most people know /proc exists. Fewer realize that nothing inside it is stored on disk. Not a single byte. The entire directory tree is generated on the fly by the kernel whenever something reads it. It's a live conversation between userspace programs and the running kernel, disguised as a filesystem.

When you read /proc/meminfo, you're not reading a file that some daemon updates every second. The kernel synthesizes that response the moment your program opens it. When I looked at it on this machine:

MemTotal:        9437184 kB
MemFree:         9426048 kB
MemAvailable:    9426048 kB
Buffers:               0 kB
Cached:             5608 kB
SwapTotal:             0 kB
SwapFree:              0 kB

Notice swap is completely zero. This container has no swap configured at all — which means any memory pressure gets handled through the OOM killer, not by paging to disk. That single line tells you something meaningful about how this system is designed to behave under load.

The kernel also reports uptime through /proc/uptime. The two numbers there aren't a timestamp — they're seconds of elapsed time and seconds of CPU idle time. On this system, reading it showed roughly 39 seconds of uptime and 0 idle seconds, which makes sense for a freshly started container.

Insight: Tools like free, top, and ps are just formatting the raw data from /proc. Understanding the source means you can always go directly to it, even in stripped-down environments where those tools aren't installed.


2. /etc/nsswitch.conf decides the order DNS resolution actually happens

Path: /etc/nsswitch.conf

Most people's mental model of DNS goes like this: you type a domain name, Linux looks it up, done. What actually happens is determined by a file almost nobody talks about: /etc/nsswitch.conf.

The critical line on this system was:

hosts:  files dns

That single line controls the entire resolution pipeline. files means check /etc/hosts first. dns means then go to the nameserver. The order is not alphabetical, it's not priority-based in some hidden config — it's exactly the left-to-right order you see here.

This is why editing /etc/hosts can override DNS for any domain. It's not a hack or an undocumented trick. It works because nsswitch.conf puts files before dns, so the system checks your hosts file first and stops there if it finds a match.

The /etc/resolv.conf on this system had just one line:

nameserver 8.8.8.8

No search domains, no fallback nameservers. Google's public DNS, nothing else. If 8.8.8.8 is unreachable, DNS fails entirely — there is no second option.

Insight: On modern Ubuntu systems with systemd-resolved, /etc/resolv.conf is often a symlink to a generated file, not a real config. Editing it directly does nothing permanent. You have to understand what's generating it — which is one reason debugging DNS issues on Linux feels harder than it should be.


3. The routing table is readable as a plain text file, but the values are hex-encoded and backwards

Path: /proc/net/route

You can see the kernel's routing table without any tools. It's at /proc/net/route. The catch is that the IP addresses are stored as little-endian hexadecimal, so they look nothing like IPs at first glance:

Iface          Destination  Gateway   Flags  Mask
1e2297f6f2-v   08000415     00000000  0001   7FFFFFFF
1e2297f6f2-v   00000000     09000415  0003   00000000

Once you decode those hex values by reversing the byte order and converting each byte to decimal, the second row becomes: destination 0.0.0.0, gateway 21.4.0.9. That's the default route — the "send everything here if you don't know where else it goes" rule.

Interface        Destination    Gateway     Flags  Mask
1e2297f6f2-v     21.4.0.8       0.0.0.0     1      255.255.255.127
1e2297f6f2-v     0.0.0.0        21.4.0.9    3      0.0.0.0   ← default route

The flags column uses bitmasks. Flag 1 means the route is up. Flag 3 (which is 1 + 2) means the route is up and uses a gateway. So you can tell immediately that the second entry is the default gateway just from its flags.

Insight: The interface name here is a long hash-like string rather than eth0 or enp3s0. That tells you this is inside a container runtime that generates synthetic interface names. The routing table reveals the container's network topology without you needing any network tools installed.


4. /proc/self is a process looking at itself in a mirror

Path: /proc/self

Every running process on Linux has an entry in /proc named after its PID. But there's a special one: /proc/self. It's a symlink that always resolves to the /proc entry of whichever process is currently reading it. It's a runtime self-referential directory.

The most revealing file inside is /proc/self/maps, which shows the exact memory layout of a running process — every shared library, every mapped region, with permissions:

7ea8ae428000-7ea8ae5b0000  r-xp  ...  /usr/lib/x86_64-linux-gnu/libc.so.6
7ea8ae5b0000-7ea8ae5ff000  r--p  ...  /usr/lib/x86_64-linux-gnu/libc.so.6
7ea8ae603000-7ea8ae605000  rw-p  ...  /usr/lib/x86_64-linux-gnu/libc.so.6

Notice how the same libc.so.6 appears three times. One region is r-xp (readable and executable — that's the actual code), one is r--p (read-only data), and one is rw-p (writable data). The OS doesn't just load a library as one blob. It maps different sections with different permissions, which is a real security measure: the code section can't be written to, so an attacker can't modify it in memory without triggering an access violation.

The /proc/self/fd directory is equally revealing. It shows every open file descriptor as symlinks pointing to what they actually are:

0 -> pipe:[14]   # stdin
1 -> pipe:[15]   # stdout
2 -> pipe:[16]   # stderr

All three standard streams are pipes, not a terminal. That tells you immediately that this process is running non-interactively, with its I/O piped elsewhere.

Insight: Security tools use /proc/[pid]/maps to detect injected code in running processes. If a library appears mapped into memory that shouldn't be there, that's a red flag. Forensics analysts look here before anywhere else when investigating a compromised process.


5. /etc/sysctl.d contains Ubuntu's actual kernel hardening philosophy

Path: /etc/sysctl.d

The comments inside these files are surprisingly honest. Each file configures a different aspect of kernel behavior at boot, and the Ubuntu maintainers left detailed explanations for every setting. Reading through them is genuinely educational.

The most interesting one was 10-ptrace.conf. PTRACE is the system call that debuggers use — it lets one process inspect and control another. Without restrictions, any process you own can attach to any other process you own and read its memory. That means an attacker who compromises one process can extract SSH keys, GPG agent secrets, or anything else living in memory of your other processes.

# /etc/sysctl.d/10-ptrace.conf
kernel.yama.ptrace_scope = 1

# 0 = any process can ptrace any other (same user)
# 1 = only direct children can be ptraced
# 2 = only processes with CAP_SYS_PTRACE

Setting it to 1 means gdb ./myprogram still works (direct child), but gdb --pid 1234 to attach to an already-running process does not. That one setting significantly reduces the blast radius of a process compromise.

Then there's 10-zeropage.conf, which sets vm.mmap_min_addr = 65536. This prevents any process from mapping memory at very low addresses near zero. It exists because a category of kernel bugs called NULL pointer dereferences can be exploited if an attacker can place their payload at address zero. Blocking that mapping removes the exploitation primitive entirely.

The IPv6 privacy config was also worth noting:

# /etc/sysctl.d/10-ipv6-privacy.conf
net.ipv6.conf.all.use_tempaddr = 2

# 2 = prefer temporary random addresses over MAC-derived ones

Without this, your IPv6 address encodes your network interface's MAC address, which means it's stable and trackable across networks. With it set to 2, the system generates random addresses that rotate over time. Ubuntu turns this on by default. Most people have no idea it's happening.

Insight: These files are where the gap between "default Ubuntu" and "hardened Ubuntu" starts to close. Knowing they exist means you can audit them, understand the reasoning, and extend them. A single wrong value — like setting ptrace_scope back to 0 — can undo meaningful security work.


6. /etc/passwd is world-readable by design, and that's the point

Path: /etc/passwd and /etc/shadow

The first time you see that /etc/passwd can be read by any user on the system, it feels like a mistake. It isn't. The file doesn't contain passwords — it contains account metadata. The username, user ID, group ID, home directory, and login shell. Programs need this information constantly. When you run ls -l and see a username instead of a number, that lookup comes from /etc/passwd. If it weren't world-readable, basic system functionality would break for unprivileged users.

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
claude:x:1001:1001::/home/claude:/bin/bash

That x in the password field is the clue. It means "the password hash is stored elsewhere." That elsewhere is /etc/shadow. And the permissions are completely different:

-rw-r--r--  root root    /etc/passwd    # readable by everyone
-rw-r-----  root shadow  /etc/shadow    # readable only by root and shadow group

The split exists because of that exact tradeoff: some information needs to be public, some needs to be locked down. Putting everything in one file either made passwords readable to all users (the old approach, genuinely insecure) or made account metadata inaccessible to regular programs.

Also worth noting: /etc/login.defs sets PASS_MAX_DAYS = 99999 on this system, which effectively means passwords never expire. For a server hardened in other ways, that might be fine. For a multi-user system where people set weak passwords and forget about them, it's worth revisiting.

Security note: The daemon, bin, sys, and similar accounts all have /usr/sbin/nologin as their shell. This prevents anyone from logging in as those service accounts even if they somehow obtain credentials. It's a quiet but effective hardening measure.


7. /dev/null and /dev/zero are not files — they're kernel character devices

Path: /dev/null, /dev/zero, /dev/urandom

The /dev directory contains things that look like files but are actually interfaces to kernel drivers. The letter at the start of a ls -l listing tells you what kind: c for character device, b for block device, l for symlink.

crw-rw-rw-  root root  1,3   /dev/null
crw-rw-rw-  root root  1,5   /dev/zero
crw-rw-rw-  root root  1,8   /dev/random
crw-rw-rw-  root root  1,9   /dev/urandom

Those numbers — 1, 3 and 1, 9 — are the major and minor device numbers. Major 1 is the kernel's "memory" driver. Each minor number identifies a specific behavior: 3 is null (discard writes, return EOF on reads), 5 is zero (return infinite zero bytes), 8 is random (blocking, waits for entropy), 9 is urandom (non-blocking, never waits).

/dev/null returning nothing when you read it and silently discarding everything you write is exactly why it became a shorthand for "the void" in shell scripting. It doesn't store data, it doesn't buffer, it has no size. Writing 10GB to it completes immediately.

The /dev/stdin, /dev/stdout, and /dev/stderr on this system are symlinks to /proc/self/fd/0, /proc/self/fd/1, and /proc/self/fd/2 respectively. So even the standard streams are just views into the process's file descriptor table, via /proc.

Insight: The distinction between /dev/random and /dev/urandom caused real security problems historically. Applications needing cryptographic randomness but accidentally using /dev/urandom in low-entropy environments (like freshly booted VMs) could get predictable output. On modern kernels (5.6+), the distinction has largely been erased — both behave identically once the system is initialized.


8. cgroups in /sys/fs/cgroup expose exactly how container resource limits work

Path: /sys/fs/cgroup, /proc/self/cgroup

Control groups (cgroups) are how Linux enforces resource limits on processes. Docker, Kubernetes, systemd — they all use cgroups under the hood. You can see the cgroup membership of any process by reading /proc/self/cgroup:

7:pids:/container_01MwpS...
6:memory:/container_01MwpS.../process_api/a6ab32b2...
5:job:/container_01MwpS...
1:cpu:/container_01MwpS...

Each line is a different resource controller. This process is tracked by four of them: PIDs (max number of processes), memory, jobs, and CPU. The path after the colon identifies which cgroup hierarchy the process belongs to.

What's more interesting is reading the actual limits and usage directly from /sys/fs/cgroup/memory/:

memory.limit_in_bytes   →  9223372036854775807   (unlimited — max int64)
memory.usage_in_bytes   →  11309056              (~10.8 MB currently used)

That max int64 limit means no memory cap is set at this cgroup level. The actual enforced limit is set by the container runtime at a higher level — the process startup command showed --memory-limit-bytes 4294967296, which is exactly 4GB. The limit lives in the parent cgroup, not this one.

Insight: This is how container escape detection works. If a process reads its own cgroup path and it contains a container ID, it knows it's running inside a container. Some applications use this self-awareness to adjust their behavior. And some escape techniques involve finding misconfigured cgroup permissions that let a process write to the cgroup filesystem and inject into the host namespace.


9. SUID binaries are the permission model's deliberate exception — and its most exploited feature

Path: /usr/bin (SUID binaries)

Normal Unix permissions work like this: when you run a program, it runs with your UID and has your permissions. SUID (Set User ID) flips that. When you execute an SUID binary, it runs with the permissions of the file's owner, not yours. If root owns it, it runs as root, regardless of who launched it.

This is not a bug. It's how passwd works. You, a regular user, can change your own password — which requires writing to /etc/shadow, a file only root can write. The passwd binary is SUID root. When you run it, it temporarily runs as root, performs its controlled writes to shadow, and exits. The SUID bit is the intentional, tightly scoped escalation.

-rwsr-xr-x  root  /usr/bin/passwd     # change passwords
-rwsr-xr-x  root  /usr/bin/su         # switch users
-rwsr-xr-x  root  /usr/bin/mount      # mount filesystems
-rwsr-xr-x  root  /usr/bin/newgrp     # switch group
-rwsr-xr-x  root  /usr/bin/chage      # change password aging

The lowercase s where the execute bit would normally be is how the permission string shows SUID. An uppercase S means SUID is set but the file isn't actually executable — which is typically a mistake.

Security note: Every SUID binary on a system is a potential privilege escalation vector. If any of these programs have a vulnerability — a buffer overflow, a path traversal, an argument injection — an attacker can exploit it to get root. This is why security audits specifically scan for unexpected SUID binaries, and why any SUID binary you didn't put there yourself is worth investigating immediately.


10. /proc/self/ns reveals which Linux namespaces this process lives in

Path: /proc/self/ns

Linux namespaces are the fundamental technology behind containers. Each namespace type isolates a different aspect of the system: network, process IDs, mount points, users, hostname, and IPC. The kernel tracks which namespace each process belongs to, and you can see it directly:

These symlinks point to namespace inodes, not real files. Two processes in the same namespace will see the same inode number. Two processes in different namespaces will see different numbers. That's exactly how tools like nsenter work — they open the target namespace file and call setns() to join it, effectively teleporting into another process's view of the world.

The user namespace having a high inode number (1721) while net has inode 1 suggests the user namespace was created more recently than the network namespace. The network namespace was established early in the container lifecycle; the user namespace was configured later as part of the runtime setup.

The limits file at /proc/self/limits rounded this out — showing a hard cap of 20,000 open file descriptors (not the typical 65,536 on bare metal), and maximum pending signals set to 0, meaning signals can't be queued for this process at all.

Insight: Container runtimes like Docker and containerd work by calling clone() with namespace flags instead of fork(). The child process lands in fresh namespaces, sees a different filesystem root via bind mounts, has resources tracked by cgroups, and has its capabilities restricted. None of this requires virtualization. The container is the same kernel, just a different view — and all of that view is visible through /proc if you know what you're reading.


What this all adds up to

The filesystem isn't just where your files live. It's an interface. The kernel exposes its internal state as readable files in /proc and /sys. Configuration in /etc isn't just settings — it's the recorded decisions about security tradeoffs, resolution order, permission boundaries, and runtime behavior. Even /dev is less a folder and more a registry of hardware and virtual interfaces.

What keeps standing out is how much intent is embedded in these files. The comments in /etc/sysctl.d/10-ptrace.conf explain exactly why the restriction exists and who it affects. The split between /etc/passwd and /etc/shadow is a documented architectural decision from decades ago that still shapes how every Linux system handles authentication today.

The best way to understand a Linux system isn't to run commands and read the formatted output. It's to read the files the commands are reading, and think about why they're structured the way they are.


Hey! Thanks for reaching the end and reading through this investigation until the very last finding. I hope this walkthrough helped you get a much better understanding of the depth hidden behind your everyday Linux commands.

Share your insights! I’d love to hear what you have discovered in your own system exploration.