mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
95671b71a6
On Windows, the idiomatic way to check access on a named pipe is for the server to impersonate the client on its current OS thread, perform access checks using the client's access token, and then revert the OS thread's access token back to its true self. The access token is a better representation of the client's rights than just a username/userid check, as it represents the client's effective rights at connection time, which might differ from their normal rights. This patch updates safesocket to do the aforementioned impersonation, extract the token handle, and then revert the impersonation. We retain the token handle for the remaining duration of the connection (the token continues to be valid even after we have reverted back to self). Since the token is a property of the connection, I changed ipnauth to wrap the concrete net.Conn to include the token. I then plumbed that change through ipnlocal, ipnserver, and localapi as necessary. I also added a PermitLocalAdmin flag to the localapi Handler which I intend to use for controlling access to a few new localapi endpoints intended for configuring auto-update. Updates https://github.com/tailscale/tailscale/issues/755 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
219 lines
6.9 KiB
Go
219 lines
6.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package ipnauth controls access to the LocalAPI.
|
|
package ipnauth
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"os/user"
|
|
"runtime"
|
|
"strconv"
|
|
|
|
"inet.af/peercred"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/net/netstat"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/groupmember"
|
|
"tailscale.com/util/winutil"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
// ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
|
|
// implemented for the current GOOS.
|
|
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
|
|
|
// WindowsToken represents the current security context of a Windows user.
|
|
type WindowsToken interface {
|
|
io.Closer
|
|
// EqualUIDs reports whether other refers to the same user ID as the receiver.
|
|
EqualUIDs(other WindowsToken) bool
|
|
// IsAdministrator reports whether the receiver is a member of the built-in
|
|
// Administrators group, or else an error. Use IsElevated to determine whether
|
|
// the receiver is actually utilizing administrative rights.
|
|
IsAdministrator() (bool, error)
|
|
// IsUID reports whether the receiver's user ID matches uid.
|
|
IsUID(uid ipn.WindowsUserID) bool
|
|
// UID returns the ipn.WindowsUserID associated with the receiver, or else
|
|
// an error.
|
|
UID() (ipn.WindowsUserID, error)
|
|
// IsElevated reports whether the receiver is currently executing as an
|
|
// elevated administrative user.
|
|
IsElevated() bool
|
|
// UserDir returns the special directory identified by folderID as associated
|
|
// with the receiver. folderID must be one of the KNOWNFOLDERID values from
|
|
// the x/sys/windows package, serialized as a stringified GUID.
|
|
UserDir(folderID string) (string, error)
|
|
// Username returns the user name associated with the receiver.
|
|
Username() (string, error)
|
|
}
|
|
|
|
// ConnIdentity represents the owner of a localhost TCP or unix socket connection
|
|
// connecting to the LocalAPI.
|
|
type ConnIdentity struct {
|
|
conn net.Conn
|
|
notWindows bool // runtime.GOOS != "windows"
|
|
|
|
// Fields used when NotWindows:
|
|
isUnixSock bool // Conn is a *net.UnixConn
|
|
creds *peercred.Creds // or nil
|
|
|
|
// Used on Windows:
|
|
// TODO(bradfitz): merge these into the peercreds package and
|
|
// use that for all.
|
|
pid int
|
|
}
|
|
|
|
// WindowsUserID returns the local machine's userid of the connection
|
|
// if it's on Windows. Otherwise it returns the empty string.
|
|
//
|
|
// It's suitable for passing to LookupUserFromID (os/user.LookupId) on any
|
|
// operating system.
|
|
func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
|
if envknob.GOOS() != "windows" {
|
|
return ""
|
|
}
|
|
if tok, err := ci.WindowsToken(); err == nil {
|
|
defer tok.Close()
|
|
if uid, err := tok.UID(); err == nil {
|
|
return uid
|
|
}
|
|
}
|
|
// For Linux tests running as Windows:
|
|
const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
|
|
if ci.creds != nil && !isBroken {
|
|
if uid, ok := ci.creds.UserID(); ok {
|
|
return ipn.WindowsUserID(uid)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
|
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
|
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
|
|
|
var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround")
|
|
|
|
// LookupUserFromID is a wrapper around os/user.LookupId that works around some
|
|
// issues on Windows. On non-Windows platforms it's identical to user.LookupId.
|
|
func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
|
u, err := user.LookupId(uid)
|
|
if err != nil && runtime.GOOS == "windows" {
|
|
// See if uid resolves as a pseudo-user. Temporary workaround until
|
|
// https://github.com/golang/go/issues/49509 resolves and ships.
|
|
if u, err := winutil.LookupPseudoUser(uid); err == nil {
|
|
return u, nil
|
|
}
|
|
|
|
// TODO(aaron): With LookupPseudoUser in place, I don't expect us to reach
|
|
// this point anymore. Leaving the below workaround in for now to confirm
|
|
// that pseudo-user resolution sufficiently handles this problem.
|
|
|
|
// The below workaround is only applicable when uid represents a
|
|
// valid security principal. Omitting this check causes us to succeed
|
|
// even when uid represents a deleted user.
|
|
if !winutil.IsSIDValidPrincipal(uid) {
|
|
return nil, err
|
|
}
|
|
|
|
metricIssue869Workaround.Add(1)
|
|
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
|
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
|
// now. We don't strictly need the username. It's just a nice-to-have.
|
|
// So make up a *user.User if their machine is broken in this way.
|
|
return &user.User{
|
|
Uid: uid,
|
|
Username: "unknown-user-" + uid,
|
|
Name: "unknown user " + uid,
|
|
}, nil
|
|
}
|
|
return u, err
|
|
}
|
|
|
|
// IsReadonlyConn reports whether the connection should be considered read-only,
|
|
// meaning it's not allowed to change the state of the node.
|
|
//
|
|
// Read-only also means it's not allowed to access sensitive information, which
|
|
// admittedly doesn't follow from the name. Consider this "IsUnprivileged".
|
|
// Also, Windows doesn't use this. For Windows it always returns false.
|
|
//
|
|
// TODO(bradfitz): rename it? Also make Windows use this.
|
|
func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool {
|
|
if runtime.GOOS == "windows" {
|
|
// Windows doesn't need/use this mechanism, at least yet. It
|
|
// has a different last-user-wins auth model.
|
|
return false
|
|
}
|
|
const ro = true
|
|
const rw = false
|
|
if !safesocket.PlatformUsesPeerCreds() {
|
|
return rw
|
|
}
|
|
creds := ci.creds
|
|
if creds == nil {
|
|
logf("connection from unknown peer; read-only")
|
|
return ro
|
|
}
|
|
uid, ok := creds.UserID()
|
|
if !ok {
|
|
logf("connection from peer with unknown userid; read-only")
|
|
return ro
|
|
}
|
|
if uid == "0" {
|
|
logf("connection from userid %v; root has access", uid)
|
|
return rw
|
|
}
|
|
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
|
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
|
return rw
|
|
}
|
|
if operatorUID != "" && uid == operatorUID {
|
|
logf("connection from userid %v; is configured operator", uid)
|
|
return rw
|
|
}
|
|
if yes, err := isLocalAdmin(uid); err != nil {
|
|
logf("connection from userid %v; read-only; %v", uid, err)
|
|
return ro
|
|
} else if yes {
|
|
logf("connection from userid %v; is local admin, has access", uid)
|
|
return rw
|
|
}
|
|
logf("connection from userid %v; read-only", uid)
|
|
return ro
|
|
}
|
|
|
|
func isLocalAdmin(uid string) (bool, error) {
|
|
u, err := user.LookupId(uid)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var adminGroup string
|
|
switch {
|
|
case runtime.GOOS == "darwin":
|
|
adminGroup = "admin"
|
|
case distro.Get() == distro.QNAP:
|
|
adminGroup = "administrators"
|
|
default:
|
|
return false, fmt.Errorf("no system admin group found")
|
|
}
|
|
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
|
}
|
|
|
|
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
|
for _, e := range entries {
|
|
if e.Local == ra && e.Remote == la {
|
|
return e.Pid
|
|
}
|
|
}
|
|
return 0
|
|
}
|