Files
caddy/internal/bindcap_linux.go
Francis Lavoie c8341d5b69 core: Raise CAP_NET_BIND_SERVICE when binding privileged ports (#8102)
* core: Raise CAP_NET_BIND_SERVICE when binding privileged ports

When CAP_NET_BIND_SERVICE is in the permitted set but not the effective
set, raise it on a dedicated locked OS thread just for the bind, then
lower it again and let the thread exit.

This allows the binary to be granted the capability with
`setcap cap_net_bind_service=+p` instead of `=+ep`. With the effective
bit set, the kernel refuses to exec the binary at all when the
capability can't be granted (e.g. `docker run --cap-drop ALL`, or an
empty systemd CapabilityBoundingSet), even if no privileged port is
ever bound.

* core: Move bind capability helpers into internal package
2026-10-03 19:10:02 +10:00

122 lines
3.8 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
package internal
import (
"fmt"
"net"
"runtime"
"strconv"
"strings"
"golang.org/x/sys/unix"
)
// WithBindCapability calls listen, which should bind to address on
// network, raising CAP_NET_BIND_SERVICE for the duration of the call
// if address is a privileged TCP or UDP port and the capability is in
// the permitted set but not the effective set.
//
// This allows the caddy binary to be granted the capability with
// `setcap cap_net_bind_service=+p` instead of `=+ep`. With the effective
// bit set, the kernel refuses to execute the binary at all when the
// capability can't be granted (for example, in containers run with
// `--cap-drop ALL`, or systemd units with an empty CapabilityBoundingSet),
// even if Caddy never binds a privileged port. See "Safety checking for
// capability-dumb binaries" in capabilities(7).
func WithBindCapability(network, address string, listen func() (any, error)) (any, error) {
if !isPrivilegedPort(network, address) {
return listen()
}
if raise, err := shouldRaiseBindCapability(); err != nil || !raise {
return listen()
}
// capabilities are per-thread, so raise it on a locked OS thread
// and bind from there; the goroutine exits without unlocking, so
// the runtime terminates the thread instead of reusing it with
// the raised capability (or wedges it, if it is the main thread,
// which is why we also lower it again after binding)
type result struct {
ln any
err error
}
ch := make(chan result, 1)
go func() {
runtime.LockOSThread()
if err := setBindCapability(true); err != nil {
ch <- result{err: fmt.Errorf("raising CAP_NET_BIND_SERVICE: %v", err)}
return
}
ln, err := listen()
_ = setBindCapability(false)
ch <- result{ln, err}
}()
r := <-ch
return r.ln, r.err
}
// isPrivilegedPort reports whether address is a TCP or UDP
// address with a port below 1024 (other than 0).
func isPrivilegedPort(network, address string) bool {
if !strings.HasPrefix(network, "tcp") && !strings.HasPrefix(network, "udp") {
return false
}
_, portStr, err := net.SplitHostPort(address)
if err != nil {
return false
}
port, err := strconv.ParseUint(portStr, 10, 16)
return err == nil && port > 0 && port < 1024
}
const bindCapabilityMask = 1 << unix.CAP_NET_BIND_SERVICE
// shouldRaiseBindCapability reports whether CAP_NET_BIND_SERVICE is
// permitted but not effective for the calling thread.
func shouldRaiseBindCapability() (bool, error) {
_, data, err := getCapabilities()
if err != nil {
return false, err
}
return data[0].Permitted&bindCapabilityMask != 0 &&
data[0].Effective&bindCapabilityMask == 0, nil
}
// setBindCapability adds or removes CAP_NET_BIND_SERVICE in the
// effective set of the calling thread. To add it, it must already
// be permitted.
func setBindCapability(effective bool) error {
hdr, data, err := getCapabilities()
if err != nil {
return err
}
if effective {
data[0].Effective |= bindCapabilityMask
} else {
data[0].Effective &^= bindCapabilityMask
}
return unix.Capset(&hdr, &data[0])
}
func getCapabilities() (unix.CapUserHeader, [2]unix.CapUserData, error) {
hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
var data [2]unix.CapUserData
err := unix.Capget(&hdr, &data[0])
return hdr, data, err
}