@marcel91200 / caddy Mirror · read-only / commits / c8341d5b
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 04:45 UTC

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

Francis Lavoie committed Oct 3, 2026 at 05:10 UTC c8341d5b69e9e56e887c8add3903b8b3a54d3bd9
4 files changed +218 -1
internal/bindcap_linux.go new
+121
@@ -0,0 +1,121 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +//go:build linux
16 +
17 +package internal
18 +
19 +import (
20 + "fmt"
21 + "net"
22 + "runtime"
23 + "strconv"
24 + "strings"
25 +
26 + "golang.org/x/sys/unix"
27 +)
28 +
29 +// WithBindCapability calls listen, which should bind to address on
30 +// network, raising CAP_NET_BIND_SERVICE for the duration of the call
31 +// if address is a privileged TCP or UDP port and the capability is in
32 +// the permitted set but not the effective set.
33 +//
34 +// This allows the caddy binary to be granted the capability with
35 +// `setcap cap_net_bind_service=+p` instead of `=+ep`. With the effective
36 +// bit set, the kernel refuses to execute the binary at all when the
37 +// capability can't be granted (for example, in containers run with
38 +// `--cap-drop ALL`, or systemd units with an empty CapabilityBoundingSet),
39 +// even if Caddy never binds a privileged port. See "Safety checking for
40 +// capability-dumb binaries" in capabilities(7).
41 +func WithBindCapability(network, address string, listen func() (any, error)) (any, error) {
42 + if !isPrivilegedPort(network, address) {
43 + return listen()
44 + }
45 + if raise, err := shouldRaiseBindCapability(); err != nil || !raise {
46 + return listen()
47 + }
48 +
49 + // capabilities are per-thread, so raise it on a locked OS thread
50 + // and bind from there; the goroutine exits without unlocking, so
51 + // the runtime terminates the thread instead of reusing it with
52 + // the raised capability (or wedges it, if it is the main thread,
53 + // which is why we also lower it again after binding)
54 + type result struct {
55 + ln any
56 + err error
57 + }
58 + ch := make(chan result, 1)
59 + go func() {
60 + runtime.LockOSThread()
61 + if err := setBindCapability(true); err != nil {
62 + ch <- result{err: fmt.Errorf("raising CAP_NET_BIND_SERVICE: %v", err)}
63 + return
64 + }
65 + ln, err := listen()
66 + _ = setBindCapability(false)
67 + ch <- result{ln, err}
68 + }()
69 + r := <-ch
70 + return r.ln, r.err
71 +}
72 +
73 +// isPrivilegedPort reports whether address is a TCP or UDP
74 +// address with a port below 1024 (other than 0).
75 +func isPrivilegedPort(network, address string) bool {
76 + if !strings.HasPrefix(network, "tcp") && !strings.HasPrefix(network, "udp") {
77 + return false
78 + }
79 + _, portStr, err := net.SplitHostPort(address)
80 + if err != nil {
81 + return false
82 + }
83 + port, err := strconv.ParseUint(portStr, 10, 16)
84 + return err == nil && port > 0 && port < 1024
85 +}
86 +
87 +const bindCapabilityMask = 1 << unix.CAP_NET_BIND_SERVICE
88 +
89 +// shouldRaiseBindCapability reports whether CAP_NET_BIND_SERVICE is
90 +// permitted but not effective for the calling thread.
91 +func shouldRaiseBindCapability() (bool, error) {
92 + _, data, err := getCapabilities()
93 + if err != nil {
94 + return false, err
95 + }
96 + return data[0].Permitted&bindCapabilityMask != 0 &&
97 + data[0].Effective&bindCapabilityMask == 0, nil
98 +}
99 +
100 +// setBindCapability adds or removes CAP_NET_BIND_SERVICE in the
101 +// effective set of the calling thread. To add it, it must already
102 +// be permitted.
103 +func setBindCapability(effective bool) error {
104 + hdr, data, err := getCapabilities()
105 + if err != nil {
106 + return err
107 + }
108 + if effective {
109 + data[0].Effective |= bindCapabilityMask
110 + } else {
111 + data[0].Effective &^= bindCapabilityMask
112 + }
113 + return unix.Capset(&hdr, &data[0])
114 +}
115 +
116 +func getCapabilities() (unix.CapUserHeader, [2]unix.CapUserData, error) {
117 + hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
118 + var data [2]unix.CapUserData
119 + err := unix.Capget(&hdr, &data[0])
120 + return hdr, data, err
121 +}
internal/bindcap_linux_test.go new
+71
@@ -0,0 +1,71 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package internal
16 +
17 +import (
18 + "errors"
19 + "testing"
20 +)
21 +
22 +func TestIsPrivilegedPort(t *testing.T) {
23 + for _, tc := range []struct {
24 + network, address string
25 + want bool
26 + }{
27 + {"tcp", ":80", true},
28 + {"tcp4", "127.0.0.1:443", true},
29 + {"udp6", "[::1]:443", true},
30 + {"tcp", ":1023", true},
31 + {"tcp", ":1024", false},
32 + {"tcp", ":8080", false},
33 + {"tcp", ":0", false},
34 + {"unix", "/run/caddy.sock", false},
35 + {"fd", "3", false},
36 + {"ip4:icmp", "127.0.0.1", false},
37 + } {
38 + if got := isPrivilegedPort(tc.network, tc.address); got != tc.want {
39 + t.Errorf("isPrivilegedPort(%q, %q) = %v, want %v", tc.network, tc.address, got, tc.want)
40 + }
41 + }
42 +}
43 +
44 +func TestWithBindCapabilityPassthrough(t *testing.T) {
45 + before, err := shouldRaiseBindCapability()
46 + if err != nil {
47 + t.Fatalf("reading capabilities: %v", err)
48 + }
49 +
50 + sentinel := errors.New("sentinel")
51 + for _, tc := range []struct{ network, address string }{
52 + {"tcp", "localhost:80"},
53 + {"udp", ":8080"},
54 + {"unix", "/tmp/caddy.sock"},
55 + } {
56 + ln, err := WithBindCapability(tc.network, tc.address, func() (any, error) {
57 + return "listener", sentinel
58 + })
59 + if ln != "listener" || !errors.Is(err, sentinel) {
60 + t.Errorf("%s/%s: got (%v, %v), want listen's return values", tc.network, tc.address, ln, err)
61 + }
62 + }
63 +
64 + after, err := shouldRaiseBindCapability()
65 + if err != nil {
66 + t.Fatalf("reading capabilities: %v", err)
67 + }
68 + if before != after {
69 + t.Errorf("capabilities of calling thread changed")
70 + }
71 +}
internal/bindcap_other.go new
+23
@@ -0,0 +1,23 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +//go:build !linux
16 +
17 +package internal
18 +
19 +// WithBindCapability calls listen. Raising capabilities is only
20 +// supported on Linux.
21 +func WithBindCapability(_, _ string, listen func() (any, error)) (any, error) {
22 + return listen()
23 +}
listeners.go
+3 -1
@@ -179,7 +179,9 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
179 if ln == nil && err == nil {
180 // otherwise, create a new listener
181 lnKey := listenerKey(na.Network, address)
182 - ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
182 + ln, err = internal.WithBindCapability(na.Network, address, func() (any, error) {
183 + return listenReusable(ctx, lnKey, na.Network, address, config)
184 + })
185 }
186 }
187