Updated Jul 18, 2026

03 - Permissions, Users & Privileges

📋 Jump to Takeaways

🎁 You asked for a file with mode 0644, but the file that lands on disk has different permissions. Something silently changed your request between the code and the kernel. What was it, and how do you stop your program from holding more power than it needs?

Every file and every process on a Unix system has an identity and a set of permissions attached to it. This lesson is about that model: how permission bits work, why the umask rewrites the mode you pass, how to read your process's identity, and how to give up privileges you don't need.

The Unix Permission Model

Every file carries permission bits for three classes: the owning user, the owning group, and other (everyone else). Each class gets three bits: read (r), write (w), and execute (x). That's nine bits, written as an octal number like 0644 or 0755.

Go models these bits with os.FileMode, whose String() method prints the familiar ls -l form:

info, _ := os.Stat("script.sh")
fmt.Println(info.Mode())         // -rwxr-xr-x
fmt.Printf("%#o\n", info.Mode().Perm())   // 0755

Reading the octal: 0644 is rw-r--r-- (owner reads and writes, everyone else reads). 0755 is rwxr-xr-x (owner does everything, everyone else reads and runs). The first digit is often 0; the remaining three are user, group, other.

The umask

Here's what changed your 0644. Every process carries a umask: a set of permission bits that are cleared from the mode whenever a file is created. If your umask is 022, then creating a file with a requested mode of 0666 actually yields 0644, because the 022 bits (group-write and other-write) are masked off.

old := syscall.Umask(0)          // read it by setting it, returns the previous value
syscall.Umask(old)               // put it back
fmt.Printf("umask = %#o\n", old) // umask = 022

So the perm you pass to os.OpenFile or os.Create is a request, and the umask is the ceiling. That's why the file from the last lesson came out 0644 even though os.Create asks for 0666. If you truly need exact bits, create the file, then Chmod it explicitly.

Changing Permissions and Ownership

To set permissions after creation, use os.Chmod. It ignores the umask, so you get exactly what you ask for:

if err := os.Chmod("secret.key", 0600); err != nil {
    log.Fatal(err)
}
// secret.key is now rw------- regardless of umask

Ownership is changed with os.Chown(name, uid, gid). Only a privileged process (root) may give a file away to another user, so ordinary programs usually leave this alone:

err := os.Chown("data.db", 1000, 1000)
if errors.Is(err, os.ErrPermission) {
    fmt.Println("need root to change owner")
}

Use os.Lchown when the target is a symlink and you want to change the link itself rather than what it points to, the same follow-or-not distinction you saw with Stat and Lstat in File I/O Deep.

Process Identity

A running process has a user ID and group ID that decide what it's allowed to touch. Go exposes them directly:

fmt.Println(os.Getuid())    // 1000   real user ID
fmt.Println(os.Geteuid())   // 1000   effective user ID (used for permission checks)
fmt.Println(os.Getgid())    // 1000   real group ID

The real ID is who you are; the effective ID is who the kernel checks against for permissions. They usually match, but a setuid binary can run with an effective ID different from the real one. A Geteuid() of 0 means the process is running as root.

To turn a username into IDs, use the os/user package:

u, err := user.Lookup("nobody")
if err != nil {
    log.Fatal(err)
}
fmt.Println(u.Uid, u.Gid)   // 65534 65534

Privilege Dropping

The principle of least privilege says a program should hold only the power it needs, only for as long as it needs it. If a service must bind port 80 (a privileged port) it might start as root, but it should not stay root while handling untrusted network traffic. Ports below 1024 are privileged: the kernel only lets a process bind (claim and listen on) one when it runs as root.

The pattern: do the root-only work first, then drop. Set the group before the user, because once you drop the user ID you may no longer have the privilege to change the group:

// after binding the privileged port, while still root:
if err := syscall.Setgid(65534); err != nil {   // drop group first
    log.Fatal(err)
}
if err := syscall.Setuid(65534); err != nil {    // then drop user
    log.Fatal(err)
}
fmt.Println(os.Geteuid())   // 65534, no longer root

Be honest about the trade-offs here. On modern Linux the strongly preferred approach is to never run as root at all: let the init system start your service as an unprivileged user (User= in a systemd unit, covered in Writing a Daemon), or grant one narrow capability instead of full root. The init system is the first process the kernel starts and the one that launches and supervises long-running services; on most Linux machines that's systemd. Manual Setuid/Setgid is the "if you really must" path, and it has to happen early at startup before you spawn goroutines that assume a fixed identity.

File Capabilities

Root is all-or-nothing, which is exactly the problem. Linux capabilities slice root's powers into individual pieces you can grant one at a time. The classic example: a web server needs to bind a low port but nothing else root can do. Instead of running it as root, grant just CAP_NET_BIND_SERVICE to the binary:

# grant the binary permission to bind privileged ports, nothing more
sudo setcap 'cap_net_bind_service=+ep' ./myserver

Now ./myserver can listen on port 80 as an ordinary user, and a bug in it can't do anything else a root process could. Capabilities are set on the file with setcap (a shell command, not Go code), and they are the right tool when "I just need this one privileged thing" describes your situation.

Key Takeaways

  • Permissions are nine bits across user/group/other as r/w/x, written octal (0644, 0755). os.FileMode.Perm() gives the bits; its String() prints -rw-r--r--.
  • The process umask clears bits at creation time, so os.Create's requested 0666 becomes 0644 under a 022 umask. Read/set it with syscall.Umask(mask) (it returns the previous value).
  • os.Chmod(name, mode) sets exact permissions (umask does not apply); os.Chown(name, uid, gid) changes ownership but needs root; os.Lchown targets a symlink itself.
  • os.Getuid/os.Geteuid/os.Getgid report identity; the effective ID drives permission checks, and Geteuid() == 0 means root. user.Lookup(name) resolves a name to IDs.
  • Drop privileges after the root-only work: syscall.Setgid before syscall.Setuid. Better yet, never run as root (use systemd User=).
  • Linux capabilities (e.g. cap_net_bind_service via setcap) grant one slice of root's power instead of the whole thing.

🎁 Two processes on the same machine need to talk. They can't just share a variable, they don't share memory. So how does one program hand a stream of bytes to another, and what's the plumbing the shell uses when you type cat file | grep error?

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy