From 8484edbb2397e65c352063ca38b9f78c50b5aeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 29 Jul 2024 16:21:45 +0200 Subject: [PATCH] Drop root privileges before reading files from TELEPORT_HOME --- lib/vnet/osconfig.go | 58 ++++++++++++++++++++++++------------- lib/vnet/osconfig_darwin.go | 33 +++++++++++++++++++++ lib/vnet/osconfig_other.go | 28 ++++++++++++++++++ lib/vnet/setup.go | 12 +++++--- lib/vnet/setup_other.go | 4 --- 5 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 lib/vnet/osconfig_other.go diff --git a/lib/vnet/osconfig.go b/lib/vnet/osconfig.go index 1171ff8c693cf..0642ebd0980dd 100644 --- a/lib/vnet/osconfig.go +++ b/lib/vnet/osconfig.go @@ -27,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 { @@ -42,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") @@ -60,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()) @@ -88,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) @@ -113,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, diff --git a/lib/vnet/osconfig_darwin.go b/lib/vnet/osconfig_darwin.go index 373bbbdb3a1dc..68f54466c8a17 100644 --- a/lib/vnet/osconfig_darwin.go +++ b/lib/vnet/osconfig_darwin.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "syscall" "github.com/gravitational/trace" ) @@ -139,3 +140,35 @@ func vnetManagedResolverFiles() (map[string]struct{}, error) { } return matchingFiles, nil } + +// 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) { + rootEgid := os.Getegid() + rootEuid := os.Geteuid() + + if err := syscall.Setegid(c.daemonClientCred.Egid); err != nil { + return trace.Wrap(err, "setting egid") + } + defer func() { + syscallErr := trace.Wrap(syscall.Setegid(rootEgid), "reverting egid") + err = trace.NewAggregate(err, syscallErr) + }() + + if err := syscall.Seteuid(c.daemonClientCred.Euid); err != nil { + return trace.Wrap(err, "setting euid") + } + defer func() { + syscallErr := trace.Wrap(syscall.Seteuid(rootEuid), "reverting euid") + err = trace.NewAggregate(err, syscallErr) + }() + + log.InfoContext(ctx, "Temporarily dropping root privileges.", "egid", c.daemonClientCred.Egid, "euid", c.daemonClientCred.Euid) + defer func() { + if err == nil { + 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..8ec54ff755460 --- /dev/null +++ b/lib/vnet/osconfig_other.go @@ -0,0 +1,28 @@ +// 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 + +func configureOS(ctx context.Context, cfg *osConfig) error { + return trace.Wrap(ErrVnetNotImplemented) +} + +func (c *osConfigurator) doWithDroppedPrivileges(fn func() error) error { + return trace.Wrap(ErrVnetNotImplemented) +} diff --git a/lib/vnet/setup.go b/lib/vnet/setup.go index 20458f0d5c300..3ad97e2c64be2 100644 --- a/lib/vnet/setup.go +++ b/lib/vnet/setup.go @@ -222,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) @@ -237,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. @@ -282,8 +285,8 @@ 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") } @@ -314,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 { 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) }