Privileges, Capabilities, and Least Privilege

Updated

September 8, 2026

Privileges, Capabilities, and Least Privilege

Overview

Systems software often needs a little privilege (bind port 80, open raw sockets, read another user’s files)—not a permanent root shell. This chapter is a minimal, Go-oriented map of least privilege: run as non-root, drop what you can, and prefer orchestrator policy over home-grown setuid.

Not a full Linux security course—pair with 17 Security hardening.

Prefer external policy

Mechanism Who configures Example
Kubernetes securityContext Platform runAsNonRoot, drop caps
systemd User= / AmbientCapabilities= Operator DynamicUser, CAP_NET_BIND_SERVICE
Container rootless Runtime podman/docker user ns

Your Go code should assume it might be non-root and fail with a clear error when a needed privilege is missing.

Check identity early

if os.Geteuid() == 0 {
    slog.Warn("running as root — prefer dedicated user")
}
ln, err := net.Listen("tcp", ":80")
if err != nil {
    return fmt.Errorf("listen :80 (need privilege or CAP_NET_BIND_SERVICE): %w", err)
}

Bind low ports without full root

Linux capabilities split root power. CAP_NET_BIND_SERVICE allows bind <1024 without full UID 0.

Typical systemd:

[Service]
User=myapp
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

Go app still just calls Listen—no special code.

setuid binaries (avoid if possible)

Historically, setuid-root helpers did privileged ops then dropped. Risks are high (TOCTOU, env attacks). Prefer:

  • systemd socket activation (privileged unit hands over FD)
  • privileged sidecar
  • CAP_* ambient on the service user

If you must touch UIDs:

// conceptual — platform-specific, error-prone
// unix.Setuid, Setgid after privileged setup

Test extensively; do not invent setuid stacks casually.

File permissions as privilege

config dir  0750 root:myapp
config file 0640 root:myapp
data dir    0700 myapp:myapp
socket      0660 myapp:myapp

Go: create with tight modes (chapter 144 umask + chmod).

Secrets

Prefer Avoid
Files 0600 + dedicated user World-readable env in ps dump discussions
Runtime mounts (K8s secrets) Baking secrets into images
Short-lived tokens Long-lived root API keys in config

Minimal pattern: refuse unsafe start

func requireSecurePaths(cfgDir string) error {
    st, err := os.Stat(cfgDir)
    if err != nil {
        return err
    }
    if st.Mode().Perm()&0o077 != 0 {
        return fmt.Errorf("%s mode %#o is too open", cfgDir, st.Mode().Perm())
    }
    return nil
}

Optional hard fail in production profiles.

Rules of thumb

Do Don’t
Run non-root by default Require root “for convenience”
Use capabilities/socket activation Custom setuid without review
Clear errors on EPERM/EACCES Silent fallback to insecure paths
Tight file modes on secrets chmod 777 to “make it work”

Try next

  1. Run a server as non-root; bind :8080 vs :80; document the error.
  2. Read your systemd unit / K8s securityContext for one service.
  3. Add a startup check that warns on world-writable config dirs.