Nick Khyl 551d6ae0f3 ipn, ipn/ipnauth: implement API surface for LocalBackend access checking
We have a lot of access checks spread around the
ipnserver, ipnlocal, localapi, and ipnauth
packages, with a significant number of
platform-specific checks that are used exclusively
on either Windows or Unix-like platforms.
Additionally, with the exception of a few
Windows-specific checks, most of these checks are
per-device rather than per-profile, which is not
always correct even on single-user/single-session
environments, but even more problematic on
multi-user/multi-session environments such as
Windows.

We initially attempted to map all possible
operations onto the permitRead/permitWrite access
flags. However, these flags are not utilized on
Windows and prove insufficient on Unix machines.
Specifically, on Windows, the first user to
connect is granted full access, while subsequent
logged-in users have no access to the LocalAPI at
all. This restriction applies regardless of the
environment, local user roles (e.g., whether a
Windows user is a local admin), or whether they
are the active user on a shared Windows client
device. Conversely, on Unix, we introduced the
permitCert flag to enable granting non-root web
servers (such as www-data, caddy, nginx, etc.)
access to certificates. We also added additional
access check to distinguish local admins (root
on Unix-like platforms, elevated admins on
Windows) from users with permitWrite access,
and used it as a fix for the serve path LPE.

A more fine-grained access control system could
better suit our current and future needs, especially
in improving the UX across various scenarios on
corporate and personal Windows devices.

This adds an API surface in ipnauth that will be
used in LocalBackend to check access to individual
Tailscale profiles as well as any device-wide
information and operations.

Updates tailscale/corp#18342

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-19 16:15:14 -05:00

222 lines
7.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"errors"
"runtime"
"tailscale.com/ipn"
)
var _ Identity = (*windowsIdentity)(nil)
// windowsIdentity represents identity of a Windows user.
type windowsIdentity struct {
tok WindowsToken
env WindowsEnvironment
}
// newWindowsIdentity returns a new WindowsIdentity with the specified token and environment.
func newWindowsIdentity(tok WindowsToken, env WindowsEnvironment) *windowsIdentity {
identity := &windowsIdentity{tok, env}
runtime.SetFinalizer(identity, func(i *windowsIdentity) { i.Close() })
return identity
}
// UserID returns SID of a Windows user account.
func (wi *windowsIdentity) UserID() ipn.WindowsUserID {
if uid, err := wi.tok.UID(); err == nil {
return uid
}
return ""
}
// UserID returns SID of a Windows user account.
func (wi *windowsIdentity) Username() (string, error) {
return wi.tok.Username()
}
// CheckAccess reports whether wi is allowed or denied the requested access.
func (wi *windowsIdentity) CheckAccess(requested DeviceAccess) AccessCheckResult {
checker := newAccessChecker(requested)
// Debug and ResetAllProfiles access rights can only be granted to elevated admins.
if res := checker.mustGrant(DeleteAllProfiles|Debug, wi.checkElevatedAdmin); res.HasResult() {
return res
}
if wi.env.IsServer {
// Only admins can create new profiles or install client updates on Windows Server devices.
// However, we should allow these operations from non-elevated contexts (e.g GUI).
if res := checker.tryGrant(CreateProfile|InstallUpdates, wi.checkAdmin); res.HasResult() {
return res
}
} else {
// But any user should be able to create a profile or initiate an update on non-server (e.g. Windows 10/11) devices.
if res := checker.grant(CreateProfile | InstallUpdates); res.HasResult() {
return res
}
}
// Unconditionally grant ReadStatus and GenerateBugReport to all authenticated users, regardless of the environment.
if res := checker.grant(ReadDeviceStatus | GenerateBugReport); res.HasResult() {
return res
}
// Grant unrestricted device access to elevated admins.
if res := checker.tryGrant(UnrestrictedDeviceAccess, wi.checkElevatedAdmin); res.HasResult() {
return res
}
// Returns the final access check result, implicitly denying any access rights that have not been explicitly granted.
return checker.result()
}
// CheckProfileAccess reports whether wi is allowed or denied the requested access to the profile.
func (wi *windowsIdentity) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
checker := newAccessChecker(requested)
// To avoid privilege escalation, the ServePath access right must only be granted to elevated admins.
// The access request will be immediately denied if wi is not an elevated admin.
if res := checker.mustGrant(ServePath, wi.checkElevatedAdmin); res.HasResult() {
return res
}
// Profile owners have unrestricted access to their own profiles.
if wi.isProfileOwner(profile) {
if res := checker.grant(UnrestrictedProfileAccess); res.HasResult() {
return res
}
}
if isProfileShared(profile, prefs) {
// Allow all users to read basic profile info (e.g. profile and tailnet name)
// and list network device for shared profiles.
// Profile is considered shared if it has unattended mode enabled
// and/or is not owned by a specific user (e.g. created via MDM/GP).
sharedProfileRights := ReadProfileInfo | ListPeers
if !wi.env.IsServer && !wi.env.IsManaged {
// Additionally, on non-managed Windows client devices we should allow users to
// connect / disconnect, read preferences and select exit nodes.
sharedProfileRights |= Connect | Disconnect | ReadPrefs | ChangeExitNode
}
if res := checker.grant(sharedProfileRights); res.HasResult() {
return res
}
}
if !wi.env.IsServer && !isProfileEnforced(profile, prefs) {
// Allow any user to disconnect from non-enforced Tailnets on non-Windows Server devices.
// TODO(nickkhyl): automatically disconnect from the current Tailnet
// when a different user logs in or unlocks their Windows session,
// unless the unattended mode is enabled. But in the meantime, we should allow users
// to disconnect themselves.
if res := checker.grant(Disconnect); res.HasResult() {
return res
}
}
if isAdmin, _ := wi.tok.IsAdministrator(); isAdmin {
// Allow local admins to disconnect from any tailnet.
localAdminRights := Disconnect
if wi.tok.IsElevated() {
// Allow elevated admins unrestricted access to all local profiles,
// except for reading private keys.
localAdminRights |= UnrestrictedProfileAccess & ^ReadPrivateKeys
}
if isProfileShared(profile, prefs) {
// Allow all admins unrestricted access to shared profiles,
// except for reading private keys.
// This is to allow shared profiles created by others (admins or users)
// to be managed from the GUI client.
localAdminRights |= UnrestrictedProfileAccess & ^ReadPrivateKeys
}
if res := checker.grant(localAdminRights); res.HasResult() {
return res
}
}
return checker.result()
}
// Close implements io.Closer by releasing resources associated with the Windows user identity.
func (wi *windowsIdentity) Close() error {
if wi == nil || wi.tok == nil {
return nil
}
if err := wi.tok.Close(); err != nil {
return err
}
runtime.SetFinalizer(wi, nil)
wi.tok = nil
return nil
}
func (wi *windowsIdentity) checkAdmin() error {
isAdmin, err := wi.tok.IsAdministrator()
if err != nil {
return err
}
if !isAdmin {
return errors.New("the requested operation requires local admin rights")
}
return nil
}
func (wi *windowsIdentity) checkElevatedAdmin() error {
if !wi.tok.IsElevated() {
return errors.New("the requested operation requires elevation")
}
return nil
}
func (wi *windowsIdentity) isProfileOwner(profile ipn.LoginProfileView) bool {
return wi.tok.IsUID(profile.LocalUserID())
}
// isProfileShared reports whether the specified profile is considered shared,
// meaning that all local users should have at least ReadProfileInfo and ListPeers
// access to it, but may be granted additional access rights based on the environment
// and their role on the device.
func isProfileShared(profile ipn.LoginProfileView, prefs ipn.PrefsGetter) bool {
if profile.LocalUserID() == "" {
// Profiles created as LocalSystem (e.g. via MDM) can be used by everyone on the device.
return true
}
if prefs, err := prefs(); err == nil {
// Profiles that have unattended mode enabled can be used by everyone on the device.
return prefs.ForceDaemon()
}
return false
}
func isProfileEnforced(ipn.LoginProfileView, ipn.PrefsGetter) bool {
// TODO(nickkhyl): allow to mark profiles as enforced to prevent
// regular users from disconnecting.
return false
}
// WindowsEnvironment describes the current Windows environment.
type WindowsEnvironment struct {
IsServer bool // whether running on a server edition of Windows
IsManaged bool // whether the device is managed (domain-joined or MDM-enrolled)
}
// String returns a string representation of the environment.
func (env WindowsEnvironment) String() string {
switch {
case env.IsManaged && env.IsServer:
return "Managed Server"
case env.IsManaged && !env.IsServer:
return "Managed Client"
case !env.IsManaged && env.IsServer:
return "Non-Managed Server"
case !env.IsManaged && !env.IsServer:
return "Non-Managed Client"
default:
panic("unreachable")
}
}