Linux has had fine-grained process capabilities for over a decade. setcap, setpriv, hardened systemd units, and a one-line sysctl change each solve the low-port problem without handing your entire system to a web-facing process. Here's what they are, how they work, and why you should have been using them years ago.

If you only read one section

Go (or any compiled binary) on a systemd hostsetcap cap_net_bind_service=+ep, baked into your Makefile. Python / Node / Rubysetpriv, or generate a hardened unit with mkunit. Anything systemd-managedmkunit writes the unit file for you with hardening defaults already in place. Single-purpose host or containersysctl net.ipv4.ip_unprivileged_port_start=80 and stop pretending the privileged-port concept is protecting anything. Don't reach forsudo, or authbind on new builds. The rest of this article is the why.

The Problem Nobody Talks About

Ports below 1024 are privileged on Linux. That's a design decision inherited from the 1980s, when multi-user Unix systems needed to guarantee that only trusted daemons could listen on well-known ports. It made sense when your PDP-11 had twelve users and three services. (This is a Linux article. macOS and FreeBSD have their own capability and privilege models — some overlapping, some not. Everything here assumes a Linux kernel.)

It makes no sense in 2026, when a single Go binary serving an API has exactly one job and needs exactly one thing from the kernel: permission to bind to port 443. Instead of asking for that one thing, the default playbook in tutorials, blog posts, and Stack Overflow answers is to reach for sudo.

# Don't do this
$ sudo ./myserver --port 443

This works. It also runs your entire application as root — the user with unrestricted access to every file, every process, every device, and every kernel interface on the machine. A web-facing binary, typically parsing untrusted input from the open internet, now has the keys to the kingdom. A single exploit — a dependency vulnerability, a buffer overrun, a path traversal — doesn't just compromise your application. It compromises the host.

The Blast Radius

Running as root means an attacker who compromises your process can: read /etc/shadow, install kernel modules, pivot to other services, exfiltrate anything on the filesystem, and persist across reboots. All because you wanted port 443.

The Linux kernel solved this problem a long time ago with capabilities — a system for decomposing root's monolithic power into discrete, grantable permissions. The specific capability you need to bind a port below 1024 is CAP_NET_BIND_SERVICE. One capability. One permission. No root.

There are several mechanisms that grant it cleanly, plus one that sidesteps the privileged-port concept entirely. All of them have been available for years. None of them require root at runtime. We'll walk through each — and where there's a tool that already bakes the right defaults in, we'll point at it rather than rewrite it from scratch. Let's start with the simplest.

Option 1: setcap — Stamp the Binary

setcap writes a capability directly into the extended attributes of an executable file. Once stamped, any user who runs that binary gets the specified capability for that process and nothing more. For a compiled language like Go, this is the shortest path from problem to solution:

# Build your server
$ go build -o myserver ./cmd/server

# Grant only the bind-low-ports capability
$ sudo setcap cap_net_bind_service=+ep ./myserver

# Run as a normal user — port 443 works
$ ./myserver --addr :443

Three commands. The binary runs as your regular user with a single additional kernel permission. If it's compromised, the attacker has your user's permissions, not root's. The blast radius drops from "entire system" to "one user account."

This works because Go's single-binary model means the executable is your program. There's no interpreter, no runtime indirection, no shared binary between your application and every other application on the system. setcap targets exactly the file you built, and the capability applies to exactly the process that runs it. The same is true of Rust, C, and anything else that compiles to a native executable.

The Rebuild Gotcha

There's an operational detail that will catch you exactly once if nobody warns you: setcap stores capabilities in the filesystem's extended attributes. When you go build again, the compiler writes a new file. The extended attributes are gone. Your next deploy fails to bind port 443 and you spend ten minutes remembering why.

The fix is to make it part of your build process, not a manual step you run once and forget.

.PHONY: build deploy

build:
	go build -o bin/myserver ./cmd/server

deploy: build
	sudo setcap cap_net_bind_service=+ep bin/myserver
	# restart service, copy to target, etc.
# GitHub Actions
- name: Build
  run: go build -o myserver ./cmd/server

- name: Set capabilities
  run: sudo setcap cap_net_bind_service=+ep ./myserver

- name: Verify
  run: getcap ./myserver
  # should output: ./myserver cap_net_bind_service=ep

getcap is the diagnostic counterpart — use it to verify the capability is actually present before you ship. If your CI pipeline builds and deploys without a setcap step, you've got a ticking time bomb.

Interpreted Languages — Where setcap Falls Apart

Interpreted languages hit a wall here that reveals something about the abstraction boundary between your code and the operating system.

When you run python3 server.py, the kernel isn't executing server.py — it's executing /usr/bin/python3. Your script is just data the interpreter reads. The kernel doesn't know your script exists. It loaded a binary called python3, and that's the only entity it can attach capabilities to.

So setcap cap_net_bind_service=+ep server.py does nothing. The capability needs to go on the interpreter.

# Don't do this either —
# this "works" but grants the capability to EVERY Python script
$ sudo setcap cap_net_bind_service=+ep /usr/bin/python3

# Now ANY user running ANY Python script can bind low ports
$ python3 some_totally_unrelated_script.py  # has CAP_NET_BIND_SERVICE
The Interpreter Trap

Stamping the interpreter grants the capability to every script that interpreter runs, for every user on the system. You've solved one security problem by creating a broader one. This applies equally to Python, Ruby, Node, Perl — any language where your code is interpreted by a shared system binary. For interpreted languages, you need a tool that applies capabilities at the process level, not the file level.

This isn't a flaw in setcap. It's an honest reflection of how interpreted execution works. But it means we need to understand a bit more about how capabilities actually work in the kernel before reaching for the right tool.

How Linux Capabilities Actually Work

You've seen the simple case — setcap on a compiled binary — and the case where it breaks down. To understand why it breaks down, and why the next tools work differently, you need to know what the kernel is actually doing with capabilities. This isn't academic. If you don't understand the model, you'll cargo-cult flags from blog posts without knowing what they do. That's the same energy as sudo — it works but you've learned nothing, and the next problem will bite you.

Linux doesn't have a single "capabilities" flag per process. It has five capability sets, and they interact:

Permitted
The ceiling. The maximum set of capabilities this process is allowed to use. Having a capability in the permitted set doesn't mean the process is using it — it means it could activate it. Think of it as a licence you hold but haven't shown yet.
Effective
The capabilities that are actually active right now. These are the ones the kernel checks when your process makes a privileged system call. If CAP_NET_BIND_SERVICE isn't in the effective set at the moment you call bind(), you get EACCES. End of story.
Inheritable
Capabilities that can be passed to a child process across an execve() call — but only if the new binary also has the same capability in its own inheritable set. Both sides have to agree. This is how the kernel prevents a process from handing out capabilities to arbitrary binaries.
Ambient
Added in Linux 4.3 to solve a real problem: inheritable capabilities are useless for unprivileged binaries that don't have file capabilities set. Ambient capabilities survive execve() without requiring anything on the target binary. This is the set that makes setpriv work for interpreted languages — the interpreter doesn't need to be stamped.
Bounding
The hard limit. A capability removed from the bounding set can never be reacquired by this process or its children, regardless of what the other sets say. Dropping the bounding set to a single capability is how you guarantee a process can never escalate.

One more detail that matters in practice: capabilities are cleared across setuid transitions. If your process forks and execs a setuid binary, the kernel wipes the ambient and inheritable sets. This is usually what you want — you don't want to leak CAP_NET_BIND_SERVICE to an unrelated setuid program. But it means that if your application spawns helper processes via setuid binaries, those helpers won't inherit the capability. In practice, a well-designed service doesn't do this. But if you're debugging a capability that mysteriously vanishes in a child process, the setuid transition is almost certainly why.

Now the setcap flags make sense: cap_net_bind_service=+ep adds the capability to both the effective and permitted sets on the file. When the kernel loads that binary, the process gets the capability immediately — no activation step, no inheritance required. For a compiled binary, this is all you need. For an interpreter, it's too much because it applies to everything the interpreter runs. The next tool solves that.

Option 2: setpriv — Per-Process, Per-Launch

setpriv is part of util-linux, which means it's already installed on essentially every Linux system you'll ever touch. It has been since 2012. No package to add, no dependency to manage, no version to pin. It's just there, waiting for you to stop using sudo.

Where setcap modifies the binary, setpriv modifies the process. It launches a command with a specific UID, GID, and capability set, without touching the executable file at all. The capability exists for one process, for one launch, and then it's gone.

$ sudo setpriv \
    --reuid=appuser --regid=appgroup \
    --init-groups \
    --inh-caps=+net_bind_service \
    --ambient-caps=+net_bind_service \
    --bounding-set=-all,+net_bind_service \
    ./myserver --addr :443

Now that you understand the capability sets, every flag should make sense:

--reuid=appuser --regid=appgroup
Drop to a non-root user and group immediately. The process never executes a single instruction as root — setpriv transitions the UID/GID before launching the target command.
--init-groups
Initialise supplementary groups for appuser. Without this, the process might lack group memberships it needs for file access.
--inh-caps=+net_bind_service
Add CAP_NET_BIND_SERVICE to the inheritable set. Necessary for the capability to survive the execve() that launches your binary.
--ambient-caps=+net_bind_service
Add it to the ambient set. This is the critical flag for interpreted languages — ambient capabilities are granted to the new process regardless of whether the target binary has file capabilities. Without ambient, --inh-caps alone won't get the capability through to an unstamped binary: inheritable gates what ambient is allowed to carry across execve(), but it doesn't propagate on its own. Both flags are needed: inheritable makes it eligible, ambient makes it arrive.
--bounding-set=-all,+net_bind_service
Drop every capability from the bounding set except net_bind_service. The insurance policy: even if the process is later exploited, it cannot acquire any other capability. No CAP_SYS_ADMIN, no CAP_DAC_OVERRIDE, nothing. The ceiling is clamped.

Skip --ambient-caps and your unstamped binary won't receive the capability. Skip --bounding-set and you've granted a capability without limiting what else could be acquired. These aren't redundant flags doing the same thing — they're each controlling a different layer of the model. Together they compose into a process that starts with minimum privilege and cannot escalate. That's not the same thing as "it works" — it's the difference between a locked door and an open one that nobody has walked through yet.

People sometimes conflate setpriv with sudo because both appear in the same command line. The distinction is directional. sudo is designed to grant power — it elevates you to root. setpriv is designed to restrict power — it drops from root to a constrained user with a single named capability. You use sudo once, to launch setpriv, and setpriv immediately sheds everything you don't need. sudo is the airlift; setpriv is the parachute. The process never runs as root.

The Interpreter Problem, Solved

Because setpriv applies capabilities at the process level, interpreted languages work exactly the same as compiled ones:

# Python
$ sudo setpriv \
    --reuid=appuser --regid=appgroup \
    --init-groups \
    --inh-caps=+net_bind_service \
    --ambient-caps=+net_bind_service \
    --bounding-set=-all,+net_bind_service \
    python3 server.py
# Node
$ sudo setpriv \
    --reuid=appuser --regid=appgroup \
    --init-groups \
    --inh-caps=+net_bind_service \
    --ambient-caps=+net_bind_service \
    --bounding-set=-all,+net_bind_service \
    node server.js

The interpreter isn't permanently stamped. Another user running another Python script gets no special capabilities. A rogue dependency in a completely different project can't bind port 53 and start impersonating your DNS server. The permission boundary is the process, not the binary.

Option 3: systemd — Declare It, Don't Script It

If your service runs under systemd — and on any modern Linux server, it almost certainly does — capabilities can be declared directly in the unit file. No setcap, no setpriv wrapper, no shell script glue. The init system handles the capability assignment during process launch, which means the interpreter-vs-binary distinction doesn't apply. systemd doesn't care what your binary is — it sets up the process environment before your code executes a single instruction.

Here's what a properly hardened unit file looks like. Don't worry about typing it from memory — we'll get to a tool that writes this for you in a moment. But you should be able to read it, because every directive corresponds to a specific attack surface you're closing:

# /etc/systemd/system/myserver.service
[Unit]
Description=My Application Server
After=network.target

[Service]
Type=simple
ExecStart=/opt/myserver/myserver --addr :443
User=appuser
Group=appgroup
Restart=on-failure

# Capabilities — one line each
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

# Hardening — same principle, extended further
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ReadWritePaths=/var/lib/myserver

[Install]
WantedBy=multi-user.target

The capability block is three lines. But the hardening directives underneath aren't a bonus or an afterthought — they're the same principle applied consistently. Each one removes an attack surface.

ProtectSystem=strict makes the entire filesystem read-only except explicitly listed paths. Your compromised process can't write to /usr/bin, can't drop a backdoor into /etc/cron.d, can't modify its own binary. PrivateTmp gives the service its own /tmp namespace so it can't read other services' temporary files — a classic data exfiltration vector. ProtectKernelModules blocks the process from loading kernel modules even if it somehow acquired CAP_SYS_MODULE. And NoNewPrivileges=true ensures the process can never gain additional capabilities after startup — no setuid binaries, no file capabilities on child processes, nothing.

If you're already using systemd and you're not setting these directives, you're leaving doors open that cost nothing to close. The service file is where your security posture is declared. Every directive you leave at the default is an implicit statement: "I'm fine with unrestricted." Make that statement on purpose if you're going to make it at all.

Stop Hand-Writing Unit Files: Use mkunit

Now for the practical version. Hand-writing a unit file like the one above is fine the first time you do it, when you're learning what each directive means. After that, it's a copy-paste exercise — and copy-paste is exactly where hardening directives quietly get dropped between deployments. Someone forgets NoNewPrivileges. Someone leaves ProtectSystem at default. The directives that matter most are the ones easiest to forget, because nothing breaks when they're missing.

mkunit is a CLI that generates systemd unit files with sensible hardening defaults baked in. It bakes the recommendations from this section into the tool, so the unit file it produces is already sandboxed properly — capabilities scoped, kernel surface area protected, filesystem locked down — without you having to remember every directive name. You describe the service in flags; it writes the unit file:

$ cargo install mkunit

$ mkunit service myserver \
    --exec "/opt/myserver/myserver --addr :443" \
    --user appuser \
    --restart on-failure \
    --install

This is the systemd equivalent of what setcap is for binaries: the right defaults, applied automatically, with the option to override. It supports services, timers, sockets, paths, mounts, and targets — which means the same hardening discipline extends to scheduled jobs and socket-activated services, not just long-running daemons. If you're declaring more than one or two services by hand, you're spending time reinventing what mkunit already gets right.

If you want to verify what mkunit (or any other unit file) actually produced, systemd will tell you exactly what's exposed:

$ systemd-analyze security myserver.service
  NAME                                  DESCRIPTION                    EXPOSURE
✗ PrivateNetwork=                       Service has access to the      0.5
                                        host's network
✓ PrivateTmp=                           Service uses private /tmp      0.1
✗ ProtectClock=                         Service may write to the       0.2
                                        hardware clock
  ...
→ Overall exposure level for myserver.service: 4.2 MEDIUM

It scores your unit file on a 0–10 scale and flags every hardening directive you haven't set. This is the systemd equivalent of getcap and /proc/self/status — the verification step that turns "I think this is hardened" into "I can see exactly what's exposed." Run it after generating your unit file. Run it again after every change.

Works for interpreters too

ExecStart=/usr/bin/python3 /opt/myapp/server.py — same directives, same result. systemd handles the capability injection at the process level, so the interpreter isn't stamped and the capability is scoped to this service alone. mkunit handles this transparently — point --exec at the interpreter and your script, and the hardening applies just the same.

Option 4: authbind — The One You'll See in the Wild

If you've been around long enough, you'll have seen authbind in deployment scripts and old wiki pages. It pre-dates the ambient capabilities mechanism and solves the same problem through a different route: it interposes on the bind() system call via LD_PRELOAD and checks a set of per-port permission files in /etc/authbind.

# Allow appuser to bind port 443
$ sudo touch /etc/authbind/byport/443
$ sudo chown appuser /etc/authbind/byport/443
$ sudo chmod 500 /etc/authbind/byport/443

# Run with authbind
$ authbind --deep python3 server.py

It works, and for years it was the standard answer for interpreted languages. But it has real limitations worth understanding. It uses LD_PRELOAD, which means it doesn't work with statically linked binaries — including most Go programs compiled with CGO_ENABLED=0. It intercepts libc's bind(), so any binary that makes the system call directly bypasses it entirely. And the permission model is file-based, which means per-port configuration scattered across /etc/authbind/ rather than declared in one place.

A confusing middle ground worth noting: Go with CGO_ENABLED=1 — which is the default when packages like net use the cgo DNS resolver — does produce a dynamically linked binary, and authbind will technically work with it. But building your deployment strategy around a linker flag that changes depending on which packages you import is fragile in a way that will eventually surprise you. If you're using Go, use setcap. It works regardless of how the binary is linked.

If you encounter authbind in existing infrastructure, it's doing its job and there's no urgent reason to rip it out. But for new deployments, setpriv or systemd capabilities are the better tools — they work at the kernel level rather than through library interposition, they handle static binaries, and they integrate with the rest of the Linux security model rather than side-stepping it.

Option 5: Just Remove the Restriction

Since kernel 4.11 (2017), Linux lets you change the threshold for what counts as a "privileged" port. One sysctl call:

# Lower the privileged port threshold — 80 and above are unprivileged
$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80

# Or go all the way — every port is unprivileged
$ sudo sysctl net.ipv4.ip_unprivileged_port_start=0

# Persist across reboots
$ echo "net.ipv4.ip_unprivileged_port_start=80" | \
    sudo tee /etc/sysctl.d/50-unprivileged-ports.conf
$ sudo sysctl --system

The choice between 80 and 0 is a real operational decision, not a style preference. Setting the threshold to 80 frees up HTTP (80) and HTTPS (443) — the ports your application actually cares about — while keeping SSH (22), DNS (53), SMTP (25), and other infrastructure ports privileged. On a system where you want web services to run unprivileged but still want kernel-level protection for system daemons, 80 is the right call. Setting it to 0 removes the concept entirely — appropriate when the machine runs one application and the multi-user threat model no longer applies.

Know Your Context

This is system-wide. On a multi-user system or a shared development box, lowering the port threshold means any user can impersonate any service on any port. On a dedicated application server running one service — which describes the majority of modern production deployments — that's a non-issue. The "privileged port" concept is protecting nobody because there's nobody else on the box.

The container angle makes this even more pointed. If your application runs inside a container, the port it binds to inside the container is mapped at the runtime level anyway — -p 443:8080 in Docker, hostPort in Kubernetes. The process inside the container can listen on port 8080 as a regular user, and the host maps it to 443. The privileged port restriction inside the container is protecting precisely nothing. Many container base images already set net.ipv4.ip_unprivileged_port_start=0, or run processes on high ports by default, because the abstraction makes the restriction meaningless.

If you're already containerised and you're still running your process as root inside the container "because of port 443" — stop. You're carrying a restriction that the architecture has already obsoleted.

Verify It's Working

Setting capabilities is half the job. Confirming they're active at runtime is the other half. The gap between "I configured this" and "I can prove this is running as intended" is where production incidents live.

From the Shell

Check file capabilities with getcap, and inspect a running process's capability sets via /proc:

# Verify file capabilities on a binary
$ getcap ./myserver
./myserver cap_net_bind_service=ep

# Decode the hex — 0x400 is bit 10, which is CAP_NET_BIND_SERVICE
$ capsh --decode=0000000000000400
0x0000000000000400=cap_net_bind_service

What you see in /proc/$PID/status depends on how you granted the capability, and reading those two outputs side-by-side is the clearest way to internalise the difference between the file-level and process-level approaches.

A binary stamped with setcap +ep, run by a normal user, looks like this:

# setcap path: file capabilities promoted into the process
$ grep Cap /proc/$PID/status
CapInh: 0000000000000000
CapPrm: 0000000000000400
CapEff: 0000000000000400
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

CapPrm and CapEff hold exactly the capability the file was stamped with. CapInh and CapAmb are zero — there's no propagation in play. CapBnd is the system-wide default (most bits set) because nothing has restricted it. That's fine for a single-binary deployment, but the bounding set isn't doing any defensive work for you here.

A process launched through setpriv with --bounding-set=-all,+net_bind_service looks like this:

# setpriv path: bounding set clamped to the one capability you wanted
$ grep Cap /proc/$PID/status
CapInh: 0000000000000400
CapPrm: 0000000000000400
CapEff: 0000000000000400
CapBnd: 0000000000000400
CapAmb: 0000000000000400

All five sets showing 0x400 and nothing else tells you the process has exactly one capability — the one it needs — and the clamped CapBnd means it can't ever acquire another one even if it tried. That's the target state for anything that handles untrusted input. The setcap path is operationally simpler; the setpriv path is strictly tighter. If you want both, generate a systemd unit with mkunit and set CapabilityBoundingSet to the same single capability.

From Go at Startup

Don't trust infrastructure blindly. If your application requires a capability to function, check for it at startup and fail fast with a clear error. This costs a handful of lines and saves twenty minutes of debugging a cryptic bind: permission denied in production at 2 AM.

package main

import (
	"fmt"
	"os"
	"strings"
)

func checkCapabilities() error {
	data, err := os.ReadFile("/proc/self/status")
	if err != nil {
		return fmt.Errorf("cannot read /proc/self/status: %w", err)
	}
	for _, line := range strings.Split(string(data), "\n") {
		if strings.HasPrefix(line, "CapEff:") {
			hex := strings.TrimSpace(strings.TrimPrefix(line, "CapEff:"))
			var caps uint64
			if _, err := fmt.Sscanf(hex, "%x", &caps); err != nil {
				return fmt.Errorf("parse CapEff %q: %w", hex, err)
			}
			const capNetBindService = 1 << 10
			if caps&capNetBindService == 0 {
				return fmt.Errorf(
					"CAP_NET_BIND_SERVICE not in effective set (CapEff: %s)\n"+
						"Fix: sudo setcap cap_net_bind_service=+ep %s",
					hex, os.Args[0],
				)
			}
			return nil
		}
	}
	return fmt.Errorf("CapEff not found in /proc/self/status")
}

func main() {
	if err := checkCapabilities(); err != nil {
		fmt.Fprintf(os.Stderr, "startup: capability check failed: %v\n", err)
		os.Exit(1)
	}
	// ... start server on :443
}

The error message includes the exact setcap command needed to fix the problem. When this fires in CI or on a fresh deployment where someone forgot the Makefile step, the person reading the log knows exactly what happened and exactly what to run. No guessing, no Googling, no Stack Overflow thread from 2014.

Which One Should You Use?

Method Works with Interpreters Persists Scope Complexity
setcap No — stamps the interpreter Until binary is replaced Single binary One command
setpriv Yes — per-process Per launch only Single process Verbose but precise
systemd (use mkunit) Yes — via unit config Service lifetime Single service Declarative
authbind Yes — LD_PRELOAD Permanent config Per-port File-based setup
sysctl N/A — removes restriction Until reboot (or persisted) System-wide One line

For Go on a systemd-managed server, setcap is the shortest path — one command after each build, permanent until you recompile, no runtime wrapper. Put it in your Makefile, verify at startup, and move on. For interpreted languages, setpriv or a hardened systemd unit are the correct tools — they apply permissions at the process level without contaminating the interpreter, and mkunit generates the unit file with hardening defaults already in place so you don't have to remember every directive. For legacy infrastructure, authbind does its job — don't rip it out for ideology, but don't reach for it on new builds. For single-purpose servers and containers, the sysctl approach is the simplest and the most honest.

Why Does Nobody Do This?

The real question isn't technical. Every tool described in this article is stable, well-documented, and ships with the kernel or with util-linux. There's nothing to install. There's nothing experimental. setcap has been available since Linux 2.6.24 — that's 2008. The sysctl option landed in kernel 4.11 — 2017. setpriv has been in util-linux since 2012. Ambient capabilities arrived in kernel 4.3 — 2015. None of this is new.

The reason people still reach for sudo is the same reason they reach for chmod 777, or wrap everything in Docker just to get process isolation, or add an Nginx reverse proxy on the same machine just to do port forwarding from 443 to 8080. It's the path of least resistance, and the path of least resistance is the path of maximum unexamined surface area. It works in the screenshot. It passes the demo. It ships.

Tutorials don't teach capabilities because tutorials optimise for "it works" screenshots, not operational security. Deployment guides skip setcap because it takes an extra paragraph to explain. Cloud provider documentation pushes you toward their load balancers and managed services, where the port problem is abstracted away along with your understanding of it. And once sudo ./myserver is in your muscle memory, the incentive to learn the right way evaporates — until it doesn't, and by then you're reading a post-mortem instead of a blog post.

Go makes this particularly easy to get right. The single binary is the executable, so setcap does exactly what you'd expect — no interpreter caveats, no LD_PRELOAD workarounds, no virtualenv complications. Five lines of startup code verify the capability is present. A Makefile target ensures it's set after every build. The deployment story is four verbs: build, stamp, verify, run. Zero ambiguity, zero root.

Stop running as root. You never needed to.