Compare commits
1 Commits
irbekrm/we
...
awly/linux
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ee8828139 |
@@ -5,15 +5,20 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/peercred"
|
||||
"tailscale.com/envknob"
|
||||
@@ -191,21 +196,47 @@ func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) boo
|
||||
return ro
|
||||
}
|
||||
|
||||
// IsLocalAdmin reports whether the connected user has local administrative
|
||||
// privileges on the host. This means root, or one of:
|
||||
//
|
||||
// - Windows: member of the Administrators group
|
||||
// - macOS: member of the admin group
|
||||
// - Linux: member of any sudoers group (usually "sudo" or "wheel")
|
||||
func (ci *ConnIdentity) IsLocalAdmin() (bool, error) {
|
||||
if ci.creds == nil {
|
||||
return false, nil
|
||||
}
|
||||
uid, ok := ci.creds.UserID()
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
if uid == "0" {
|
||||
return true, nil
|
||||
}
|
||||
return isLocalAdmin(uid)
|
||||
}
|
||||
|
||||
func isLocalAdmin(uid string) (bool, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var adminGroup string
|
||||
var adminGroups []string
|
||||
switch {
|
||||
case runtime.GOOS == "darwin":
|
||||
adminGroup = "admin"
|
||||
adminGroups = []string{"admin"}
|
||||
case distro.Get() == distro.QNAP:
|
||||
adminGroup = "administrators"
|
||||
adminGroups = []string{"administrators"}
|
||||
case runtime.GOOS == "linux":
|
||||
adminGroups, err = linuxSudoersGroups("/etc/sudoers")
|
||||
log.Printf("========= linuxSudoersGroups(etc/sudoers): %q %v", adminGroups, err)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("no system admin group found")
|
||||
}
|
||||
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||
return groupmember.IsMemberOfAnyGroup(u.Username, adminGroups...)
|
||||
}
|
||||
|
||||
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
||||
@@ -216,3 +247,59 @@ func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func linuxSudoersGroups(path string) ([]string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// We're looking for lines like:
|
||||
//
|
||||
// %wheel ALL=(ALL:ALL) ALL
|
||||
// %sudo ALL=(ALL:ALL) ALL
|
||||
//
|
||||
// where group name after % is allowed to sudo as any user and run any
|
||||
// command. Membership in these groups is equivalent to local admin.
|
||||
s := bufio.NewScanner(f)
|
||||
var groups []string
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if strings.HasPrefix(line, "@includedir ") {
|
||||
dir := strings.TrimPrefix(line, "@includedir ")
|
||||
paths, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range paths {
|
||||
if !p.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
incGroups, err := linuxSudoersGroups(filepath.Join(dir, p.Name()))
|
||||
log.Printf("========= linuxSudoersGroups(%q): %q %v", filepath.Join(dir, p.Name()), incGroups, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups = append(groups, incGroups...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, "%") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains([]string{"ALL=(ALL:ALL) ALL", "ALL=(ALL) ALL"}, parts[1]) {
|
||||
continue
|
||||
}
|
||||
group := strings.TrimPrefix(parts[0], "%")
|
||||
if group != "" {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, s.Err()
|
||||
}
|
||||
|
||||
@@ -202,7 +202,10 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||
lah.CallerIsLocalAdmin = s.connIsLocalAdmin(ci)
|
||||
lah.CallerIsLocalAdmin, err = ci.IsLocalAdmin()
|
||||
if err != nil {
|
||||
s.logf("IsLocalAdmin: %v", err)
|
||||
}
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -364,31 +367,6 @@ func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// connIsLocalAdmin reports whether ci has administrative access to the local
|
||||
// machine, for whatever that means with respect to the current OS.
|
||||
//
|
||||
// This returns true only on Windows machines when the client user is a
|
||||
// member of the built-in Administrators group (but not necessarily elevated).
|
||||
// This is useful because, on Windows, tailscaled itself always runs with
|
||||
// elevated rights: we want to avoid privilege escalation for certain mutative operations.
|
||||
func (s *Server) connIsLocalAdmin(ci *ipnauth.ConnIdentity) bool {
|
||||
tok, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
s.logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer tok.Close()
|
||||
|
||||
isAdmin, err := tok.IsAdministrator()
|
||||
if err != nil {
|
||||
s.logf("ipnauth.WindowsToken.IsAdministrator() error: %v", err)
|
||||
return false
|
||||
}
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
// addActiveHTTPRequest adds c to the server's list of active HTTP requests.
|
||||
//
|
||||
// If the returned error may be of type inUseOtherUserError.
|
||||
|
||||
@@ -921,7 +921,7 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 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)
|
||||
http.Error(w, "must be a local admin to serve a path", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -941,7 +941,7 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func shouldDenyServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) bool {
|
||||
if goos != "windows" {
|
||||
if !slices.Contains([]string{"windows", "linux"}, goos) {
|
||||
return false
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
|
||||
@@ -27,3 +27,26 @@ func IsMemberOfGroup(group, userName string) (bool, error) {
|
||||
}
|
||||
return slices.Contains(ugids, g.Gid), nil
|
||||
}
|
||||
|
||||
// IsMemberOfAnyGroup reports whether the provided user is a member of the any
|
||||
// of the provided system groups.
|
||||
func IsMemberOfAnyGroup(userName string, groups ...string) (bool, error) {
|
||||
u, err := user.Lookup(userName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ugids, err := u.GroupIds()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, group := range groups {
|
||||
g, err := user.LookupGroup(group)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if slices.Contains(ugids, g.Gid) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user