Privileges, Capabilities, and Least Privilege
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=trueGo 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 setupTest 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
- Run a server as non-root; bind
:8080vs:80; document the error.
- Read your systemd unit / K8s securityContext for one service.
- Add a startup check that warns on world-writable config dirs.