mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 13:18:53 +00:00
ipn/localapi: require root or sudo+operator access for SetServeConfig (#10142)
For an operator user, require them to be able to `sudo tailscale` to use `tailscale serve`. This is similar to the Windows elevated token check. Updates tailscale/corp#15405 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
@@ -920,8 +920,8 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// require a local admin when setting a path handler
|
||||
// TODO: roll-up this Windows-specific check into either PermitWrite
|
||||
// or a global admin escalation check.
|
||||
if shouldDenyServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h) {
|
||||
http.Error(w, "must be a Windows local admin to serve a path", http.StatusUnauthorized)
|
||||
if err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -940,14 +940,30 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func shouldDenyServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) bool {
|
||||
if goos != "windows" {
|
||||
return false
|
||||
func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
|
||||
if !slices.Contains([]string{"windows", "linux", "darwin"}, goos) {
|
||||
return nil
|
||||
}
|
||||
if goos == "darwin" && !version.IsSandboxedMacOS() {
|
||||
return nil
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
return !h.CallerIsLocalAdmin
|
||||
if h.CallerIsLocalAdmin {
|
||||
return nil
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
return errors.New("must be a Windows local admin to serve a path")
|
||||
case "linux", "darwin":
|
||||
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
||||
default:
|
||||
// We filter goos at the start of the func, this default case
|
||||
// should never happen.
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -161,14 +161,40 @@ func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
goos string
|
||||
configIn *ipn.ServeConfig
|
||||
h *Handler
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "linux",
|
||||
goos: "linux",
|
||||
configIn: &ipn.ServeConfig{},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "linux-path-handler-admin",
|
||||
goos: "linux",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: true},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "linux-path-handler-not-admin",
|
||||
goos: "linux",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "windows-not-path-handler",
|
||||
@@ -180,8 +206,8 @@ func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-admin",
|
||||
@@ -193,8 +219,8 @@ func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: true},
|
||||
want: false,
|
||||
h: &Handler{CallerIsLocalAdmin: true},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-not-admin",
|
||||
@@ -206,16 +232,17 @@ func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: true,
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := shouldDenyServeConfigForGOOSAndUserContext(tt.goos, tt.configIn, tt.h)
|
||||
if got != tt.want {
|
||||
t.Errorf("shouldDenyServeConfigForGOOSAndUserContext() got = %v, want %v", got, tt.want)
|
||||
err := authorizeServeConfigForGOOSAndUserContext(tt.goos, tt.configIn, tt.h)
|
||||
gotErr := err != nil
|
||||
if gotErr != tt.wantErr {
|
||||
t.Errorf("authorizeServeConfigForGOOSAndUserContext() got error = %v, want error %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user