diff --git a/lib/vnet/customdnszonevalidator.go b/lib/vnet/customdnszonevalidator.go index a328df5bea1a6..752606581eaa4 100644 --- a/lib/vnet/customdnszonevalidator.go +++ b/lib/vnet/customdnszonevalidator.go @@ -19,7 +19,6 @@ package vnet import ( "context" "errors" - "log/slog" "net" "slices" "sync" @@ -67,7 +66,7 @@ func (c *customDNSZoneValidator) validate(ctx context.Context, clusterName, cust } requiredTXTRecord := clusterTXTRecordPrefix + clusterName - slog.InfoContext(ctx, "Checking validity of custom DNS zone by querying for required TXT record.", "zone", customDNSZone, "record", requiredTXTRecord) + log.InfoContext(ctx, "Checking validity of custom DNS zone by querying for required TXT record.", "zone", customDNSZone, "record", requiredTXTRecord) records, err := c.lookupTXT(ctx, customDNSZone) if err != nil { @@ -83,7 +82,7 @@ func (c *customDNSZoneValidator) validate(ctx context.Context, clusterName, cust return trace.Wrap(errNoTXTRecord(customDNSZone, requiredTXTRecord)) } - slog.InfoContext(ctx, "Custom DNS zone has valid TXT record.", "zone", customDNSZone, "cluster", clusterName) + log.InfoContext(ctx, "Custom DNS zone has valid TXT record.", "zone", customDNSZone, "cluster", clusterName) c.mu.Lock() defer c.mu.Unlock() diff --git a/lib/vnet/daemon/client_darwin.go b/lib/vnet/daemon/client_darwin.go index 9c53d8a7ee580..56775166e8d95 100644 --- a/lib/vnet/daemon/client_darwin.go +++ b/lib/vnet/daemon/client_darwin.go @@ -320,7 +320,15 @@ func startByCalling(ctx context.Context, bundlePath string, config Config) error } if errorDomain == nsCocoaErrorDomain && errorCode == errorCodeNSXPCConnectionCodeSigningRequirementFailure { - errC <- trace.Wrap(errXPCConnectionCodeSigningRequirementFailure, "the daemon does not appear to be code signed correctly") + // If the client submits TELEPORT_HOME to which the user doesn't have access, the daemon is + // going to shut down with an error soon after starting. Because of that, macOS won't have + // enough time to perform the verification of the code signing requirement of the daemon, as + // requested by the client. + // + // In that scenario, macOS is going to simply error that connection with + // NSXPCConnectionCodeSigningRequirementFailure. Without looking at logs, it's not possible + // to differentiate that from a "legitimate" failure caused by an incorrect requirement. + errC <- trace.Wrap(errXPCConnectionCodeSigningRequirementFailure, "either daemon is not signed correctly or it shut down before signature could be verified") return } diff --git a/lib/vnet/daemon/common.go b/lib/vnet/daemon/common.go index 896e6b2ab5814..fbcd3969d5926 100644 --- a/lib/vnet/daemon/common.go +++ b/lib/vnet/daemon/common.go @@ -17,6 +17,7 @@ package daemon import ( + "log/slog" "time" "github.com/gravitational/trace" @@ -34,6 +35,26 @@ type Config struct { DNSAddr string // HomePath points to TELEPORT_HOME that will be used by the admin process. HomePath string + // ClientCred are the credentials of the unprivileged process that wants to start VNet. + ClientCred ClientCred +} + +// ClientCred are the credentials of the unprivileged process that wants to start VNet. +type ClientCred struct { + // Valid is set if the Euid and Egid fields have been set. + Valid bool + // Egid is the effective group ID of the unprivileged process. + Egid int + // Euid is the effective user ID of the unprivileged process. + Euid int +} + +func (c ClientCred) LogValue() slog.Value { + return slog.GroupValue( + slog.Bool("creds_valid", c.Valid), + slog.Int("egid", c.Egid), + slog.Int("euid", c.Euid), + ) } func (c *Config) CheckAndSetDefaults() error { @@ -46,6 +67,8 @@ func (c *Config) CheckAndSetDefaults() error { return trace.BadParameter("missing DNS address") case c.HomePath == "": return trace.BadParameter("missing home path") + case c.ClientCred.Valid == false: + return trace.BadParameter("missing client credentials") } return nil } diff --git a/lib/vnet/daemon/service_darwin.go b/lib/vnet/daemon/service_darwin.go index bd7db4096da06..b4e940c77bb63 100644 --- a/lib/vnet/daemon/service_darwin.go +++ b/lib/vnet/daemon/service_darwin.go @@ -77,6 +77,7 @@ func Start(ctx context.Context, workFn func(context.Context, Config) error) erro "ipv6_prefix", config.IPv6Prefix, "dns_addr", config.DNSAddr, "home_path", config.HomePath, + "client_cred", config.ClientCred, ) return trace.Wrap(workFn(ctx, config)) @@ -101,8 +102,10 @@ func waitForVnetConfig(ctx context.Context) (Config, error) { C.free(unsafe.Pointer(result.home_path)) }() + var clientCred C.ClientCred + // This call gets unblocked when the daemon gets stopped through C.DaemonStop. - C.WaitForVnetConfig(&result) + C.WaitForVnetConfig(&result, &clientCred) if !result.ok { errC <- trace.Wrap(errors.New(C.GoString(result.error_description))) return @@ -113,6 +116,11 @@ func waitForVnetConfig(ctx context.Context) (Config, error) { IPv6Prefix: C.GoString(result.ipv6_prefix), DNSAddr: C.GoString(result.dns_addr), HomePath: C.GoString(result.home_path), + ClientCred: ClientCred{ + Valid: bool(clientCred.valid), + Egid: int(clientCred.egid), + Euid: int(clientCred.euid), + }, } errC <- nil }() diff --git a/lib/vnet/daemon/service_darwin.h b/lib/vnet/daemon/service_darwin.h index fd39979eed25f..9d14d06780b4f 100644 --- a/lib/vnet/daemon/service_darwin.h +++ b/lib/vnet/daemon/service_darwin.h @@ -48,11 +48,20 @@ typedef struct VnetConfigResult { const char *home_path; } VnetConfigResult; +typedef struct ClientCred { + // valid is set if the euid and egid fields have been set. + bool valid; + // egid is the effective group ID of the process on the other side of the XPC connection. + gid_t egid; + // euid is the effective user ID of the process on the other side of the XPC connection. + uid_t euid; +} ClientCred; + // WaitForVnetConfig blocks until a client calls the daemon with a config necessary to start VNet. // It can be interrupted by calling DaemonStop. // // The caller is expected to check outResult.ok to see if the call succeeded and to free strings // in VnetConfigResult. -void WaitForVnetConfig(VnetConfigResult *outResult); +void WaitForVnetConfig(VnetConfigResult *outResult, ClientCred *outClientCred); #endif /* TELEPORT_LIB_VNET_DAEMON_SERVICE_DARWIN_H_ */ diff --git a/lib/vnet/daemon/service_darwin.m b/lib/vnet/daemon/service_darwin.m index 01b636f72fe6a..613ec18f116c5 100644 --- a/lib/vnet/daemon/service_darwin.m +++ b/lib/vnet/daemon/service_darwin.m @@ -25,6 +25,21 @@ #include +@interface VNEClientCred : NSObject +{ + BOOL valid; + gid_t egid; + uid_t euid; +} +@property(nonatomic, readwrite) BOOL valid; +@property(nonatomic, readwrite) gid_t egid; +@property(nonatomic, readwrite) uid_t euid; +@end + +@implementation VNEClientCred +@synthesize valid,egid,euid; +@end + @interface VNEDaemonService () @property(nonatomic, strong, readwrite) NSXPCListener *listener; @@ -37,6 +52,7 @@ @interface VNEDaemonService () @property(nonatomic, readwrite) NSString *ipv6Prefix; @property(nonatomic, readwrite) NSString *dnsAddr; @property(nonatomic, readwrite) NSString *homePath; +@property(nonatomic, readwrite) VNEClientCred *clientCred; @property(nonatomic, readwrite) dispatch_semaphore_t gotVnetConfigSema; @end @@ -106,6 +122,12 @@ - (void)startVnet:(VnetConfig *)vnetConfig completion:(void (^)(NSError *error)) _dnsAddr = @(vnetConfig->dns_addr); _homePath = @(vnetConfig->home_path); + NSXPCConnection *currentConn = [NSXPCConnection currentConnection]; + _clientCred = [[VNEClientCred alloc] init]; + [_clientCred setEgid:[currentConn effectiveGroupIdentifier]]; + [_clientCred setEuid:[currentConn effectiveUserIdentifier]]; + [_clientCred setValid:YES]; + dispatch_semaphore_signal(_gotVnetConfigSema); completion(nil); } @@ -158,7 +180,7 @@ void DaemonStop(void) { } } -void WaitForVnetConfig(VnetConfigResult *outResult) { +void WaitForVnetConfig(VnetConfigResult *outResult, ClientCred *outClientCred) { if (!daemonService) { outResult->error_description = strdup("daemon was not initialized yet"); return; @@ -180,6 +202,13 @@ void WaitForVnetConfig(VnetConfigResult *outResult) { outResult->ipv6_prefix = VNECopyNSString([daemonService ipv6Prefix]); outResult->dns_addr = VNECopyNSString([daemonService dnsAddr]); outResult->home_path = VNECopyNSString([daemonService homePath]); + + if ([daemonService clientCred] && [[daemonService clientCred] valid]) { + outClientCred->egid = [[daemonService clientCred] egid]; + outClientCred->euid = [[daemonService clientCred] euid]; + outClientCred->valid = true; + } + outResult->ok = true; } } diff --git a/lib/vnet/osconfig.go b/lib/vnet/osconfig.go index b47aec5415b12..0642ebd0980dd 100644 --- a/lib/vnet/osconfig.go +++ b/lib/vnet/osconfig.go @@ -18,7 +18,6 @@ package vnet import ( "context" - "log/slog" "net" "github.com/gravitational/trace" @@ -28,6 +27,7 @@ import ( "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" + "github.com/gravitational/teleport/lib/vnet/daemon" ) type osConfig struct { @@ -43,14 +43,16 @@ type osConfigurator struct { clientStore *client.Store clientCache *clientcache.Cache clusterConfigCache *ClusterConfigCache - tunName string - tunIPv6 string - dnsAddr string - homePath string - tunIPv4 string + // daemonClientCred are the credentials of the process that contacted the daemon. + daemonClientCred daemon.ClientCred + tunName string + tunIPv6 string + dnsAddr string + homePath string + tunIPv4 string } -func newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath string) (*osConfigurator, error) { +func newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath string, daemonClientCred daemon.ClientCred) (*osConfigurator, error) { if homePath == "" { // This runs as root so we need to be configured with the user's home path. return nil, trace.BadParameter("homePath must be passed from unprivileged process") @@ -61,11 +63,12 @@ func newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath string) (*osConfig tunIPv6 := ipv6Prefix + "1" configurator := &osConfigurator{ - tunName: tunName, - tunIPv6: tunIPv6, - dnsAddr: dnsAddr, - homePath: homePath, - clientStore: client.NewFSClientStore(homePath), + tunName: tunName, + tunIPv6: tunIPv6, + dnsAddr: dnsAddr, + homePath: homePath, + clientStore: client.NewFSClientStore(homePath), + daemonClientCred: daemonClientCred, } configurator.clusterConfigCache = NewClusterConfigCache(clockwork.NewRealClock()) @@ -89,18 +92,32 @@ func (c *osConfigurator) close() error { return trace.Wrap(c.clientCache.Clear()) } +// updateOSConfiguration reads tsh profiles out of [c.homePath]. For each profile, it reads the VNet +// config of the root cluster and of each leaf cluster. Then it proceeds to update the OS based on +// information from that config. +// +// For the duration of reading data from clusters, it drops the root privileges, only to regain them +// before configuring the OS. func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { var dnsZones []string var cidrRanges []string - profileNames, err := profile.ListProfileNames(c.homePath) - if err != nil { - return trace.Wrap(err, "listing user profiles") - } - for _, profileName := range profileNames { - profileDNSZones, profileCIDRRanges := c.getDNSZonesAndCIDRRangesForProfile(ctx, profileName) - dnsZones = append(dnsZones, profileDNSZones...) - cidrRanges = append(cidrRanges, profileCIDRRanges...) + // Drop privileges to ensure that the user who spawned the daemon client has privileges necessary + // to access c.homePath that it sent when starting the daemon. + // Otherwise a client could make the daemon read a profile out of any directory. + if err := c.doWithDroppedRootPrivileges(ctx, func() error { + profileNames, err := profile.ListProfileNames(c.homePath) + if err != nil { + return trace.Wrap(err, "listing user profiles") + } + for _, profileName := range profileNames { + profileDNSZones, profileCIDRRanges := c.getDNSZonesAndCIDRRangesForProfile(ctx, profileName) + dnsZones = append(dnsZones, profileDNSZones...) + cidrRanges = append(cidrRanges, profileCIDRRanges...) + } + return nil + }); err != nil { + return trace.Wrap(err) } dnsZones = utils.Deduplicate(dnsZones) @@ -114,7 +131,7 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { } } - err = configureOS(ctx, &osConfig{ + err := configureOS(ctx, &osConfig{ tunName: c.tunName, tunIPv6: c.tunIPv6, tunIPv4: c.tunIPv4, @@ -137,14 +154,14 @@ func (c *osConfigurator) getDNSZonesAndCIDRRangesForProfile(ctx context.Context, defer func() { if shouldClearCacheForRoot { if err := c.clientCache.ClearForRoot(profileName); err != nil { - slog.ErrorContext(ctx, "Error while clearing client cache", "profile", profileName, "error", err) + log.ErrorContext(ctx, "Error while clearing client cache", "profile", profileName, "error", err) } } }() rootClient, err := c.clientCache.Get(ctx, profileName, "" /*leafClusterName*/) if err != nil { - slog.WarnContext(ctx, + log.WarnContext(ctx, "Failed to get root cluster client from cache, profile may be expired, not configuring VNet for this cluster", "profile", profileName, "error", err) @@ -152,7 +169,7 @@ func (c *osConfigurator) getDNSZonesAndCIDRRangesForProfile(ctx context.Context, } clusterConfig, err := c.clusterConfigCache.GetClusterConfig(ctx, rootClient) if err != nil { - slog.WarnContext(ctx, + log.WarnContext(ctx, "Failed to load VNet configuration, profile may be expired, not configuring VNet for this cluster", "profile", profileName, "error", err) @@ -164,7 +181,7 @@ func (c *osConfigurator) getDNSZonesAndCIDRRangesForProfile(ctx context.Context, leafClusters, err := getLeafClusters(ctx, rootClient) if err != nil { - slog.WarnContext(ctx, + log.WarnContext(ctx, "Failed to list leaf clusters, profile may be expired, not configuring VNet for leaf clusters of this cluster", "profile", profileName, "error", err) @@ -179,7 +196,7 @@ func (c *osConfigurator) getDNSZonesAndCIDRRangesForProfile(ctx context.Context, for _, leafClusterName := range leafClusters { clusterClient, err := c.clientCache.Get(ctx, profileName, leafClusterName) if err != nil { - slog.WarnContext(ctx, + log.WarnContext(ctx, "Failed to create leaf cluster client, not configuring VNet for this cluster", "profile", profileName, "leaf_cluster", leafClusterName, "error", err) continue @@ -187,7 +204,7 @@ func (c *osConfigurator) getDNSZonesAndCIDRRangesForProfile(ctx context.Context, clusterConfig, err := c.clusterConfigCache.GetClusterConfig(ctx, clusterClient) if err != nil { - slog.WarnContext(ctx, + log.WarnContext(ctx, "Failed to load VNet configuration, not configuring VNet for this cluster", "profile", profileName, "leaf_cluster", leafClusterName, "error", err) continue diff --git a/lib/vnet/osconfig_darwin.go b/lib/vnet/osconfig_darwin.go index e2428bf46b773..27864c80bb400 100644 --- a/lib/vnet/osconfig_darwin.go +++ b/lib/vnet/osconfig_darwin.go @@ -19,10 +19,11 @@ package vnet import ( "bufio" "context" - "log/slog" "os" "os/exec" "path/filepath" + "sync/atomic" + "syscall" "github.com/gravitational/trace" ) @@ -34,14 +35,14 @@ func configureOS(ctx context.Context, cfg *osConfig) error { // process exits and the TUN is deleted. if cfg.tunIPv4 != "" { - slog.InfoContext(ctx, "Setting IPv4 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv4) + log.InfoContext(ctx, "Setting IPv4 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv4) cmd := exec.CommandContext(ctx, "ifconfig", cfg.tunName, cfg.tunIPv4, cfg.tunIPv4, "up") if err := cmd.Run(); err != nil { return trace.Wrap(err, "running %v", cmd.Args) } } for _, cidrRange := range cfg.cidrRanges { - slog.InfoContext(ctx, "Setting an IP route for the VNet.", "netmask", cidrRange) + log.InfoContext(ctx, "Setting an IP route for the VNet.", "netmask", cidrRange) cmd := exec.CommandContext(ctx, "route", "add", "-net", cidrRange, "-interface", cfg.tunName) if err := cmd.Run(); err != nil { return trace.Wrap(err, "running %v", cmd.Args) @@ -49,13 +50,13 @@ func configureOS(ctx context.Context, cfg *osConfig) error { } if cfg.tunIPv6 != "" { - slog.InfoContext(ctx, "Setting IPv6 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv6) + log.InfoContext(ctx, "Setting IPv6 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv6) cmd := exec.CommandContext(ctx, "ifconfig", cfg.tunName, "inet6", cfg.tunIPv6, "prefixlen", "64") if err := cmd.Run(); err != nil { return trace.Wrap(err, "running %v", cmd.Args) } - slog.InfoContext(ctx, "Setting an IPv6 route for the VNet.") + log.InfoContext(ctx, "Setting an IPv6 route for the VNet.") cmd = exec.CommandContext(ctx, "route", "add", "-inet6", cfg.tunIPv6, "-prefixlen", "64", "-interface", cfg.tunName) if err := cmd.Run(); err != nil { return trace.Wrap(err, "running %v", cmd.Args) @@ -78,7 +79,7 @@ func configureDNS(ctx context.Context, nameserver string, zones []string) error return trace.BadParameter("empty nameserver with non-empty zones") } - slog.DebugContext(ctx, "Configuring DNS.", "nameserver", nameserver, "zones", zones) + log.DebugContext(ctx, "Configuring DNS.", "nameserver", nameserver, "zones", zones) if err := os.MkdirAll(resolverPath, os.FileMode(0755)); err != nil { return trace.Wrap(err, "creating %s", resolverPath) } @@ -140,3 +141,42 @@ func vnetManagedResolverFiles() (map[string]struct{}, error) { } return matchingFiles, nil } + +var hasDroppedPrivileges atomic.Bool + +// doWithDroppedRootPrivileges drops the privileges of the current process to those of the client +// process that called the VNet daemon. +func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn func() error) (err error) { + if !hasDroppedPrivileges.CompareAndSwap(false, true) { + // At the moment of writing, the VNet daemon wasn't expected to do multiple things in parallel + // with dropped privileges. If you run into this error, consider if employing a mutex is going + // to be enough or if a more elaborate refactoring is required. + return trace.CompareFailed("privileges are being temporarily dropped already") + } + defer hasDroppedPrivileges.Store(false) + + rootEgid := os.Getegid() + rootEuid := os.Geteuid() + + log.InfoContext(ctx, "Temporarily dropping root privileges.", "egid", c.daemonClientCred.Egid, "euid", c.daemonClientCred.Euid) + + if err := syscall.Setegid(c.daemonClientCred.Egid); err != nil { + panic(trace.Wrap(err, "setting egid")) + } + if err := syscall.Seteuid(c.daemonClientCred.Euid); err != nil { + panic(trace.Wrap(err, "setting euid")) + } + + defer func() { + if err := syscall.Seteuid(rootEuid); err != nil { + panic(trace.Wrap(err, "reverting euid")) + } + if err := syscall.Setegid(rootEgid); err != nil { + panic(trace.Wrap(err, "reverting egid")) + } + + log.InfoContext(ctx, "Restored root privileges.", "egid", rootEgid, "euid", rootEuid) + }() + + return trace.Wrap(fn()) +} diff --git a/lib/vnet/osconfig_other.go b/lib/vnet/osconfig_other.go new file mode 100644 index 0000000000000..22780f8bc5f11 --- /dev/null +++ b/lib/vnet/osconfig_other.go @@ -0,0 +1,34 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build !darwin +// +build !darwin + +package vnet + +import ( + "context" + + "github.com/gravitational/trace" +) + +func configureOS(ctx context.Context, cfg *osConfig) error { + return trace.Wrap(ErrVnetNotImplemented) +} + +func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn func() error) (err error) { + return trace.Wrap(ErrVnetNotImplemented) +} diff --git a/lib/vnet/setup.go b/lib/vnet/setup.go index cf0e360a8b5f1..446fa5d1022c6 100644 --- a/lib/vnet/setup.go +++ b/lib/vnet/setup.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "log/slog" "os" "time" @@ -28,11 +27,15 @@ import ( "golang.org/x/sync/errgroup" "golang.zx2c4.com/wireguard/tun" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/types" + logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/vnet/daemon" ) +var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") + // SetupAndRun creates a network stack for VNet and runs it in the background. To do this, it also // needs to launch an admin process in the background. It returns [ProcessManager] which controls // the lifecycle of both background tasks. @@ -68,7 +71,7 @@ func SetupAndRun(ctx context.Context, config *SetupAndRunConfig) (*ProcessManage if err != nil { return nil, trace.Wrap(err) } - slog.DebugContext(ctx, "Created unix socket for admin process", "socket", socketPath) + log.DebugContext(ctx, "Created unix socket for admin process", "socket", socketPath) pm.AddCriticalBackgroundTask("socket closer", func() error { // Keep the socket open until the process context is canceled. // Closing the socket signals the admin process to terminate. @@ -115,7 +118,7 @@ func SetupAndRun(ctx context.Context, config *SetupAndRunConfig) (*ProcessManage // problem with the admin process. // Returning error from processCtx will be more informative to the user, e.g., the error // will say "password prompt closed by user" instead of "read from closed socket". - slog.DebugContext(ctx, "Error from recvTUNErr ignored in favor of processCtx.Err", "error", err) + log.DebugContext(ctx, "Error from recvTUNErr ignored in favor of processCtx.Err", "error", err) return nil, trace.Wrap(context.Cause(processCtx)) } return nil, trace.Wrap(err, "receiving TUN device from admin process") @@ -219,6 +222,9 @@ func (pm *ProcessManager) Close() { // It also handles host OS configuration that must run as root, and stays alive to keep the host configuration // up to date. It will stay running until the socket at config.socketPath is deleted or until encountering an // unrecoverable error. +// +// OS configuration is updated every [osConfigurationInterval]. During the update, it temporarily +// changes egid and euid of the process to that of the client connecting to the daemon. func AdminSetup(ctx context.Context, config daemon.Config) error { if err := config.CheckAndSetDefaults(); err != nil { return trace.Wrap(err) @@ -234,7 +240,7 @@ func AdminSetup(ctx context.Context, config daemon.Config) error { errCh := make(chan error) go func() { - errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath)) + errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath, config.ClientCred)) }() // Stay alive until we get an error on errCh, indicating that the osConfig loop exited. @@ -246,7 +252,7 @@ func AdminSetup(ctx context.Context, config daemon.Config) error { select { case <-ticker.C: if _, err := os.Stat(config.SocketPath); err != nil { - slog.DebugContext(ctx, "failed to stat socket path, assuming parent exited") + log.DebugContext(ctx, "failed to stat socket path, assuming parent exited") cancel() return trace.Wrap(<-errCh) } @@ -267,7 +273,7 @@ func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, err defer func() { // We can safely close the TUN device in the admin process after it has been sent on the socket. if err := tun.Close(); err != nil { - slog.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err)) + log.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err)) } }() @@ -279,14 +285,14 @@ func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, err // osConfigurationLoop will keep running until [ctx] is canceled or an unrecoverable error is encountered, in // order to keep the host OS configuration up to date. -func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, homePath string) error { - osConfigurator, err := newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath) +func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, homePath string, clientCred daemon.ClientCred) error { + osConfigurator, err := newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath, clientCred) if err != nil { return trace.Wrap(err, "creating OS configurator") } defer func() { if err := osConfigurator.close(); err != nil { - slog.ErrorContext(ctx, "Error while closing OS configurator", "error", err) + log.ErrorContext(ctx, "Error while closing OS configurator", "error", err) } }() @@ -301,7 +307,7 @@ func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, home // Shutting down, deconfigure OS. Pass context.Background because [ctx] has likely been canceled // already but we still need to clean up. if err := osConfigurator.deconfigureOS(context.Background()); err != nil { - slog.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err) + log.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err) } }() @@ -311,7 +317,8 @@ func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, home // Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by // reading profiles from TELEPORT_HOME. - ticker := time.NewTicker(10 * time.Second) + const osConfigurationInterval = 10 * time.Second + ticker := time.NewTicker(osConfigurationInterval) defer ticker.Stop() for { select { @@ -326,7 +333,7 @@ func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, home } func createTUNDevice(ctx context.Context) (tun.Device, string, error) { - slog.DebugContext(ctx, "Creating TUN device.") + log.DebugContext(ctx, "Creating TUN device.") dev, err := tun.CreateTUN("utun", mtu) if err != nil { return nil, "", trace.Wrap(err, "creating TUN device") diff --git a/lib/vnet/setup_darwin.go b/lib/vnet/setup_darwin.go index 688a39ff55c04..e967eb8f11b73 100644 --- a/lib/vnet/setup_darwin.go +++ b/lib/vnet/setup_darwin.go @@ -72,9 +72,11 @@ do shell script quoted form of executableName & `+ `" %s -d --socket " & quoted form of socketPath & `+ `" --ipv6-prefix " & quoted form of ipv6Prefix & `+ `" --dns-addr " & quoted form of dnsAddr & `+ + `" --egid %d --euid %d" & `+ `" >/var/log/vnet.log 2>&1" `+ `with prompt "Teleport VNet wants to set up a virtual network device." with administrator privileges`, - executableName, config.SocketPath, config.IPv6Prefix, config.DNSAddr, teleport.VnetAdminSetupSubCommand) + executableName, config.SocketPath, config.IPv6Prefix, config.DNSAddr, teleport.VnetAdminSetupSubCommand, + os.Getegid(), os.Geteuid()) // The context we pass here has effect only on the password prompt being shown. Once osascript spawns the // privileged process, canceling the context (and thus killing osascript) has no effect on the privileged diff --git a/lib/vnet/setup_other.go b/lib/vnet/setup_other.go index e7f723138b187..11916d1bd94a7 100644 --- a/lib/vnet/setup_other.go +++ b/lib/vnet/setup_other.go @@ -48,10 +48,6 @@ func receiveTUNDevice(socket *net.UnixListener) (tun.Device, error) { return nil, trace.Wrap(ErrVnetNotImplemented) } -func configureOS(ctx context.Context, cfg *osConfig) error { - return trace.Wrap(ErrVnetNotImplemented) -} - func execAdminProcess(ctx context.Context, config daemon.Config) error { return trace.Wrap(ErrVnetNotImplemented) } diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go index 05e849a973287..9a155298e41f1 100644 --- a/tool/tsh/common/vnet_darwin.go +++ b/tool/tsh/common/vnet_darwin.go @@ -80,6 +80,12 @@ type vnetAdminSetupCommand struct { ipv6Prefix string // dnsAddr is the IP address for the VNet DNS server. dnsAddr string + // egid of the user starting VNet. Unsafe for production use, as the egid comes from an unstrusted + // source. + egid int + // euid of the user starting VNet. Unsafe for production use, as the euid comes from an unstrusted + // source. + euid int } func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { @@ -89,6 +95,8 @@ func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { cmd.Flag("socket", "unix socket path").StringVar(&cmd.socketPath) cmd.Flag("ipv6-prefix", "IPv6 prefix for the VNet").StringVar(&cmd.ipv6Prefix) cmd.Flag("dns-addr", "VNet DNS address").StringVar(&cmd.dnsAddr) + cmd.Flag("egid", "effective group ID of the user starting VNet").IntVar(&cmd.egid) + cmd.Flag("euid", "effective user ID of the user starting VNet").IntVar(&cmd.euid) return cmd } @@ -104,6 +112,11 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { IPv6Prefix: c.ipv6Prefix, DNSAddr: c.dnsAddr, HomePath: homePath, + ClientCred: daemon.ClientCred{ + Valid: true, + Egid: c.egid, + Euid: c.euid, + }, } return trace.Wrap(vnet.AdminSetup(cf.Context, config))