mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-28 19:27:41 +00:00

In this PR, we move the code that checks the AlwaysOn policy from ipnserver.actor to ipnauth. It is intended to be used by ipnauth.Actor implementations, and we temporarily make it exported while these implementations reside in ipnserver and in corp. We'll unexport it later. We also update [ipnauth.Actor.CheckProfileAccess] to accept an auditLogger, which is called to write details about the action to the audit log when required by the policy, and update LocalBackend.EditPrefsAs to use an auditLogger that writes to the regular backend log. Updates tailscale/corp#26146 Signed-off-by: Nick Khyl <nickk@tailscale.com>
246 lines
7.5 KiB
Go
246 lines
7.5 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnserver
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"runtime"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnauth"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/ctxkey"
|
|
"tailscale.com/util/osuser"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
var _ ipnauth.Actor = (*actor)(nil)
|
|
|
|
// actor implements [ipnauth.Actor] and provides additional functionality that is
|
|
// specific to the current (as of 2024-08-27) permission model.
|
|
//
|
|
// Deprecated: this type exists for compatibility reasons and will be removed as
|
|
// we progress on tailscale/corp#18342.
|
|
type actor struct {
|
|
logf logger.Logf
|
|
ci *ipnauth.ConnIdentity
|
|
|
|
clientID ipnauth.ClientID
|
|
// accessOverrideReason specifies the reason for overriding certain access restrictions,
|
|
// such as permitting a user to disconnect when the always-on mode is enabled,
|
|
// provided that such justification is allowed by the policy.
|
|
accessOverrideReason string
|
|
isLocalSystem bool // whether the actor is the Windows' Local System identity.
|
|
}
|
|
|
|
func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
|
|
ci, err := ipnauth.GetConnIdentity(logf, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var clientID ipnauth.ClientID
|
|
if pid := ci.Pid(); pid != 0 {
|
|
// Derive [ipnauth.ClientID] from the PID of the connected client process.
|
|
// TODO(nickkhyl): This is transient and will be re-worked as we
|
|
// progress on tailscale/corp#18342. At minimum, we should use a 2-tuple
|
|
// (PID + StartTime) or a 3-tuple (PID + StartTime + UID) to identify
|
|
// the client process. This helps prevent security issues where a
|
|
// terminated client process's PID could be reused by a different
|
|
// process. This is not currently an issue as we allow only one user to
|
|
// connect anyway.
|
|
// Additionally, we should consider caching authentication results since
|
|
// operations like retrieving a username by SID might require network
|
|
// connectivity on domain-joined devices and/or be slow.
|
|
clientID = ipnauth.ClientIDFrom(pid)
|
|
}
|
|
return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil
|
|
}
|
|
|
|
// actorWithAccessOverride returns a new actor that carries the specified
|
|
// reason for overriding certain access restrictions, if permitted by the
|
|
// policy. If the reason is "", it returns the base actor.
|
|
func actorWithAccessOverride(baseActor *actor, reason string) *actor {
|
|
if reason == "" {
|
|
return baseActor
|
|
}
|
|
return &actor{
|
|
logf: baseActor.logf,
|
|
ci: baseActor.ci,
|
|
clientID: baseActor.clientID,
|
|
accessOverrideReason: reason,
|
|
isLocalSystem: baseActor.isLocalSystem,
|
|
}
|
|
}
|
|
|
|
// CheckProfileAccess implements [ipnauth.Actor].
|
|
func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ipnauth.ProfileAccess, auditLogger ipnauth.AuditLogFunc) error {
|
|
// TODO(nickkhyl): return errors of more specific types and have them
|
|
// translated to the appropriate HTTP status codes in the API handler.
|
|
if profile.LocalUserID() != a.UserID() {
|
|
return errors.New("the target profile does not belong to the user")
|
|
}
|
|
switch requestedAccess {
|
|
case ipnauth.Disconnect:
|
|
// Disconnect is allowed if a user owns the profile and the policy permits it.
|
|
return ipnauth.CheckDisconnectPolicy(a, profile, a.accessOverrideReason, auditLogger)
|
|
default:
|
|
return errors.New("the requested operation is not allowed")
|
|
}
|
|
}
|
|
|
|
// IsLocalSystem implements [ipnauth.Actor].
|
|
func (a *actor) IsLocalSystem() bool {
|
|
return a.isLocalSystem
|
|
}
|
|
|
|
// IsLocalAdmin implements [ipnauth.Actor].
|
|
func (a *actor) IsLocalAdmin(operatorUID string) bool {
|
|
return a.isLocalSystem || connIsLocalAdmin(a.logf, a.ci, operatorUID)
|
|
}
|
|
|
|
// UserID implements [ipnauth.Actor].
|
|
func (a *actor) UserID() ipn.WindowsUserID {
|
|
return a.ci.WindowsUserID()
|
|
}
|
|
|
|
func (a *actor) pid() int {
|
|
return a.ci.Pid()
|
|
}
|
|
|
|
// ClientID implements [ipnauth.Actor].
|
|
func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
|
|
return a.clientID, a.clientID != ipnauth.NoClientID
|
|
}
|
|
|
|
// Username implements [ipnauth.Actor].
|
|
func (a *actor) Username() (string, error) {
|
|
if a.ci == nil {
|
|
a.logf("[unexpected] missing ConnIdentity in ipnserver.actor")
|
|
return "", errors.New("missing ConnIdentity")
|
|
}
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
tok, err := a.ci.WindowsToken()
|
|
if err != nil {
|
|
return "", fmt.Errorf("get windows token: %w", err)
|
|
}
|
|
defer tok.Close()
|
|
return tok.Username()
|
|
case "darwin", "linux", "illumos", "solaris":
|
|
uid, ok := a.ci.Creds().UserID()
|
|
if !ok {
|
|
return "", errors.New("missing user ID")
|
|
}
|
|
u, err := osuser.LookupByUID(uid)
|
|
if err != nil {
|
|
return "", fmt.Errorf("lookup user: %w", err)
|
|
}
|
|
return u.Username, nil
|
|
default:
|
|
return "", errors.New("unsupported OS")
|
|
}
|
|
}
|
|
|
|
type actorOrError struct {
|
|
actor ipnauth.Actor
|
|
err error
|
|
}
|
|
|
|
func (a actorOrError) unwrap() (ipnauth.Actor, error) {
|
|
return a.actor, a.err
|
|
}
|
|
|
|
var errNoActor = errors.New("connection actor not available")
|
|
|
|
var actorKey = ctxkey.New("ipnserver.actor", actorOrError{err: errNoActor})
|
|
|
|
// contextWithActor returns a new context that carries the identity of the actor
|
|
// owning the other end of the [net.Conn]. It can be retrieved with [actorFromContext].
|
|
func contextWithActor(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
|
|
actor, err := newActor(logf, c)
|
|
return actorKey.WithValue(ctx, actorOrError{actor: actor, err: err})
|
|
}
|
|
|
|
// actorFromContext returns an [ipnauth.Actor] associated with ctx,
|
|
// or an error if the context does not carry an actor's identity.
|
|
func actorFromContext(ctx context.Context) (ipnauth.Actor, error) {
|
|
return actorKey.Value(ctx).unwrap()
|
|
}
|
|
|
|
func connIsLocalSystem(ci *ipnauth.ConnIdentity) bool {
|
|
token, err := ci.WindowsToken()
|
|
return err == nil && token.IsLocalSystem()
|
|
}
|
|
|
|
// 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 connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID string) bool {
|
|
if ci == nil {
|
|
logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
|
|
return false
|
|
}
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
tok, err := ci.WindowsToken()
|
|
if err != nil {
|
|
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
|
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 := ci.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".
|
|
if operatorUID != "" && uid != operatorUID {
|
|
return false
|
|
}
|
|
u, err := osuser.LookupByUID(uid)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// Short timeout just in case sudo hangs 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
|
|
}
|
|
}
|