tsweb: add TS_DEBUG_TRUSTED_CIDRS envknob to debug (#19283)
Add a new envknob that allows connections from trusted CIDR ranges to access debug endpoints without Tailscale authentication. This is useful for in-cluster scrapers like Prometheus that are not on a tailnet, do not have static IP addresses and cannot use debug keys. Fixes #19282 Signed-off-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -206,3 +208,82 @@ func ExampleDebugHandler_Section() {
|
|||||||
fmt.Fprintf(w, "<code>%#v</code>", r)
|
fmt.Fprintf(w, "<code>%#v</code>", r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTrustedCIDRs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
want []netip.Prefix
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
raw: "",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single_v4",
|
||||||
|
raw: "10.0.0.0/8",
|
||||||
|
want: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple",
|
||||||
|
raw: "10.0.0.0/8,172.16.0.0/12",
|
||||||
|
want: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
netip.MustParsePrefix("172.16.0.0/12"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spaces_trimmed",
|
||||||
|
raw: " 10.0.0.0/8 , 192.168.0.0/16 ",
|
||||||
|
want: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6",
|
||||||
|
raw: "fd00::/8",
|
||||||
|
want: []netip.Prefix{netip.MustParsePrefix("fd00::/8")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing_comma",
|
||||||
|
raw: "10.0.0.0/8,",
|
||||||
|
want: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := parseTrustedCIDRs(tt.raw)
|
||||||
|
if !slices.Equal(got, tt.want) {
|
||||||
|
t.Fatalf("got %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowDebugAccessTrustedCIDRContains(t *testing.T) {
|
||||||
|
// Verify that parsed CIDRs correctly match/reject IPs.
|
||||||
|
cidrs := parseTrustedCIDRs("10.0.0.0/8,192.168.1.0/24,fd00::/8")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"10.1.2.3", true},
|
||||||
|
{"10.255.255.255", true},
|
||||||
|
{"192.168.1.50", true},
|
||||||
|
{"192.168.2.1", false},
|
||||||
|
{"172.16.0.1", false},
|
||||||
|
{"8.8.8.8", false},
|
||||||
|
{"fd00::1", true},
|
||||||
|
{"fe80::1", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
ip := netip.MustParseAddr(tt.ip)
|
||||||
|
if got := cidrsContain(cidrs, ip); got != tt.want {
|
||||||
|
t.Errorf("CIDRs contain %s = %v, want %v", tt.ip, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -54,6 +55,50 @@ func IsProd443(addr string) bool {
|
|||||||
return port == "443" || port == "https"
|
return port == "443" || port == "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debugTrustedCIDRs is the envknob for TS_DEBUG_TRUSTED_CIDRS, a
|
||||||
|
// comma-separated list of CIDR ranges (e.g. "10.0.0.0/8,172.16.0.0/12")
|
||||||
|
// whose source IPs are allowed to access debug endpoints without Tailscale
|
||||||
|
// authentication. This will supersede both IsTailscaleIP() and
|
||||||
|
// TS_ALLOW_DEBUG_IP.
|
||||||
|
var debugTrustedCIDRs = envknob.RegisterString("TS_DEBUG_TRUSTED_CIDRS")
|
||||||
|
|
||||||
|
// trustedCIDRs returns the parsed CIDR prefixes from TS_DEBUG_TRUSTED_CIDRS.
|
||||||
|
var trustedCIDRs = sync.OnceValue(func() []netip.Prefix {
|
||||||
|
return parseTrustedCIDRs(debugTrustedCIDRs())
|
||||||
|
})
|
||||||
|
|
||||||
|
// parseTrustedCIDRs parses a comma-separated list of CIDR prefixes.
|
||||||
|
// It fatals on invalid entries, consistent with other envknob parsing.
|
||||||
|
func parseTrustedCIDRs(raw string) []netip.Prefix {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var prefixes []netip.Prefix
|
||||||
|
for _, s := range strings.Split(raw, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pfx, err := netip.ParsePrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid CIDR in TS_DEBUG_TRUSTED_CIDRS: %q: %v", s, err)
|
||||||
|
}
|
||||||
|
prefixes = append(prefixes, pfx)
|
||||||
|
}
|
||||||
|
return prefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// cidrsContain checks if the source IP is associated with one of the
|
||||||
|
// provided cidrs.
|
||||||
|
func cidrsContain(cidrs []netip.Prefix, ip netip.Addr) bool {
|
||||||
|
for _, pfx := range cidrs {
|
||||||
|
if pfx.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// AllowDebugAccess reports whether r should be permitted to access
|
// AllowDebugAccess reports whether r should be permitted to access
|
||||||
// various debug endpoints.
|
// various debug endpoints.
|
||||||
func AllowDebugAccess(r *http.Request) bool {
|
func AllowDebugAccess(r *http.Request) bool {
|
||||||
@@ -75,6 +120,9 @@ func AllowDebugAccess(r *http.Request) bool {
|
|||||||
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
|
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if cidrsContain(trustedCIDRs(), ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user