@ -18,6 +18,7 @@ import (
"net/http/httputil"
"net/netip"
"net/url"
"os/exec"
"runtime"
"slices"
"strconv"
@ -30,6 +31,7 @@ import (
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail"
@ -50,6 +52,7 @@ import (
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/osdiag"
"tailscale.com/util/osuser"
"tailscale.com/util/rands"
"tailscale.com/version"
"tailscale.com/wgengine/magicsock"
@ -159,16 +162,12 @@ type Handler struct {
// cert fetching access.
PermitCert bool
// CallerIsLocalAdmin is whether the this handler is being invoked as a
// result of a LocalAPI call from a user who is a local admin of the current
// machine.
//
// As of 2023-10-26 it is only populated on Windows.
//
// It can be used to to restrict some LocalAPI operations which should only
// be run by an admin and not unprivileged users in a computing environment
// managed by IT admins.
CallerIsLocalAdmin bool
// ConnIdentity is the identity of the client connected to the Handler.
ConnIdentity * ipnauth . ConnIdentity
// Test-only override for connIsLocalAdmin method. If non-nil,
// connIsLocalAdmin returns this value.
testConnIsLocalAdmin * bool
b * ipnlocal . LocalBackend
logf logger . Logf
@ -942,16 +941,22 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
}
func authorizeServeConfigForGOOSAndUserContext ( goos string , configIn * ipn . ServeConfig , h * Handler ) error {
if ! slices . Contains ( [ ] string { "windows" , "linux" , "darwin" } , goos ) {
switch goos {
case "windows" , "linux" , "darwin" :
default :
return nil
}
if goos == "darwin" && ! version . IsSandboxedMacOS ( ) {
// Only check for local admin on tailscaled-on-mac (based on "sudo"
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
// cannot serve files outside of the sandbox and this check is not
// relevant.
if goos == "darwin" && version . IsSandboxedMacOS ( ) {
return nil
}
if ! configIn . HasPathHandler ( ) {
return nil
}
if h . CallerIsLocalAdmin {
if h . connIsLocalAdmin ( ) {
return nil
}
switch goos {
@ -967,6 +972,76 @@ func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeC
}
// connIsLocalAdmin reports whether the connected client has administrative
// access to the local machine, for whatever that means with respect to the
// current OS.
//
// This is useful because tailscaled itself always runs with elevated rights:
// we want to avoid privilege escalation for certain mutative operations.
func ( h * Handler ) connIsLocalAdmin ( ) bool {
if h . testConnIsLocalAdmin != nil {
return * h . testConnIsLocalAdmin
}
if h . ConnIdentity == nil {
h . logf ( "[unexpected] missing ConnIdentity in LocalAPI Handler" )
return false
}
switch runtime . GOOS {
case "windows" :
tok , err := h . ConnIdentity . WindowsToken ( )
if err != nil {
if ! errors . Is ( err , ipnauth . ErrNotImplemented ) {
h . logf ( "ipnauth.ConnIdentity.WindowsToken() error: %v" , err )
}
return false
}
defer tok . Close ( )
return tok . IsElevated ( )
case "darwin" :
// Unknown, or at least unchecked on sandboxed macOS variants. Err on
// the side of less permissions.
//
// authorizeServeConfigForGOOSAndUserContext should not call
// connIsLocalAdmin on sandboxed variants anyway.
if version . IsSandboxedMacOS ( ) {
return false
}
// This is a standalone tailscaled setup, use the same logic as on
// Linux.
fallthrough
case "linux" :
uid , ok := h . ConnIdentity . Creds ( ) . UserID ( )
if ! ok {
return false
}
// root is always admin.
if uid == "0" {
return true
}
// if non-root, must be operator AND able to execute "sudo tailscale".
operatorUID := h . b . OperatorUserID ( )
if operatorUID != "" && uid != operatorUID {
return false
}
u , err := osuser . LookupByUID ( uid )
if err != nil {
return false
}
// Short timeout just in case sudo hands for some reason.
ctx , cancel := context . WithTimeout ( context . Background ( ) , 3 * time . Second )
defer cancel ( )
if err := exec . CommandContext ( ctx , "sudo" , "--other-user=" + u . Name , "--list" , "tailscale" ) . Run ( ) ; err != nil {
return false
}
return true
default :
return false
}
}
func ( h * Handler ) serveCheckIPForwarding ( w http . ResponseWriter , r * http . Request ) {
if ! h . PermitRead {
http . Error ( w , "IP forwarding check access denied" , http . StatusForbidden )