How to Bind a Systemd Service to Privileged Ports (443) as Non-Root User with CAP_NET_BIND_SERVICE


2 views

When deploying security-sensitive applications like Goldfish (a Vault UI) in production, we often need to balance between least privilege principles and operational requirements. The specific challenge emerges when a non-root service needs to bind to ports below 1024 (like HTTPS port 443).

The most elegant solution should work through systemd's socket activation:

[Unit]
Description=Goldfish Socket

[Socket]
ListenStream=443
NoDelay=true

However, as you've discovered, this often fails because:

  • The Go application tries to bind directly rather than inheriting the socket
  • Some applications don't properly support socket activation

The correct capability syntax for systemd services requires three key elements:

[Service]
User=goldfish
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SecureBits=keep-caps

Common pitfalls include:

  1. Missing SecureBits (required for capabilities to persist across UID change)
  2. Not specifying both AmbientCapabilities and CapabilityBoundingSet
  3. Using deprecated Capabilities parameter

While setcap works, it's indeed less secure. A better approach combines both methods:

sudo setcap cap_net_bind_service=+ep /usr/local/bin/goldfish

Then restrict execution:

chmod 750 /usr/local/bin/goldfish
chown goldfish:goldfish /usr/local/bin/goldfish

For maximum security, consider this nginx configuration:

server {
    listen 443 ssl;
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
    }
}

Then run Goldfish on port 8000 without special permissions.

For Go applications, I recommend this hybrid approach:

  1. Use systemd capabilities with proper SecureBits
  2. Add socket activation as fallback
  3. Implement application-level port configuration

Example Goldfish systemd unit:

[Service]
ExecStart=/usr/local/bin/goldfish \
    -config=/etc/goldfish.hcl \
    -address=:8000 \
    -tls-cert=/etc/ssl/certs/goldfish.pem \
    -tls-key=/etc/ssl/private/goldfish.key

# Capability configuration
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SecureBits=keep-caps

When deploying security-sensitive applications like Goldfish (a Vault UI) on Linux, we often face the dilemma of running services as non-root users while needing privileged port access. Here's a deep dive into solving this properly with systemd.

The socket activation approach seems elegant but has limitations:

[Socket]
ListenStream=443
NoDelay=true

The key misunderstanding here is that socket activation only handles the initial connection - the service itself still needs binding permissions. This explains your "permission denied" errors.

There are three capability approaches, but only one works reliably:

1. Systemd Service Capabilities (Problematic)

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

This often fails because:

  • Go binaries don't maintain capabilities across execve() calls
  • Systemd's capability inheritance has edge cases

2. Binary Capabilities (Works)

sudo setcap cap_net_bind_service=+ep /usr/local/bin/goldfish

This works because the capability is embedded in the executable itself. Security concerns can be mitigated with:

sudo chmod 750 /usr/local/bin/goldfish
sudo chown goldfish:goldfish /usr/local/bin/goldfish

3. Hybrid Approach (Most Secure)

Combine capabilities with strict permissions:

# Set capability
sudo setcap cap_net_bind_service=+ep /usr/local/bin/goldfish

# Lock down permissions
sudo chown root:goldfish /usr/local/bin/goldfish
sudo chmod 750 /usr/local/bin/goldfish

# Systemd service
[Service]
User=goldfish
Group=goldfish
ExecStart=/usr/local/bin/goldfish -config=/etc/goldfish.hcl
ProtectSystem=strict
ReadWritePaths=/etc/goldfish.hcl

For maximum security, consider this nginx configuration:

server {
    listen 443 ssl;
    server_name vault-ui.example.com;
    
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # Security headers
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
    }
    
    # SSL configuration omitted for brevity
}

Then run Goldfish on port 8000 as non-root user.

When choosing between approaches:

Method Security Complexity
Binary capabilities Medium Low
Reverse proxy High Medium
Systemd capabilities High (when working) High

For most production deployments, I recommend either the hybrid capability approach or the reverse proxy method, depending on your security requirements.