actor.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ipnserver
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "net"
  9. "os/exec"
  10. "runtime"
  11. "time"
  12. "tailscale.com/feature/buildfeatures"
  13. "tailscale.com/ipn"
  14. "tailscale.com/ipn/ipnauth"
  15. "tailscale.com/types/logger"
  16. "tailscale.com/util/ctxkey"
  17. "tailscale.com/util/osuser"
  18. "tailscale.com/version"
  19. )
  20. var _ ipnauth.Actor = (*actor)(nil)
  21. // actor implements [ipnauth.Actor] and provides additional functionality that is
  22. // specific to the current (as of 2024-08-27) permission model.
  23. //
  24. // Deprecated: this type exists for compatibility reasons and will be removed as
  25. // we progress on tailscale/corp#18342.
  26. type actor struct {
  27. logf logger.Logf
  28. ci *ipnauth.ConnIdentity
  29. clientID ipnauth.ClientID
  30. userID ipn.WindowsUserID // cached Windows user ID of the connected client process.
  31. // accessOverrideReason specifies the reason for overriding certain access restrictions,
  32. // such as permitting a user to disconnect when the always-on mode is enabled,
  33. // provided that such justification is allowed by the policy.
  34. accessOverrideReason string
  35. isLocalSystem bool // whether the actor is the Windows' Local System identity.
  36. }
  37. func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
  38. ci, err := ipnauth.GetConnIdentity(logf, c)
  39. if err != nil {
  40. return nil, err
  41. }
  42. var clientID ipnauth.ClientID
  43. if pid := ci.Pid(); pid != 0 {
  44. // Derive [ipnauth.ClientID] from the PID of the connected client process.
  45. // TODO(nickkhyl): This is transient and will be re-worked as we
  46. // progress on tailscale/corp#18342. At minimum, we should use a 2-tuple
  47. // (PID + StartTime) or a 3-tuple (PID + StartTime + UID) to identify
  48. // the client process. This helps prevent security issues where a
  49. // terminated client process's PID could be reused by a different
  50. // process. This is not currently an issue as we allow only one user to
  51. // connect anyway.
  52. // Additionally, we should consider caching authentication results since
  53. // operations like retrieving a username by SID might require network
  54. // connectivity on domain-joined devices and/or be slow.
  55. clientID = ipnauth.ClientIDFrom(pid)
  56. }
  57. return &actor{
  58. logf: logf,
  59. ci: ci,
  60. clientID: clientID,
  61. userID: ci.WindowsUserID(),
  62. isLocalSystem: connIsLocalSystem(ci),
  63. },
  64. nil
  65. }
  66. // actorWithAccessOverride returns a new actor that carries the specified
  67. // reason for overriding certain access restrictions, if permitted by the
  68. // policy. If the reason is "", it returns the base actor.
  69. func actorWithAccessOverride(baseActor *actor, reason string) *actor {
  70. if reason == "" {
  71. return baseActor
  72. }
  73. return &actor{
  74. logf: baseActor.logf,
  75. ci: baseActor.ci,
  76. clientID: baseActor.clientID,
  77. userID: baseActor.userID,
  78. accessOverrideReason: reason,
  79. isLocalSystem: baseActor.isLocalSystem,
  80. }
  81. }
  82. // CheckProfileAccess implements [ipnauth.Actor].
  83. func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ipnauth.ProfileAccess, auditLogger ipnauth.AuditLogFunc) error {
  84. // TODO(nickkhyl): return errors of more specific types and have them
  85. // translated to the appropriate HTTP status codes in the API handler.
  86. if profile.LocalUserID() != a.UserID() {
  87. return errors.New("the target profile does not belong to the user")
  88. }
  89. switch requestedAccess {
  90. case ipnauth.Disconnect:
  91. // Disconnect is allowed if a user owns the profile and the policy permits it.
  92. return ipnauth.CheckDisconnectPolicy(a, profile, a.accessOverrideReason, auditLogger)
  93. default:
  94. return errors.New("the requested operation is not allowed")
  95. }
  96. }
  97. // IsLocalSystem implements [ipnauth.Actor].
  98. func (a *actor) IsLocalSystem() bool {
  99. return a.isLocalSystem
  100. }
  101. // IsLocalAdmin implements [ipnauth.Actor].
  102. func (a *actor) IsLocalAdmin(operatorUID string) bool {
  103. return a.isLocalSystem || connIsLocalAdmin(a.logf, a.ci, operatorUID)
  104. }
  105. // UserID implements [ipnauth.Actor].
  106. func (a *actor) UserID() ipn.WindowsUserID {
  107. return a.userID
  108. }
  109. func (a *actor) pid() int {
  110. return a.ci.Pid()
  111. }
  112. // ClientID implements [ipnauth.Actor].
  113. func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
  114. return a.clientID, a.clientID != ipnauth.NoClientID
  115. }
  116. // Context implements [ipnauth.Actor].
  117. func (a *actor) Context() context.Context { return context.Background() }
  118. // Username implements [ipnauth.Actor].
  119. func (a *actor) Username() (string, error) {
  120. if a.ci == nil {
  121. a.logf("[unexpected] missing ConnIdentity in ipnserver.actor")
  122. return "", errors.New("missing ConnIdentity")
  123. }
  124. switch runtime.GOOS {
  125. case "windows":
  126. tok, err := a.ci.WindowsToken()
  127. if err != nil {
  128. return "", fmt.Errorf("get windows token: %w", err)
  129. }
  130. defer tok.Close()
  131. return tok.Username()
  132. case "darwin", "linux", "illumos", "solaris", "openbsd":
  133. creds := a.ci.Creds()
  134. if creds == nil {
  135. return "", errors.New("peer credentials not implemented on this OS")
  136. }
  137. uid, ok := creds.UserID()
  138. if !ok {
  139. return "", errors.New("missing user ID")
  140. }
  141. u, err := osuser.LookupByUID(uid)
  142. if err != nil {
  143. return "", fmt.Errorf("lookup user: %w", err)
  144. }
  145. return u.Username, nil
  146. default:
  147. return "", errors.New("unsupported OS")
  148. }
  149. }
  150. type actorOrError struct {
  151. actor ipnauth.Actor
  152. err error
  153. }
  154. func (a actorOrError) unwrap() (ipnauth.Actor, error) {
  155. return a.actor, a.err
  156. }
  157. var errNoActor = errors.New("connection actor not available")
  158. var actorKey = ctxkey.New("ipnserver.actor", actorOrError{err: errNoActor})
  159. // contextWithActor returns a new context that carries the identity of the actor
  160. // owning the other end of the [net.Conn]. It can be retrieved with [actorFromContext].
  161. func contextWithActor(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
  162. actor, err := newActor(logf, c)
  163. return actorKey.WithValue(ctx, actorOrError{actor: actor, err: err})
  164. }
  165. // NewContextWithActorForTest returns a new context that carries the identity
  166. // of the specified actor. It is used in tests only.
  167. func NewContextWithActorForTest(ctx context.Context, actor ipnauth.Actor) context.Context {
  168. return actorKey.WithValue(ctx, actorOrError{actor: actor})
  169. }
  170. // actorFromContext returns an [ipnauth.Actor] associated with ctx,
  171. // or an error if the context does not carry an actor's identity.
  172. func actorFromContext(ctx context.Context) (ipnauth.Actor, error) {
  173. return actorKey.Value(ctx).unwrap()
  174. }
  175. func connIsLocalSystem(ci *ipnauth.ConnIdentity) bool {
  176. token, err := ci.WindowsToken()
  177. return err == nil && token.IsLocalSystem()
  178. }
  179. // connIsLocalAdmin reports whether the connected client has administrative
  180. // access to the local machine, for whatever that means with respect to the
  181. // current OS.
  182. //
  183. // This is useful because tailscaled itself always runs with elevated rights:
  184. // we want to avoid privilege escalation for certain mutative operations.
  185. func connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID string) bool {
  186. if ci == nil {
  187. logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
  188. return false
  189. }
  190. switch runtime.GOOS {
  191. case "windows":
  192. tok, err := ci.WindowsToken()
  193. if err != nil {
  194. if !errors.Is(err, ipnauth.ErrNotImplemented) {
  195. logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
  196. }
  197. return false
  198. }
  199. defer tok.Close()
  200. return tok.IsElevated()
  201. case "darwin":
  202. // Unknown, or at least unchecked on sandboxed macOS variants. Err on
  203. // the side of less permissions.
  204. //
  205. // authorizeServeConfigForGOOSAndUserContext should not call
  206. // connIsLocalAdmin on sandboxed variants anyway.
  207. if version.IsSandboxedMacOS() {
  208. return false
  209. }
  210. // This is a standalone tailscaled setup, use the same logic as on
  211. // Linux.
  212. fallthrough
  213. case "linux":
  214. if !buildfeatures.HasUnixSocketIdentity {
  215. // Everybody is an admin if support for unix socket identities
  216. // is omitted for the build.
  217. return true
  218. }
  219. uid, ok := ci.Creds().UserID()
  220. if !ok {
  221. return false
  222. }
  223. // root is always admin.
  224. if uid == "0" {
  225. return true
  226. }
  227. // if non-root, must be operator AND able to execute "sudo tailscale".
  228. if operatorUID != "" && uid != operatorUID {
  229. return false
  230. }
  231. u, err := osuser.LookupByUID(uid)
  232. if err != nil {
  233. return false
  234. }
  235. // Short timeout just in case sudo hangs for some reason.
  236. ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  237. defer cancel()
  238. if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
  239. return false
  240. }
  241. return true
  242. default:
  243. return false
  244. }
  245. }