mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-01 07:51:04 -05:00
* 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
122 lines
3.8 KiB
Go
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
|
|
}
|