Files
teleport/lib/vnet/user_process.go
Chris Thach 5481586a57 VNet SSH will dial direct with user cert before falling back to legacy per-session MFA cert (#66584)
* Cherry-pick in-band MFA support changes for VNet that do no depend on x/crypto/ssh.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Use cached client.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Migrate to mfav2 after rebase.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

---------

Signed-off-by: Chris Thach <chris.thach@goteleport.com>
2026-05-29 23:43:16 +00:00

200 lines
8.5 KiB
Go

// Teleport
// Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
package vnet
import (
"context"
"crypto/tls"
"os"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
apiutils "github.com/gravitational/teleport/api/utils"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
)
// ClientApplication is the common interface implemented by each VNet client
// application: Connect and tsh. It provides methods to list user profiles, get
// cluster clients, issue app certificates, and report metrics and errors -
// anything that uses the user's credentials or a Teleport client.
// The name "client application" refers to a user-facing client application, in
// contrast to the MacOS daemon or Windows service.
type ClientApplication interface {
// ListProfiles lists the names of all profiles saved for the user.
ListProfiles() ([]string, error)
// GetCachedClient returns a [*client.ClusterClient] for the given profile and leaf cluster.
// [leafClusterName] may be empty when requesting a client for the root cluster. Returned clients are
// expected to be cached, as this may be called frequently.
GetCachedClient(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error)
// ReissueAppCert issues a new cert for the target app.
ReissueAppCert(ctx context.Context, appInfo *vnetv1.AppInfo, targetPort uint16) (tls.Certificate, error)
// UserTLSCert returns the user TLS certificate for the given profile.
UserTLSCert(ctx context.Context, profileName string) (tls.Certificate, error)
// GetDialOptions returns ALPN dial options for the profile.
GetDialOptions(ctx context.Context, profileName string) (*vnetv1.DialOptions, error)
// OnNewSSHSession should be called whenever a new SSH session is about to be
// started, after getting the user SSH certificate for the session.
OnNewSSHSession(ctx context.Context, profileName, rootClusterName string)
// OnNewAppConnection gets called whenever a new app connection is about to be established through VNet.
// By the time OnNewAppConnection, VNet has already verified that the user holds a valid cert for the
// app.
//
// The connection won't be established until OnNewAppConnection returns. Returning an error prevents
// the connection from being made.
OnNewAppConnection(ctx context.Context, appKey *vnetv1.AppKey) error
// OnInvalidLocalPort gets called before VNet refuses to handle a connection to a multi-port TCP app
// because the provided port does not match any of the TCP ports in the app spec.
OnInvalidLocalPort(ctx context.Context, appInfo *vnetv1.AppInfo, targetPort uint16)
// ReissueDBCert issues a new cert for the target database.
ReissueDBCert(ctx context.Context, dbInfo *vnetv1.DatabaseInfo) (tls.Certificate, error)
// OnNewDBConnection gets called whenever a new database connection is about to be established
// through VNet. By the time OnNewDBConnection is called, VNet has already verified that the user
// holds a valid cert for the database.
//
// The connection won't be established until OnNewDBConnection returns. Returning an error prevents
// the connection from being made.
OnNewDBConnection(ctx context.Context, dbKey *vnetv1.DatabaseKey) error
}
// ClusterClient is an interface defining the subset of [client.ClusterClient]
// methods used by via [ClientApplication].
type ClusterClient interface {
CurrentCluster() authclient.ClientI
ClusterName() string
RootClusterName() string
SessionSSHKeyRing(ctx context.Context, user string, target client.NodeDetails) (keyRing *client.KeyRing, completedMFA bool, err error)
PerformSessionMFACeremony(ctx context.Context, sessionID []byte) (challengeName string, err error)
}
// RunUserProcess is the entry point called by all VNet client applications
// (tsh, Connect) to start and run all VNet tasks.
//
// ctx is used for setup steps that happen before RunUserProcess passes control
// to the process manager. Canceling ctx after RunUserProcess returns will _not_
// cancel the background tasks.
//
// If [RunUserProcess] returns without error the caller is expected to
// eventually call Close on the UserProcess to clean up any resources, terminate
// all child processes, and remove any OS configuration used for actively
// running VNet. Callers should also call Wait to get notified if the process
// terminates early due to an error.
func RunUserProcess(ctx context.Context, clientApplication ClientApplication) (*UserProcess, error) {
clock := clockwork.NewRealClock()
clusterConfigCache := NewClusterConfigCache(clock)
leafClusterCache, err := newLeafClusterCache(clock)
if err != nil {
return nil, trace.Wrap(err)
}
// TODO(greedy52) VNet config may have a flag like `allow_app_https_tunnel`
// to opt-in this feature once browser support is added for app HTTPS
// tunnel. Using an unstable env var for testing purpose for now.
allowAppHTTPSTunnel, _ := apiutils.ParseBool(os.Getenv("TELEPORT_UNSTABLE_VNET_APP_HTTPS_TUNNEL"))
if allowAppHTTPSTunnel {
log.InfoContext(ctx, "App HTTPS tunnel is enabled")
}
fqdnResolver := newFQDNResolver(&fqdnResolverConfig{
clientApplication: clientApplication,
clusterConfigCache: clusterConfigCache,
leafClusterCache: leafClusterCache,
// DB access via VNet is currently only supported by the tbot beams, but
// disabled for tsh/Connect by default. flip to true to enable DB access via VNet
// for tsh/Connect to validate locally.
allowDatabaseAccess: false,
allowAppHTTPSTunnel: allowAppHTTPSTunnel,
})
unifiedClusterConfigProvider := NewUnifiedClusterConfigProvider(&UnifiedClusterConfigProviderConfig{
clientApplication: clientApplication,
clusterConfigCache: clusterConfigCache,
leafClusterCache: leafClusterCache,
})
clientApplicationService, err := newClientApplicationService(&clientApplicationServiceConfig{
clientApplication: clientApplication,
fqdnResolver: fqdnResolver,
unifiedClusterConfigProvider: unifiedClusterConfigProvider,
clock: clock,
})
if err != nil {
return nil, trace.Wrap(err)
}
processManager, processCtx := newProcessManager()
sshConfigurator := newSSHConfigurator(sshConfiguratorConfig{
clientApplication: clientApplication,
leafClusterCache: leafClusterCache,
})
processManager.AddCriticalBackgroundTask("SSH configuration loop", func() error {
return trace.Wrap(sshConfigurator.runConfigurationLoop(processCtx))
})
userProcess := &UserProcess{
clientApplication: clientApplication,
unifiedClusterConfigProvider: unifiedClusterConfigProvider,
clientApplicationService: clientApplicationService,
clock: clock,
processManager: processManager,
}
if err := userProcess.runPlatformUserProcess(processCtx); err != nil {
return nil, trace.Wrap(err)
}
return userProcess, nil
}
// UserProcess holds the state of the VNet user process, see the comment on
// [RunUserProcess] for usage details.
type UserProcess struct {
clientApplication ClientApplication
clock clockwork.Clock
unifiedClusterConfigProvider *UnifiedClusterConfigProvider
clientApplicationService *clientApplicationService
processManager *ProcessManager
networkStackInfo *vnetv1.NetworkStackInfo
}
func (p *UserProcess) Close() {
p.processManager.Close()
}
func (p *UserProcess) Wait() error {
return p.processManager.Wait()
}
func (p *UserProcess) NetworkStackInfo() *vnetv1.NetworkStackInfo {
return p.networkStackInfo
}
// GetTargetOSConfiguration returns the LocalOSConfigProvider which clients may
// use to report the proxied DNS zones, run diagnostics, etc. The returned
// *LocalOSConfigProvider will remain valid even if the UserProcess is closed.
func (p *UserProcess) GetUnifiedClusterConfigProvider() *UnifiedClusterConfigProvider {
return p.unifiedClusterConfigProvider
}