Files
teleport/lib/client/api.go
T
Michael eeb1596c98 Use serverUID for web scp target (#23124)
* Use serverUID for scp target

Revert unneeded hostname change

Remove comment

Get name

* Change name to serverId

* Fix lint errors

* Add comment

* Fix comment and signature format
2023-03-15 22:00:25 +00:00

4507 lines
143 KiB
Go

/*
Copyright 2016-2019 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/net/http2"
"golang.org/x/sync/errgroup"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1"
apitracing "github.com/gravitational/teleport/api/observability/tracing"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
"github.com/gravitational/teleport/api/profile"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/auth/touchid"
wancli "github.com/gravitational/teleport/lib/auth/webauthncli"
"github.com/gravitational/teleport/lib/client/terminal"
"github.com/gravitational/teleport/lib/defaults"
dtauthn "github.com/gravitational/teleport/lib/devicetrust/authn"
"github.com/gravitational/teleport/lib/events"
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/shell"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/scp"
"github.com/gravitational/teleport/lib/sshutils/sftp"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/agentconn"
"github.com/gravitational/teleport/lib/utils/prompt"
"github.com/gravitational/teleport/lib/utils/proxy"
)
const (
AddKeysToAgentAuto = "auto"
AddKeysToAgentNo = "no"
AddKeysToAgentYes = "yes"
AddKeysToAgentOnly = "only"
)
var AllAddKeysOptions = []string{AddKeysToAgentAuto, AddKeysToAgentNo, AddKeysToAgentYes, AddKeysToAgentOnly}
// ValidateAgentKeyOption validates that a string is a valid option for the AddKeysToAgent parameter.
func ValidateAgentKeyOption(supplied string) error {
for _, option := range AllAddKeysOptions {
if supplied == option {
return nil
}
}
return trace.BadParameter("invalid value %q, must be one of %v", supplied, AllAddKeysOptions)
}
// AgentForwardingMode describes how the user key agent will be forwarded
// to a remote machine, if at all.
type AgentForwardingMode int
const (
ForwardAgentNo AgentForwardingMode = iota
ForwardAgentYes
ForwardAgentLocal
)
var log = logrus.WithFields(logrus.Fields{
trace.Component: teleport.ComponentClient,
})
// ForwardedPort specifies local tunnel to remote
// destination managed by the client, is equivalent
// of ssh -L src:host:dst command
type ForwardedPort struct {
SrcIP string
SrcPort int
DestPort int
DestHost string
}
// ForwardedPorts contains an array of forwarded port structs
type ForwardedPorts []ForwardedPort
// ToString returns a string representation of a forwarded port spec, compatible
// with OpenSSH's -L flag, i.e. "src_host:src_port:dest_host:dest_port".
func (p *ForwardedPort) ToString() string {
sport := strconv.Itoa(p.SrcPort)
dport := strconv.Itoa(p.DestPort)
if utils.IsLocalhost(p.SrcIP) {
return sport + ":" + net.JoinHostPort(p.DestHost, dport)
}
return net.JoinHostPort(p.SrcIP, sport) + ":" + net.JoinHostPort(p.DestHost, dport)
}
// DynamicForwardedPort local port for dynamic application-level port
// forwarding. Whenever a connection is made to this port, SOCKS5 protocol
// is used to determine the address of the remote host. More or less
// equivalent to OpenSSH's -D flag.
type DynamicForwardedPort struct {
// SrcIP is the IP address to listen on locally.
SrcIP string
// SrcPort is the port to listen on locally.
SrcPort int
}
// DynamicForwardedPorts is a slice of locally forwarded dynamic ports (SOCKS5).
type DynamicForwardedPorts []DynamicForwardedPort
// ToString returns a string representation of a dynamic port spec, compatible
// with OpenSSH's -D flag, i.e. "src_host:src_port".
func (p *DynamicForwardedPort) ToString() string {
sport := strconv.Itoa(p.SrcPort)
if utils.IsLocalhost(p.SrcIP) {
return sport
}
return net.JoinHostPort(p.SrcIP, sport)
}
// HostKeyCallback is called by SSH client when it needs to check
// remote host key or certificate validity
type HostKeyCallback func(host string, ip net.Addr, key ssh.PublicKey) error
// Config is a client config
type Config struct {
// Username is the Teleport account username (for logging into Teleport proxies)
Username string
// ExplicitUsername is true if Username was initially set by the end-user
// (for example, using command-line flags).
ExplicitUsername bool
// Remote host to connect
Host string
// SearchKeywords host to connect
SearchKeywords []string
// PredicateExpression host to connect
PredicateExpression string
// Labels represent host Labels
Labels map[string]string
// Namespace is nodes namespace
Namespace string
// HostLogin is a user login on a remote host
HostLogin string
// HostPort is a remote host port to connect to. This is used for **explicit**
// port setting via -p flag, otherwise '0' is passed which means "use server default"
HostPort int
// JumpHosts if specified are interpreted in a similar way
// as -J flag in ssh - used to dial through
JumpHosts []utils.JumpHost
// WebProxyAddr is the host:port the web proxy can be accessed at.
WebProxyAddr string
// SSHProxyAddr is the host:port the SSH proxy can be accessed at.
SSHProxyAddr string
// KubeProxyAddr is the host:port the Kubernetes proxy can be accessed at.
KubeProxyAddr string
// PostgresProxyAddr is the host:port the Postgres proxy can be accessed at.
PostgresProxyAddr string
// MongoProxyAddr is the host:port the Mongo proxy can be accessed at.
MongoProxyAddr string
// MySQLProxyAddr is the host:port the MySQL proxy can be accessed at.
MySQLProxyAddr string
// KeyTTL is a time to live for the temporary SSH keypair to remain valid:
KeyTTL time.Duration
// InsecureSkipVerify is an option to skip HTTPS cert check
InsecureSkipVerify bool
// NonInteractive tells the client not to trigger interactive features for non-interactive commands,
// such as prompting user to re-login on credential errors. This is used by external programs linking
// against Teleport client and obtaining credentials from elsewhere. e.g. from an identity file.
NonInteractive bool
// Agent is an SSH agent to use for local Agent procedures. Defaults to in-memory agent keyring.
Agent agent.ExtendedAgent
ClientStore *Store
// ForwardAgent is used by the client to request agent forwarding from the server.
ForwardAgent AgentForwardingMode
// EnableX11Forwarding specifies whether X11 forwarding should be enabled.
EnableX11Forwarding bool
// X11ForwardingTimeout can be set to set a X11 forwarding timeout in seconds,
// after which any X11 forwarding requests in that session will be rejected.
X11ForwardingTimeout time.Duration
// X11ForwardingTrusted specifies the X11 forwarding security mode.
X11ForwardingTrusted bool
// AuthMethods are used to login into the cluster. If specified, the client will
// use them in addition to certs stored in the client store.
AuthMethods []ssh.AuthMethod
// TLSConfig is TLS configuration, if specified, the client
// will use this TLS configuration to access API endpoints
TLS *tls.Config
// ProxySSHPrincipal determines the SSH username (principal) the client should be using
// when connecting to auth/proxy servers. By default, the SSH username is pulled from
// the user's certificate, but the web-based terminal provides this username explicitly.
ProxySSHPrincipal string
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
// ExitStatus carries the returned value (exit status) of the remote
// process execution (via SSH exec)
ExitStatus int
// SiteName specifies site to execute operation,
// if omitted, first available site will be selected
SiteName string
// KubernetesCluster specifies the kubernetes cluster for any relevant
// operations. If empty, the auth server will choose one using stable (same
// cluster every time) but unspecified logic.
KubernetesCluster string
// DatabaseService specifies name of the database proxy server to issue
// certificate for.
DatabaseService string
// LocalForwardPorts are the local ports tsh listens on for port forwarding
// (parameters to -L ssh flag).
LocalForwardPorts ForwardedPorts
// DynamicForwardedPorts are the list of ports tsh listens on for dynamic
// port forwarding (parameters to -D ssh flag).
DynamicForwardedPorts DynamicForwardedPorts
// HostKeyCallback will be called to check host keys of the remote
// node, if not specified will be using CheckHostSignature function
// that uses local cache to validate hosts
HostKeyCallback ssh.HostKeyCallback
// KeyDir defines where temporary session keys will be stored.
// if empty, they'll go to ~/.tsh
KeysDir string
// SessionID is a session ID to use when opening a new session.
SessionID string
// InteractiveCommand tells tsh to launch a remote exec command in interactive mode,
// i.e. attaching the terminal to it.
InteractiveCommand bool
// ClientAddr (if set) specifies the true client IP. Usually it's not needed (since the server
// can look at the connecting address to determine client's IP) but for cases when the
// client is web-based, this must be set to HTTP's remote addr
ClientAddr string
// CachePolicy defines local caching policy in case if discovery goes down
// by default does not use caching
CachePolicy *CachePolicy
// CertificateFormat is the format of the SSH certificate.
CertificateFormat string
// AuthConnector is the name of the authentication connector to use.
AuthConnector string
// AuthenticatorAttachment is the desired authenticator attachment.
AuthenticatorAttachment wancli.AuthenticatorAttachment
// PreferOTP prefers OTP in favor of other MFA methods.
// Useful in constrained environments without access to USB or platform
// authenticators, such as remote hosts or virtual machines.
PreferOTP bool
// CheckVersions will check that client version is compatible
// with auth server version when connecting.
CheckVersions bool
// BindAddr is an optional host:port to bind to for SSO redirect flows.
BindAddr string
// NoRemoteExec will not execute a remote command after connecting to a host,
// will block instead. Useful when port forwarding. Equivalent of -N for OpenSSH.
NoRemoteExec bool
// Browser can be used to pass the name of a browser to override the system default
// (not currently implemented), or set to 'none' to suppress browser opening entirely.
Browser string
// AddKeysToAgent specifies how the client handles keys.
// auto - will attempt to add keys to agent if the agent supports it
// only - attempt to load keys into agent but don't write them to disk
// on - attempt to load keys into agent
// off - do not attempt to load keys into agent
AddKeysToAgent string
// EnableEscapeSequences will scan Stdin for SSH escape sequences during
// command/shell execution. This also requires Stdin to be an interactive
// terminal.
EnableEscapeSequences bool
// MockSSOLogin is used in tests for mocking the SSO login response.
MockSSOLogin SSOLoginFunc
// MockConnectToProxy is used in tests to override connection to proxy
MockConnectToProxy ConnectToProxyFunc
// HomePath is where tsh stores profiles
HomePath string
// TLSRoutingEnabled indicates that proxy supports ALPN SNI server where
// all proxy services are exposed on a single TLS listener (Proxy Web Listener).
TLSRoutingEnabled bool
// Reason is a reason attached to started sessions meant to describe their intent.
Reason string
// Invited is a list of people invited to a session.
Invited []string
// DisplayParticipantRequirements is set if debug information about participants requirements
// should be printed in moderated sessions.
DisplayParticipantRequirements bool
// ExtraProxyHeaders is a collection of http headers to be included in requests to the WebProxy.
ExtraProxyHeaders map[string]string
// AllowStdinHijack allows stdin hijack during MFA prompts.
// Stdin hijack provides a better login UX, but it can be difficult to reason
// about and is often a source of bugs.
// Do not set this options unless you deeply understand what you are doing.
AllowStdinHijack bool
// Tracer is the tracer to create spans with
Tracer oteltrace.Tracer
// PrivateKeyPolicy is a key policy that this client will try to follow during login.
PrivateKeyPolicy keys.PrivateKeyPolicy
// LoadAllCAs indicates that tsh should load the CAs of all clusters
// instead of just the current cluster.
LoadAllCAs bool
// AllowHeadless determines whether headless login can be used. Currently, only
// the ssh, scp, and ls commands can use headless login. Other commands will ignore
// headless auth connector and default to local instead.
AllowHeadless bool
}
// CachePolicy defines cache policy for local clients
type CachePolicy struct {
// CacheTTL defines cache TTL
CacheTTL time.Duration
// NeverExpire never expires local cache information
NeverExpires bool
}
// MakeDefaultConfig returns default client config
func MakeDefaultConfig() *Config {
return &Config{
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdin: os.Stdin,
AddKeysToAgent: AddKeysToAgentAuto,
EnableEscapeSequences: true,
Tracer: tracing.NoopProvider().Tracer("TeleportClient"),
}
}
// VirtualPathKind is the suffix component for env vars denoting the type of
// file that will be loaded.
type VirtualPathKind string
const (
// VirtualPathEnvPrefix is the env var name prefix shared by all virtual
// path vars.
VirtualPathEnvPrefix = "TSH_VIRTUAL_PATH"
VirtualPathKey VirtualPathKind = "KEY"
VirtualPathCA VirtualPathKind = "CA"
VirtualPathDatabase VirtualPathKind = "DB"
VirtualPathApp VirtualPathKind = "APP"
VirtualPathKubernetes VirtualPathKind = "KUBE"
)
// VirtualPathParams are an ordered list of additional optional parameters
// for a virtual path. They can be used to specify a more exact resource name
// if multiple might be available. Simpler integrations can instead only
// specify the kind and it will apply wherever a more specific env var isn't
// found.
type VirtualPathParams []string
// VirtualPathCAParams returns parameters for selecting CA certificates.
func VirtualPathCAParams(caType types.CertAuthType) VirtualPathParams {
return VirtualPathParams{
strings.ToUpper(string(caType)),
}
}
// VirtualPathDatabaseParams returns parameters for selecting specific database
// certificates.
func VirtualPathDatabaseParams(databaseName string) VirtualPathParams {
return VirtualPathParams{databaseName}
}
// VirtualPathAppParams returns parameters for selecting specific apps by name.
func VirtualPathAppParams(appName string) VirtualPathParams {
return VirtualPathParams{appName}
}
// VirtualPathKubernetesParams returns parameters for selecting k8s clusters by
// name.
func VirtualPathKubernetesParams(k8sCluster string) VirtualPathParams {
return VirtualPathParams{k8sCluster}
}
// VirtualPathEnvName formats a single virtual path environment variable name.
func VirtualPathEnvName(kind VirtualPathKind, params VirtualPathParams) string {
components := append([]string{
VirtualPathEnvPrefix,
string(kind),
}, params...)
return strings.ToUpper(strings.Join(components, "_"))
}
// VirtualPathEnvNames determines an ordered list of environment variables that
// should be checked to resolve an env var override. Params may be nil to
// indicate no additional arguments are to be specified or accepted.
func VirtualPathEnvNames(kind VirtualPathKind, params VirtualPathParams) []string {
// Bail out early if there are no parameters.
if len(params) == 0 {
return []string{VirtualPathEnvName(kind, VirtualPathParams{})}
}
var vars []string
for i := len(params); i >= 0; i-- {
vars = append(vars, VirtualPathEnvName(kind, params[0:i]))
}
return vars
}
// RetryWithRelogin is a helper error handling method, attempts to relogin and
// retry the function once.
func RetryWithRelogin(ctx context.Context, tc *TeleportClient, fn func() error) error {
fnErr := fn()
switch {
case fnErr == nil:
return nil
case utils.IsPredicateError(fnErr):
return trace.Wrap(utils.PredicateError{Err: fnErr})
case tc.NonInteractive:
return trace.Wrap(fnErr)
case !IsErrorResolvableWithRelogin(fnErr):
return trace.Wrap(fnErr)
}
log.Debugf("Activating relogin on %v.", fnErr)
// check if the error is a private key policy error.
if privateKeyPolicy, err := keys.ParsePrivateKeyPolicyError(fnErr); err == nil {
// The current private key was rejected due to an unmet key policy requirement.
fmt.Fprintf(tc.Stderr, "Unmet private key policy %q\n", privateKeyPolicy)
fmt.Fprintf(tc.Stderr, "Relogging in with YubiKey generated private key.\n")
// The current private key was rejected due to an unmet key policy requirement.
// Set the private key policy to the expected value and re-login.
tc.PrivateKeyPolicy = privateKeyPolicy
}
key, err := tc.Login(ctx)
if err != nil {
if trace.IsTrustError(err) {
return trace.Wrap(err, "refusing to connect to untrusted proxy %v without --insecure flag\n", tc.SSHProxyAddr)
}
return trace.Wrap(err)
}
if err := tc.ActivateKey(ctx, key); err != nil {
return trace.Wrap(err)
}
// Attempt device login. This activates a fresh key if successful.
if err := tc.AttemptDeviceLogin(ctx, key); err != nil {
return trace.Wrap(err)
}
// Save profile to record proxy credentials
if err := tc.SaveProfile(true); err != nil {
log.Warningf("Failed to save profile: %v", err)
return trace.Wrap(err)
}
return fn()
}
func IsErrorResolvableWithRelogin(err error) bool {
// Assume that failed handshake is a result of expired credentials.
return utils.IsHandshakeFailedError(err) || utils.IsCertExpiredError(err) ||
trace.IsBadParameter(err) || trace.IsTrustError(err) || keys.IsPrivateKeyPolicyError(err) || trace.IsNotFound(err)
}
// LoadProfile populates Config with the values stored in the given
// profiles directory. If profileDir is an empty string, the default profile
// directory ~/.tsh is used.
func (c *Config) LoadProfile(ps ProfileStore, proxyAddr string) error {
var proxyHost string
var err error
if proxyAddr == "" {
proxyHost, err = ps.CurrentProfile()
if err != nil {
return trace.Wrap(err)
}
} else {
proxyHost, err = utils.Host(proxyAddr)
if err != nil {
return trace.Wrap(err)
}
}
profile, err := ps.GetProfile(proxyHost)
if err != nil {
return trace.Wrap(err)
}
c.Username = profile.Username
c.SiteName = profile.SiteName
c.KubeProxyAddr = profile.KubeProxyAddr
c.WebProxyAddr = profile.WebProxyAddr
c.SSHProxyAddr = profile.SSHProxyAddr
c.PostgresProxyAddr = profile.PostgresProxyAddr
c.MySQLProxyAddr = profile.MySQLProxyAddr
c.MongoProxyAddr = profile.MongoProxyAddr
c.TLSRoutingEnabled = profile.TLSRoutingEnabled
c.KeysDir = profile.Dir
c.AuthConnector = profile.AuthConnector
c.LoadAllCAs = profile.LoadAllCAs
c.PrivateKeyPolicy = profile.PrivateKeyPolicy
c.AuthenticatorAttachment, err = parseMFAMode(profile.MFAMode)
if err != nil {
return trace.BadParameter("unable to parse mfa mode in user profile: %v.", err)
}
c.LocalForwardPorts, err = ParsePortForwardSpec(profile.ForwardedPorts)
if err != nil {
log.Warnf("Unable to parse port forwarding in user profile: %v.", err)
}
c.DynamicForwardedPorts, err = ParseDynamicPortForwardSpec(profile.DynamicForwardedPorts)
if err != nil {
log.Warnf("Unable to parse dynamic port forwarding in user profile: %v.", err)
}
return nil
}
// SaveProfile updates the given profiles directory with the current configuration
// If profileDir is an empty string, the default ~/.tsh is used
func (c *Config) SaveProfile(makeCurrent bool) error {
if c.WebProxyAddr == "" {
return nil
}
p := &profile.Profile{
Username: c.Username,
WebProxyAddr: c.WebProxyAddr,
SSHProxyAddr: c.SSHProxyAddr,
KubeProxyAddr: c.KubeProxyAddr,
PostgresProxyAddr: c.PostgresProxyAddr,
MySQLProxyAddr: c.MySQLProxyAddr,
MongoProxyAddr: c.MongoProxyAddr,
ForwardedPorts: c.LocalForwardPorts.String(),
SiteName: c.SiteName,
TLSRoutingEnabled: c.TLSRoutingEnabled,
AuthConnector: c.AuthConnector,
MFAMode: c.AuthenticatorAttachment.String(),
LoadAllCAs: c.LoadAllCAs,
PrivateKeyPolicy: c.PrivateKeyPolicy,
}
if err := c.ClientStore.SaveProfile(p, makeCurrent); err != nil {
return trace.Wrap(err)
}
return nil
}
// ParsedProxyHost holds the hostname and Web & SSH proxy addresses
// parsed out of a WebProxyAddress string.
type ParsedProxyHost struct {
Host string
// UsingDefaultWebProxyPort means that the port in WebProxyAddr was
// supplied by ParseProxyHost function rather than ProxyHost string
// itself.
UsingDefaultWebProxyPort bool
WebProxyAddr string
SSHProxyAddr string
}
// ParseProxyHost parses a ProxyHost string of the format <hostname>:<proxy_web_port>,<proxy_ssh_port>
// and returns the parsed components.
//
// There are several "default" ports that the Web Proxy service may use, and if the port is not
// specified in the supplied proxyHost string
//
// If a definitive answer is not possible (e.g. no proxy port is specified in
// the supplied string), ParseProxyHost() will supply default versions and flag
// that a default value is being used in the returned `ParsedProxyHost`
func ParseProxyHost(proxyHost string) (*ParsedProxyHost, error) {
host, port, err := net.SplitHostPort(proxyHost)
if err != nil {
host = proxyHost
port = ""
}
// set the default values of the port strings. One, both, or neither may
// be overridden by the port string parsing below.
usingDefaultWebProxyPort := true
webPort := strconv.Itoa(defaults.HTTPListenPort)
sshPort := strconv.Itoa(defaults.SSHProxyListenPort)
// Split the port string out into at most two parts, the proxy port and
// ssh port. Any more that 2 parts will be considered an error.
parts := strings.Split(port, ",")
switch {
// Default ports for both the SSH and Web proxy.
case len(parts) == 0:
break
// User defined HTTP proxy port, default SSH proxy port.
case len(parts) == 1:
if text := strings.TrimSpace(parts[0]); len(text) > 0 {
webPort = text
usingDefaultWebProxyPort = false
}
// User defined HTTP and SSH proxy ports.
case len(parts) == 2:
if text := strings.TrimSpace(parts[0]); len(text) > 0 {
webPort = text
usingDefaultWebProxyPort = false
}
if text := strings.TrimSpace(parts[1]); len(text) > 0 {
sshPort = text
}
default:
return nil, trace.BadParameter("unable to parse port: %v", port)
}
result := &ParsedProxyHost{
Host: host,
UsingDefaultWebProxyPort: usingDefaultWebProxyPort,
WebProxyAddr: net.JoinHostPort(host, webPort),
SSHProxyAddr: net.JoinHostPort(host, sshPort),
}
return result, nil
}
// ParseProxyHost parses the proxyHost string and updates the config.
//
// Format of proxyHost string:
//
// proxy_web_addr:<proxy_web_port>,<proxy_ssh_port>
func (c *Config) ParseProxyHost(proxyHost string) error {
parsedAddrs, err := ParseProxyHost(proxyHost)
if err != nil {
return trace.Wrap(err)
}
c.WebProxyAddr = parsedAddrs.WebProxyAddr
c.SSHProxyAddr = parsedAddrs.SSHProxyAddr
return nil
}
// KubeProxyHostPort returns the host and port of the Kubernetes proxy.
func (c *Config) KubeProxyHostPort() (string, int) {
if c.KubeProxyAddr != "" {
addr, err := utils.ParseAddr(c.KubeProxyAddr)
if err == nil {
return addr.Host(), addr.Port(defaults.KubeListenPort)
}
}
webProxyHost, _ := c.WebProxyHostPort()
return webProxyHost, defaults.KubeListenPort
}
// KubeClusterAddr returns a public HTTPS address of the proxy for use by
// Kubernetes client.
func (c *Config) KubeClusterAddr() string {
host, port := c.KubeProxyHostPort()
return fmt.Sprintf("https://%s:%d", host, port)
}
// WebProxyHostPort returns the host and port of the web proxy.
func (c *Config) WebProxyHostPort() (string, int) {
if c.WebProxyAddr != "" {
addr, err := utils.ParseAddr(c.WebProxyAddr)
if err == nil {
return addr.Host(), addr.Port(defaults.HTTPListenPort)
}
}
return "unknown", defaults.HTTPListenPort
}
// WebProxyHost returns the web proxy host without the port number.
func (c *Config) WebProxyHost() string {
host, _ := c.WebProxyHostPort()
return host
}
// WebProxyPort returns the port of the web proxy.
func (c *Config) WebProxyPort() int {
_, port := c.WebProxyHostPort()
return port
}
// SSHProxyHostPort returns the host and port of the SSH proxy.
func (c *Config) SSHProxyHostPort() (string, int) {
if c.SSHProxyAddr != "" {
addr, err := utils.ParseAddr(c.SSHProxyAddr)
if err == nil {
return addr.Host(), addr.Port(defaults.SSHProxyListenPort)
}
}
webProxyHost, _ := c.WebProxyHostPort()
return webProxyHost, defaults.SSHProxyListenPort
}
// PostgresProxyHostPort returns the host and port of Postgres proxy.
func (c *Config) PostgresProxyHostPort() (string, int) {
if c.PostgresProxyAddr != "" {
addr, err := utils.ParseAddr(c.PostgresProxyAddr)
if err == nil {
return addr.Host(), addr.Port(c.WebProxyPort())
}
}
return c.WebProxyHostPort()
}
// MongoProxyHostPort returns the host and port of Mongo proxy.
func (c *Config) MongoProxyHostPort() (string, int) {
if c.MongoProxyAddr != "" {
addr, err := utils.ParseAddr(c.MongoProxyAddr)
if err == nil {
return addr.Host(), addr.Port(defaults.MongoListenPort)
}
}
return c.WebProxyHostPort()
}
// MySQLProxyHostPort returns the host and port of MySQL proxy.
func (c *Config) MySQLProxyHostPort() (string, int) {
if c.MySQLProxyAddr != "" {
addr, err := utils.ParseAddr(c.MySQLProxyAddr)
if err == nil {
return addr.Host(), addr.Port(defaults.MySQLListenPort)
}
}
webProxyHost, _ := c.WebProxyHostPort()
return webProxyHost, defaults.MySQLListenPort
}
// DatabaseProxyHostPort returns proxy connection endpoint for the database.
func (c *Config) DatabaseProxyHostPort(db tlsca.RouteToDatabase) (string, int) {
switch db.Protocol {
case defaults.ProtocolPostgres, defaults.ProtocolCockroachDB:
return c.PostgresProxyHostPort()
case defaults.ProtocolMySQL:
return c.MySQLProxyHostPort()
case defaults.ProtocolMongoDB:
return c.MongoProxyHostPort()
}
return c.WebProxyHostPort()
}
// GetKubeTLSServerName returns k8s server name used in KUBECONFIG to leverage TLS Routing.
func GetKubeTLSServerName(k8host string) string {
isIPFormat := net.ParseIP(k8host) != nil
if k8host == "" || isIPFormat {
// If proxy is configured without public_addr set the ServerName to the 'kube.teleport.cluster.local' value.
// The k8s server name needs to be a valid hostname but when public_addr is missing from proxy settings
// the web_listen_addr is used thus webHost will contain local proxy IP address like: 0.0.0.0 or 127.0.0.1
return addSubdomainPrefix(constants.APIDomain, constants.KubeTeleportProxyALPNPrefix)
}
return addSubdomainPrefix(k8host, constants.KubeTeleportProxyALPNPrefix)
}
// GetOldKubeTLSServerName returns k8s server name used in KUBECONFIG to leverage TLS Routing.
// TODO(smallinsky) DELETE IN 14.0.0 After dropping support for KubeSNIPrefix SNI routing handler.
func GetOldKubeTLSServerName(k8host string) string {
isIPFormat := net.ParseIP(k8host) != nil
if k8host == "" || isIPFormat {
// If proxy is configured without public_addr set the ServerName to the 'kube.teleport.cluster.local' value.
// The k8s server name needs to be a valid hostname but when public_addr is missing from proxy settings
// the web_listen_addr is used thus webHost will contain local proxy IP address like: 0.0.0.0 or 127.0.0.1
return addSubdomainPrefix(constants.APIDomain, constants.KubeSNIPrefix)
}
return addSubdomainPrefix(k8host, constants.KubeSNIPrefix)
}
func addSubdomainPrefix(domain, prefix string) string {
return fmt.Sprintf("%s%s", prefix, domain)
}
// ProxyHost returns the hostname of the proxy server (without any port numbers)
func ProxyHost(proxyHost string) string {
host, _, err := net.SplitHostPort(proxyHost)
if err != nil {
return proxyHost
}
return host
}
// ProxySpecified returns true if proxy has been specified.
func (c *Config) ProxySpecified() bool {
return c.WebProxyAddr != ""
}
// DefaultResourceFilter returns the default list resource request.
func (c *Config) DefaultResourceFilter() *proto.ListResourcesRequest {
return &proto.ListResourcesRequest{
Namespace: c.Namespace,
Labels: c.Labels,
SearchKeywords: c.SearchKeywords,
PredicateExpression: c.PredicateExpression,
}
}
// dtAuthnRunCeremonyFunc matches the signature of [dtauthn.RunCeremony].
type dtAuthnRunCeremonyFunc func(context.Context, devicepb.DeviceTrustServiceClient, *devicepb.UserCertificates) (*devicepb.UserCertificates, error)
// TeleportClient is a wrapper around SSH client with teleport specific
// workflow built in.
// TeleportClient is NOT safe for concurrent use.
type TeleportClient struct {
Config
localAgent *LocalKeyAgent
// OnShellCreated gets called when the shell is created. It's
// safe to keep it nil.
OnShellCreated ShellCreatedCallback
// eventsCh is a channel used to inform clients about events have that
// occurred during the session.
eventsCh chan events.EventFields
// Note: there's no mutex guarding this or localAgent, making
// TeleportClient NOT safe for concurrent use.
lastPing *webclient.PingResponse
// dtAttemptLoginIgnorePing allows tests to override AttemptDeviceLogin's Ping
// response validation.
dtAttemptLoginIgnorePing bool
// dtAuthnRunCeremony allows tests to override the default device
// authentication function.
// Defaults to [dtauthn.RunCeremony].
dtAuthnRunCeremony dtAuthnRunCeremonyFunc
}
// ShellCreatedCallback can be supplied for every teleport client. It will
// be called right after the remote shell is created, but the session
// hasn't begun yet.
//
// It allows clients to cancel SSH action
type ShellCreatedCallback func(s *tracessh.Session, c *tracessh.Client, terminal io.ReadWriteCloser) (exit bool, err error)
// NewClient creates a TeleportClient object and fully configures it
func NewClient(c *Config) (tc *TeleportClient, err error) {
if len(c.JumpHosts) > 1 {
return nil, trace.BadParameter("only one jump host is supported, got %v", len(c.JumpHosts))
}
// validate configuration
if c.Username == "" {
c.Username, err = Username()
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("No teleport login given. defaulting to %s", c.Username)
}
if c.WebProxyAddr == "" {
return nil, trace.BadParameter("No proxy address specified, missed --proxy flag?")
}
if c.HostLogin == "" {
c.HostLogin, err = Username()
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("no host login given. defaulting to %s", c.HostLogin)
}
if c.KeyTTL == 0 {
c.KeyTTL = apidefaults.CertDuration
}
c.Namespace = types.ProcessNamespace(c.Namespace)
if c.Tracer == nil {
c.Tracer = tracing.NoopProvider().Tracer(teleport.ComponentTeleport)
}
tc = &TeleportClient{
Config: *c,
}
if tc.Stdout == nil {
tc.Stdout = os.Stdout
}
if tc.Stderr == nil {
tc.Stderr = os.Stderr
}
if tc.Stdin == nil {
tc.Stdin = os.Stdin
}
if tc.ClientStore == nil {
if tc.TLS != nil || tc.AuthMethods != nil {
// Client will use static auth methods instead of client store.
// Initialize empty client store to prevent panics.
tc.ClientStore = NewMemClientStore()
} else {
tc.ClientStore = NewFSClientStore(c.KeysDir)
if c.AddKeysToAgent == AddKeysToAgentOnly {
// Store client keys in memory, but still save trusted certs and profile to disk.
tc.ClientStore.KeyStore = NewMemKeyStore()
}
}
}
// Create a buffered channel to hold events that occurred during this session.
// This channel must be buffered because the SSH connection directly feeds
// into it. Delays in pulling messages off the global SSH request channel
// could lead to the connection hanging.
tc.eventsCh = make(chan events.EventFields, 1024)
localAgentCfg := LocalAgentConfig{
ClientStore: tc.ClientStore,
Agent: c.Agent,
ProxyHost: tc.WebProxyHost(),
Username: c.Username,
KeysOption: c.AddKeysToAgent,
Insecure: c.InsecureSkipVerify,
Site: tc.SiteName,
LoadAllCAs: tc.LoadAllCAs,
}
// initialize the local agent (auth agent which uses local SSH keys signed by the CA):
tc.localAgent, err = NewLocalAgent(localAgentCfg)
if err != nil {
return nil, trace.Wrap(err)
}
if tc.HostKeyCallback == nil {
tc.HostKeyCallback = tc.localAgent.HostKeyCallback
}
return tc, nil
}
func (tc *TeleportClient) ProfileStatus() (*ProfileStatus, error) {
status, err := tc.ClientStore.ReadProfileStatus(tc.WebProxyAddr)
if err != nil {
return nil, trace.Wrap(err)
}
// If the profile has a different username than the current client, don't return
// the profile. This is used for login and logout logic.
if status.Username != tc.Username {
return nil, trace.NotFound("no profile for proxy %v and user %v found", tc.WebProxyAddr, tc.Username)
}
return status, nil
}
// LoadKeyForCluster fetches a cluster-specific SSH key and loads it into the
// SSH agent.
func (tc *TeleportClient) LoadKeyForCluster(ctx context.Context, clusterName string) error {
_, span := tc.Tracer.Start(
ctx,
"teleportClient/LoadKeyForCluster",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
defer span.End()
if tc.localAgent == nil {
return trace.BadParameter("TeleportClient.LoadKeyForCluster called on a client without localAgent")
}
if err := tc.localAgent.LoadKeyForCluster(clusterName); err != nil {
return trace.Wrap(err)
}
return nil
}
// LoadKeyForClusterWithReissue fetches a cluster-specific SSH key and loads it into the
// SSH agent. If the key is not found, it is requested to be reissued.
func (tc *TeleportClient) LoadKeyForClusterWithReissue(ctx context.Context, clusterName string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/LoadKeyForClusterWithReissue",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
defer span.End()
err := tc.LoadKeyForCluster(ctx, clusterName)
if err == nil {
return nil
}
if !trace.IsNotFound(err) {
return trace.Wrap(err)
}
// Reissuing also loads the new key.
err = tc.ReissueUserCerts(ctx, CertCacheKeep, ReissueParams{RouteToCluster: clusterName})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// SignersForClusterWithReissue fetches cluster-specific signers from stored certificates.
// If the cluster certificates are not found, it is requested to be reissued.
func (tc *TeleportClient) SignersForClusterWithReissue(ctx context.Context, clusterName string) ([]ssh.Signer, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/LoadKeyForClusterWithReissue",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
defer span.End()
signers, err := tc.localAgent.signersForCluster(clusterName)
if err == nil {
return signers, nil
}
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
if err := tc.WithoutJumpHosts(func(tc *TeleportClient) error {
return tc.ReissueUserCerts(ctx, CertCacheKeep, ReissueParams{RouteToCluster: clusterName})
}); err != nil {
return nil, trace.Wrap(err)
}
signers, err = tc.localAgent.signersForCluster(clusterName)
if err != nil {
log.WithError(err).Warnf("Failed to load/reissue certificates for cluster %q.", clusterName)
return nil, trace.Wrap(err)
}
return signers, nil
}
// LocalAgent is a getter function for the client's local agent
func (tc *TeleportClient) LocalAgent() *LocalKeyAgent {
return tc.localAgent
}
// RootClusterName returns root cluster name.
func (tc *TeleportClient) RootClusterName(ctx context.Context) (string, error) {
_, span := tc.Tracer.Start(
ctx,
"teleportClient/RootClusterName",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
if tc.TLS != nil {
if len(tc.TLS.Certificates) == 0 || len(tc.TLS.Certificates[0].Certificate) == 0 {
return "", trace.BadParameter("missing tc.TLS.Certificates")
}
cert, err := x509.ParseCertificate(tc.TLS.Certificates[0].Certificate[0])
if err != nil {
return "", trace.Wrap(err)
}
clusterName := cert.Issuer.CommonName
if clusterName == "" {
return "", trace.NotFound("failed to extract root cluster name from Teleport TLS cert")
}
return clusterName, nil
}
key, err := tc.LocalAgent().GetCoreKey()
if err != nil {
return "", trace.Wrap(err)
}
name, err := key.RootClusterName()
if err != nil {
return "", trace.Wrap(err)
}
return name, nil
}
// getTargetNodes returns a list of node addresses this SSH command needs to
// operate on.
func (tc *TeleportClient) getTargetNodes(ctx context.Context, proxy *ProxyClient) ([]string, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/getTargetNodes",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
// use the target node that was explicitly provided if valid
if len(tc.Labels) == 0 {
// detect the common error when users use host:port address format
_, port, err := net.SplitHostPort(tc.Host)
// client has used host:port notation
if err == nil {
return nil, trace.BadParameter("please use ssh subcommand with '--port=%v' flag instead of semicolon", port)
}
addr := net.JoinHostPort(tc.Host, strconv.Itoa(tc.HostPort))
return []string{addr}, nil
}
// find the nodes matching the labels that were provided
nodes, err := proxy.FindNodesByFilters(ctx, *tc.DefaultResourceFilter())
if err != nil {
return nil, trace.Wrap(err)
}
retval := make([]string, 0, len(nodes))
for i := 0; i < len(nodes); i++ {
// always dial nodes by UUID
retval = append(retval, fmt.Sprintf("%s:0", nodes[i].GetName()))
}
return retval, nil
}
// ReissueUserCerts issues new user certs based on params and stores them in
// the local key agent (usually on disk in ~/.tsh).
func (tc *TeleportClient) ReissueUserCerts(ctx context.Context, cachePolicy CertCachePolicy, params ReissueParams) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ReissueUserCerts",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
err = RetryWithRelogin(ctx, tc, func() error {
err := proxyClient.ReissueUserCerts(ctx, cachePolicy, params)
return trace.Wrap(err)
})
return trace.Wrap(err)
}
// IssueUserCertsWithMFA issues a single-use SSH or TLS certificate for
// connecting to a target (node/k8s/db/app) specified in params with an MFA
// check. A user has to be logged in, there should be a valid login cert
// available.
//
// If access to this target does not require per-connection MFA checks
// (according to RBAC), IssueCertsWithMFA will:
// - for SSH certs, return the existing Key from the keystore.
// - for TLS certs, fall back to ReissueUserCerts.
func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams, applyOpts func(opts *PromptMFAChallengeOpts)) (*Key, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/IssueUserCertsWithMFA",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.IssueUserCertsWithMFA(
ctx, params,
func(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) {
return tc.PromptMFAChallenge(ctx, proxyAddr, c, applyOpts)
})
}
// CreateAccessRequest registers a new access request with the auth server.
func (tc *TeleportClient) CreateAccessRequest(ctx context.Context, req types.AccessRequest) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/CreateAccessRequest",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("request", req.GetName())),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.CreateAccessRequest(ctx, req)
}
// GetAccessRequests loads all access requests matching the supplied filter.
func (tc *TeleportClient) GetAccessRequests(ctx context.Context, filter types.AccessRequestFilter) ([]types.AccessRequest, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetAccessRequests",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("id", filter.ID),
attribute.String("user", filter.User),
),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.GetAccessRequests(ctx, filter)
}
// GetRole loads a role resource by name.
func (tc *TeleportClient) GetRole(ctx context.Context, name string) (types.Role, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetRole",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("role", name),
),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.GetRole(ctx, name)
}
// watchCloser is a wrapper around a services.Watcher
// which holds a closer that must be called after the watcher
// is closed.
type watchCloser struct {
types.Watcher
io.Closer
}
func (w watchCloser) Close() error {
return trace.NewAggregate(w.Watcher.Close(), w.Closer.Close())
}
// NewWatcher sets up a new event watcher.
func (tc *TeleportClient) NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/NewWatcher",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("name", watch.Name),
),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
watcher, err := proxyClient.NewWatcher(ctx, watch)
if err != nil {
proxyClient.Close()
return nil, trace.Wrap(err)
}
return watchCloser{
Watcher: watcher,
Closer: proxyClient,
}, nil
}
// WithRootClusterClient provides a functional interface for making calls
// against the root cluster's auth server.
func (tc *TeleportClient) WithRootClusterClient(ctx context.Context, do func(clt auth.ClientI) error) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/WithRootClusterClient",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
clt, err := proxyClient.ConnectToRootCluster(ctx)
if err != nil {
return trace.Wrap(err)
}
defer clt.Close()
return trace.Wrap(do(clt))
}
// NewTracingClient provides a tracing client that will forward spans on to
// the current clusters auth server. The auth server will then forward along to the configured
// telemetry backend.
func (tc *TeleportClient) NewTracingClient(ctx context.Context) (*apitracing.Client, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := proxyClient.NewTracingClient(ctx, tc.SiteName)
if err != nil {
return nil, trace.Wrap(err)
}
return clt, nil
}
// SSH connects to a node and, if 'command' is specified, executes the command on it,
// otherwise runs interactive shell
//
// Returns nil if successful, or (possibly) *exec.ExitError
func (tc *TeleportClient) SSH(ctx context.Context, command []string, runLocally bool) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/SSH",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("proxy", tc.Config.WebProxyAddr),
),
)
defer span.End()
// connect to proxy first:
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
// which nodes are we executing this commands on?
nodeAddrs, err := tc.getTargetNodes(ctx, proxyClient)
if err != nil {
return trace.Wrap(err)
}
if len(nodeAddrs) == 0 {
return trace.BadParameter("no target host specified")
}
if len(nodeAddrs) > 1 {
return tc.runShellOrCommandOnMultipleNodes(ctx, nodeAddrs, proxyClient, command)
}
return tc.runShellOrCommandOnSingleNode(ctx, nodeAddrs[0], proxyClient, command, runLocally)
}
// ConnectToNode attempts to establish a connection to the node resolved to by the provided
// NodeDetails. If the connection fails due to an Access Denied error, Auth is queried to
// determine if per-session MFA is required for the node. If it is required then the MFA
// ceremony is performed and another connection is attempted with the freshly minted
// certificates. If it is not required, then the original Access Denied error from the node
// is returned.
func (tc *TeleportClient) ConnectToNode(ctx context.Context, proxyClient *ProxyClient, nodeDetails NodeDetails, user string) (*NodeClient, error) {
node := nodeName(nodeDetails.Addr)
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ConnectToNode",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", nodeDetails.Cluster),
attribute.String("node", node),
),
)
defer span.End()
// attempt to use the existing credentials first
authMethods := proxyClient.authMethods
// if per-session mfa is required, perform the mfa ceremony to get
// new certificates and use them instead
if nodeDetails.MFACheck != nil && nodeDetails.MFACheck.Required {
am, err := proxyClient.sessionSSHCertificate(ctx, nodeDetails)
if err != nil {
return nil, trace.Wrap(err)
}
authMethods = am
}
// grab the cluster details
details, err := proxyClient.clusterDetails(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// try connecting to the node
nodeClient, connectErr := proxyClient.ConnectToNode(ctx, nodeDetails, user, details, authMethods)
switch {
case connectErr == nil: // no error return client
return nodeClient, nil
case nodeDetails.MFACheck != nil: // per-session mfa ceremony was already performed, return the results
return nodeClient, trace.Wrap(connectErr)
case connectErr != nil && !trace.IsAccessDenied(connectErr): // catastrophic error, return it
return nil, trace.Wrap(connectErr)
}
// access was denied, determine if it was because per-session mfa is required
clt, err := proxyClient.ConnectToCluster(ctx, nodeDetails.Cluster)
if err != nil {
// return the connection error instead of any errors from connecting to auth
return nil, trace.Wrap(connectErr)
}
check, err := clt.IsMFARequired(ctx, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{
Node: &proto.NodeLogin{
Node: node,
Login: proxyClient.hostLogin,
},
},
})
if err != nil {
return nil, trace.Wrap(connectErr)
}
// per-session mfa isn't required, the user simply does not
// have access to the provided node
if !check.Required {
return nil, trace.Wrap(connectErr)
}
// per-session mfa is required, perform the mfa ceremony
key, err := proxyClient.IssueUserCertsWithMFA(
ctx,
ReissueParams{
NodeName: node,
RouteToCluster: nodeDetails.Cluster,
MFACheck: check,
AuthClient: clt,
},
func(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) {
return tc.PromptMFAChallenge(ctx, proxyAddr, c, nil /* applyOpts */)
},
)
if err != nil {
return nil, trace.Wrap(err)
}
// try connecting to the node again with the newly acquired certificates
newAuthMethods, err := key.AsAuthMethod()
if err != nil {
return nil, trace.Wrap(err)
}
nodeClient, err = proxyClient.ConnectToNode(ctx, nodeDetails, user, details, []ssh.AuthMethod{newAuthMethods})
return nodeClient, trace.Wrap(err)
}
func (tc *TeleportClient) runShellOrCommandOnSingleNode(ctx context.Context, nodeAddr string, proxyClient *ProxyClient, command []string, runLocally bool) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runShellOrCommandOnSingleNode",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", tc.SiteName),
attribute.String("node", nodeAddr),
),
)
defer span.End()
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddr, Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
tc.ExitStatus = 1
return trace.Wrap(err)
}
defer nodeClient.Close()
// If forwarding ports were specified, start port forwarding.
if err := tc.startPortForwarding(ctx, nodeClient); err != nil {
return trace.Wrap(err)
}
// If no remote command execution was requested, block on the context which
// will unblock upon error or SIGINT.
if tc.NoRemoteExec {
log.Debugf("Connected to node, no remote command execution was requested, blocking until context closes.")
<-ctx.Done()
// Only return an error if the context was canceled by something other than SIGINT.
if ctx.Err() != context.Canceled {
return ctx.Err()
}
return nil
}
// After port forwarding, run a local command that uses the connection, and
// then disconnect.
if runLocally {
if len(tc.Config.LocalForwardPorts) == 0 {
fmt.Println("Executing command locally without connecting to any servers. This makes no sense.")
}
return runLocalCommand(command)
}
if len(command) > 0 {
// Reuse the existing nodeClient we connected above.
return tc.runCommand(ctx, nodeClient, command)
}
return tc.runShell(ctx, nodeClient, types.SessionPeerMode, nil, nil)
}
func (tc *TeleportClient) runShellOrCommandOnMultipleNodes(ctx context.Context, nodeAddrs []string, proxyClient *ProxyClient, command []string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runShellOrCommandOnMultipleNodes",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", tc.SiteName),
attribute.StringSlice("node", nodeAddrs),
),
)
defer span.End()
// There was a command provided, run a non-interactive session against each match
if len(command) > 0 {
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes matched label selector, running command on all.\n")
return tc.runCommandOnNodes(ctx, tc.SiteName, nodeAddrs, proxyClient, command)
}
// Issue "shell" request to the first matching node.
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes match the label selector, picking first: %q\n", nodeAddrs[0])
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddrs[0], Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
tc.ExitStatus = 1
return trace.Wrap(err)
}
defer nodeClient.Close()
return tc.runShell(ctx, nodeClient, types.SessionPeerMode, nil, nil)
}
func (tc *TeleportClient) startPortForwarding(ctx context.Context, nodeClient *NodeClient) error {
for _, fp := range tc.Config.LocalForwardPorts {
addr := net.JoinHostPort(fp.SrcIP, strconv.Itoa(fp.SrcPort))
socket, err := net.Listen("tcp", addr)
if err != nil {
return trace.Errorf("Failed to bind to %v: %v.", addr, err)
}
go nodeClient.listenAndForward(ctx, socket, addr, net.JoinHostPort(fp.DestHost, strconv.Itoa(fp.DestPort)))
}
for _, fp := range tc.Config.DynamicForwardedPorts {
addr := net.JoinHostPort(fp.SrcIP, strconv.Itoa(fp.SrcPort))
socket, err := net.Listen("tcp", addr)
if err != nil {
return trace.Errorf("Failed to bind to %v: %v.", addr, err)
}
go nodeClient.dynamicListenAndForward(ctx, socket, addr)
}
return nil
}
// Join connects to the existing/active SSH session
func (tc *TeleportClient) Join(ctx context.Context, mode types.SessionParticipantMode, namespace string, sessionID session.ID, input io.Reader) (err error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/Join",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("session", sessionID.String()),
attribute.String("mode", string(mode)),
),
)
defer span.End()
if namespace == "" {
return trace.BadParameter(auth.MissingNamespaceError)
}
tc.Stdin = input
if sessionID.Check() != nil {
return trace.Errorf("Invalid session ID format: %s", string(sessionID))
}
// connect to proxy:
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
site := proxyClient.CurrentCluster()
// Session joining is not supported in proxy recording mode
if recConfig, err := site.GetSessionRecordingConfig(ctx); err != nil {
// If the user can't see the recording mode, just let them try joining below
if !trace.IsAccessDenied(err) {
return trace.Wrap(err)
}
} else if services.IsRecordAtProxy(recConfig.GetMode()) {
return trace.BadParameter("session joining is not supported in proxy recording mode")
}
session, err := site.GetSessionTracker(ctx, string(sessionID))
if err != nil {
if trace.IsNotFound(err) {
return trace.NotFound("session %q not found or it has ended", sessionID)
}
return trace.Wrap(err)
}
if session.GetSessionKind() != types.SSHSessionKind {
return trace.BadParameter("session joining is only supported for ssh sessions, not %q sessions", session.GetSessionKind())
}
// connect to server:
nc, err := tc.ConnectToNode(ctx,
proxyClient,
NodeDetails{Addr: session.GetAddress() + ":0", Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
return trace.Wrap(err)
}
defer nc.Close()
// Start forwarding ports if configured.
if err := tc.startPortForwarding(ctx, nc); err != nil {
return trace.Wrap(err)
}
presenceCtx, presenceCancel := context.WithCancel(ctx)
defer presenceCancel()
var beforeStart func(io.Writer)
if mode == types.SessionModeratorMode {
beforeStart = func(out io.Writer) {
nc.OnMFA = func() {
runPresenceTask(presenceCtx, out, site, tc, session.GetSessionID())
}
}
}
// running shell with a given session means "join" it:
err = tc.runShell(ctx, nc, mode, session, beforeStart)
return trace.Wrap(err)
}
// Play replays the recorded session
func (tc *TeleportClient) Play(ctx context.Context, namespace, sessionID string) (err error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/Play",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("session", sessionID),
),
)
defer span.End()
var sessionEvents []events.EventFields
var stream []byte
if namespace == "" {
return trace.BadParameter(auth.MissingNamespaceError)
}
sid, err := session.ParseID(sessionID)
if err != nil {
return fmt.Errorf("'%v' is not a valid session ID (must be GUID)", sid)
}
// connect to the auth server (site) who made the recording
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
site := proxyClient.CurrentCluster()
// request events for that session (to get timing data)
sessionEvents, err = site.GetSessionEvents(namespace, *sid, 0)
if err != nil {
return trace.Wrap(err)
}
// Return an error if it is a desktop session
if len(sessionEvents) > 0 {
if sessionEvents[0].GetType() == events.WindowsDesktopSessionStartEvent {
url := getDesktopEventWebURL(tc.localAgent.proxyHost, proxyClient.siteName, sid, sessionEvents)
message := "Desktop sessions cannot be viewed with tsh." +
" Please use the browser to play this session." +
" Click on the URL to view the session in the browser:"
return trace.BadParameter("%s\n%s", message, url)
}
}
// read the stream into a buffer:
for {
tmp, err := site.GetSessionChunk(namespace, *sid, len(stream), events.MaxChunkBytes)
if err != nil {
return trace.Wrap(err)
}
if len(tmp) == 0 {
break
}
stream = append(stream, tmp...)
}
return playSession(sessionEvents, stream)
}
func (tc *TeleportClient) GetSessionEvents(ctx context.Context, namespace, sessionID string) ([]events.EventFields, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetSessionEvents",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("session", sessionID),
),
)
defer span.End()
if namespace == "" {
return nil, trace.BadParameter(auth.MissingNamespaceError)
}
sid, err := session.ParseID(sessionID)
if err != nil {
return nil, trace.BadParameter("%q is not a valid session ID (must be GUID)", sid)
}
// connect to the auth server (site) who made the recording
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
site := proxyClient.CurrentCluster()
events, err := site.GetSessionEvents(namespace, *sid, 0)
if err != nil {
return nil, trace.Wrap(err)
}
return events, nil
}
// PlayFile plays the recorded session from a tar file
func PlayFile(ctx context.Context, tarFile io.Reader, sid string) error {
var sessionEvents []events.EventFields
var stream []byte
protoReader := events.NewProtoReader(tarFile)
playbackDir, err := os.MkdirTemp("", "playback")
if err != nil {
return trace.Wrap(err)
}
defer os.RemoveAll(playbackDir)
w, err := events.WriteForSSHPlayback(ctx, session.ID(sid), protoReader, playbackDir)
if err != nil {
return trace.Wrap(err)
}
sessionEvents, err = w.SessionEvents()
if err != nil {
return trace.Wrap(err)
}
stream, err = w.SessionChunks()
if err != nil {
return trace.Wrap(err)
}
return playSession(sessionEvents, stream)
}
// ExecuteSCP executes SCP command. It executes scp.Command using
// lower-level API integrations that mimic SCP CLI command behavior
func (tc *TeleportClient) ExecuteSCP(ctx context.Context, serverAddr string, cmd scp.Command) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ExecuteSCP",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
// connect to proxy first:
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
// We append the ":0" to tell the server to figure out the port for us.
NodeDetails{Addr: serverAddr + ":0", Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
tc.ExitStatus = 1
return trace.Wrap(err)
}
err = nodeClient.ExecuteSCP(ctx, cmd)
if err != nil {
// converts SSH error code to tc.ExitStatus
exitError, _ := trace.Unwrap(err).(*ssh.ExitError)
if exitError != nil {
tc.ExitStatus = exitError.ExitStatus()
}
return err
}
return nil
}
// SFTP securely copies files between Nodes or SSH servers using SFTP
func (tc *TeleportClient) SFTP(ctx context.Context, args []string, port int, opts sftp.Options, quiet bool) (err error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/SFTP",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
if len(args) < 2 {
return trace.Errorf("local and remote destinations are required")
}
first := args[0]
last := args[len(args)-1]
// local copy?
if !isRemoteDest(first) && !isRemoteDest(last) {
return trace.BadParameter("no remote destination specified")
}
var config *sftpConfig
if isRemoteDest(last) {
config, err = tc.uploadConfig(args, port, opts)
if err != nil {
return trace.Wrap(err)
}
} else {
config, err = tc.downloadConfig(args, port, opts)
if err != nil {
return trace.Wrap(err)
}
}
if config.hostLogin == "" {
config.hostLogin = tc.Config.HostLogin
}
if !quiet {
config.cfg.ProgressStream = func(fileInfo os.FileInfo) io.ReadWriter {
return sftp.NewProgressBar(fileInfo.Size(), fileInfo.Name(), tc.Stdout)
}
}
return trace.Wrap(tc.TransferFiles(ctx, config.hostLogin, config.addr, config.cfg))
}
type sftpConfig struct {
cfg *sftp.Config
addr string
hostLogin string
}
func (tc *TeleportClient) uploadConfig(args []string, port int, opts sftp.Options) (*sftpConfig, error) {
// args are guaranteed to have len(args) > 1
srcPaths := args[:len(args)-1]
// copy everything except the last arg (the destination)
dstPath := args[len(args)-1]
dst, addr, err := getSCPDestination(dstPath, port)
if err != nil {
return nil, trace.Wrap(err)
}
cfg, err := sftp.CreateUploadConfig(srcPaths, dst.Path, opts)
if err != nil {
return nil, trace.Wrap(err)
}
return &sftpConfig{
cfg: cfg,
addr: addr,
hostLogin: dst.Login,
}, nil
}
func (tc *TeleportClient) downloadConfig(args []string, port int, opts sftp.Options) (*sftpConfig, error) {
if len(args) > 2 {
return nil, trace.BadParameter("only one source file is supported when downloading files")
}
// args are guaranteed to have len(args) > 1
src, addr, err := getSCPDestination(args[0], port)
if err != nil {
return nil, trace.Wrap(err)
}
cfg, err := sftp.CreateDownloadConfig(src.Path, args[1], opts)
if err != nil {
return nil, trace.Wrap(err)
}
return &sftpConfig{
cfg: cfg,
addr: addr,
hostLogin: src.Login,
}, nil
}
func getSCPDestination(target string, port int) (dest *scp.Destination, addr string, err error) {
dest, err = scp.ParseSCPDestination(target)
if err != nil {
return nil, "", trace.Wrap(err)
}
addr = net.JoinHostPort(dest.Host.Host(), strconv.Itoa(port))
return dest, addr, nil
}
// TransferFiles copies files between the current machine and the
// specified Node using the supplied config
func (tc *TeleportClient) TransferFiles(ctx context.Context, hostLogin, nodeAddr string, cfg *sftp.Config) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/TransferFiles",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
if hostLogin == "" {
return trace.BadParameter("host login is not specified")
}
if nodeAddr == "" {
return trace.BadParameter("node address is not specified")
}
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
client, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddr, Namespace: tc.Namespace, Cluster: tc.SiteName},
hostLogin,
)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(client.TransferFiles(ctx, cfg))
}
func isRemoteDest(name string) bool {
return strings.ContainsRune(name, ':')
}
// ListNodesWithFilters returns a list of nodes connected to a proxy
func (tc *TeleportClient) ListNodesWithFilters(ctx context.Context) ([]types.Server, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListNodesWithFilters",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
// connect to the proxy and ask it to return a full list of servers
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
servers, err := proxyClient.FindNodesByFilters(ctx, *tc.DefaultResourceFilter())
if err != nil {
return nil, trace.Wrap(err)
}
return servers, nil
}
// GetClusterAlerts returns a list of matching alerts from the current cluster.
func (tc *TeleportClient) GetClusterAlerts(ctx context.Context, req types.GetClusterAlertsRequest) ([]types.ClusterAlert, error) {
ctx, span := tc.Tracer.Start(ctx,
"teleportClient/GetClusterAlerts",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
alerts, err := proxyClient.GetClusterAlerts(ctx, req)
return alerts, trace.Wrap(err)
}
// ListNodesWithFiltersAllClusters returns a map of all nodes in all clusters connected to this proxy.
func (tc *TeleportClient) ListNodesWithFiltersAllClusters(ctx context.Context) (map[string][]types.Server, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
clusters, err := proxyClient.GetSites(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
servers := make(map[string][]types.Server, len(clusters))
for _, cluster := range clusters {
s, err := proxyClient.FindNodesByFiltersForCluster(ctx, *tc.DefaultResourceFilter(), cluster.Name)
if err != nil {
return nil, trace.Wrap(err)
}
servers[cluster.Name] = s
}
return servers, nil
}
// ListAppServersWithFilters returns a list of application servers.
func (tc *TeleportClient) ListAppServersWithFilters(ctx context.Context, customFilter *proto.ListResourcesRequest) ([]types.AppServer, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListAppServersWithFilters",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
filter := customFilter
if filter == nil {
filter = tc.DefaultResourceFilter()
}
servers, err := proxyClient.FindAppServersByFilters(ctx, *filter)
if err != nil {
return nil, trace.Wrap(err)
}
return servers, nil
}
// listAppServersWithFiltersAllClusters returns a map of all app servers in all clusters connected to this proxy.
func (tc *TeleportClient) listAppServersWithFiltersAllClusters(ctx context.Context, customFilter *proto.ListResourcesRequest) (map[string][]types.AppServer, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
filter := customFilter
if customFilter == nil {
filter = tc.DefaultResourceFilter()
}
clusters, err := proxyClient.GetSites(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
servers := make(map[string][]types.AppServer, len(clusters))
for _, cluster := range clusters {
s, err := proxyClient.FindAppServersByFiltersForCluster(ctx, *filter, cluster.Name)
if err != nil {
return nil, trace.Wrap(err)
}
servers[cluster.Name] = s
}
return servers, nil
}
// ListApps returns all registered applications.
func (tc *TeleportClient) ListApps(ctx context.Context, customFilter *proto.ListResourcesRequest) ([]types.Application, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListApps",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
servers, err := tc.ListAppServersWithFilters(ctx, customFilter)
if err != nil {
return nil, trace.Wrap(err)
}
var apps []types.Application
for _, server := range servers {
apps = append(apps, server.GetApp())
}
return types.DeduplicateApps(apps), nil
}
// ListAppsAllClusters returns all registered applications across all clusters.
func (tc *TeleportClient) ListAppsAllClusters(ctx context.Context, customFilter *proto.ListResourcesRequest) (map[string][]types.Application, error) {
serversByCluster, err := tc.listAppServersWithFiltersAllClusters(ctx, customFilter)
if err != nil {
return nil, trace.Wrap(err)
}
clusters := make(map[string][]types.Application, len(serversByCluster))
for cluster, servers := range serversByCluster {
var apps []types.Application
for _, server := range servers {
apps = append(apps, server.GetApp())
}
clusters[cluster] = types.DeduplicateApps(apps)
}
return clusters, nil
}
// CreateAppSession creates a new application access session.
func (tc *TeleportClient) CreateAppSession(ctx context.Context, req types.CreateAppSessionRequest) (types.WebSession, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/CreateAppSession",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.CreateAppSession(ctx, req)
}
// GetAppSession returns an existing application access session.
func (tc *TeleportClient) GetAppSession(ctx context.Context, req types.GetAppSessionRequest) (types.WebSession, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetAppSession",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("session", req.SessionID),
),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.GetAppSession(ctx, req)
}
// DeleteAppSession removes the specified application access session.
func (tc *TeleportClient) DeleteAppSession(ctx context.Context, sessionID string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/DeleteAppSession",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.DeleteAppSession(ctx, sessionID)
}
// ListDatabaseServersWithFilters returns all registered database proxy servers.
func (tc *TeleportClient) ListDatabaseServersWithFilters(ctx context.Context, customFilter *proto.ListResourcesRequest) ([]types.DatabaseServer, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListDatabaseServersWithFilters",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
filter := customFilter
if filter == nil {
filter = tc.DefaultResourceFilter()
}
servers, err := proxyClient.FindDatabaseServersByFilters(ctx, *filter)
if err != nil {
return nil, trace.Wrap(err)
}
return servers, nil
}
// listDatabaseServersWithFilters returns all registered database proxy servers across all clusters.
func (tc *TeleportClient) listDatabaseServersWithFiltersAllClusters(ctx context.Context, customFilter *proto.ListResourcesRequest) (map[string][]types.DatabaseServer, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
filter := customFilter
if customFilter == nil {
filter = tc.DefaultResourceFilter()
}
clusters, err := proxyClient.GetSites(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
servers := make(map[string][]types.DatabaseServer, len(clusters))
for _, cluster := range clusters {
s, err := proxyClient.FindDatabaseServersByFiltersForCluster(ctx, *filter, cluster.Name)
if err != nil {
return nil, trace.Wrap(err)
}
servers[cluster.Name] = s
}
return servers, nil
}
// ListDatabases returns all registered databases.
func (tc *TeleportClient) ListDatabases(ctx context.Context, customFilter *proto.ListResourcesRequest) ([]types.Database, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListDatabases",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
servers, err := tc.ListDatabaseServersWithFilters(ctx, customFilter)
if err != nil {
return nil, trace.Wrap(err)
}
var databases []types.Database
for _, server := range servers {
databases = append(databases, server.GetDatabase())
}
return types.DeduplicateDatabases(databases), nil
}
// ListDatabasesAllClusters returns all registered databases across all clusters.
func (tc *TeleportClient) ListDatabasesAllClusters(ctx context.Context, customFilter *proto.ListResourcesRequest) (map[string][]types.Database, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListDatabasesAllClusters",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
serversByCluster, err := tc.listDatabaseServersWithFiltersAllClusters(ctx, customFilter)
if err != nil {
return nil, trace.Wrap(err)
}
clusters := make(map[string][]types.Database, len(serversByCluster))
for cluster, servers := range serversByCluster {
var databases []types.Database
for _, server := range servers {
databases = append(databases, server.GetDatabase())
}
clusters[cluster] = types.DeduplicateDatabases(databases)
}
return clusters, nil
}
// ListAllNodes is the same as ListNodes except that it ignores labels.
func (tc *TeleportClient) ListAllNodes(ctx context.Context) ([]types.Server, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListAllNodes",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.FindNodesByFilters(ctx, proto.ListResourcesRequest{
Namespace: tc.Namespace,
})
}
// ListKubernetesClustersWithFiltersAllClusters returns a map of all kube clusters in all clusters connected to a proxy.
func (tc *TeleportClient) ListKubernetesClustersWithFiltersAllClusters(ctx context.Context, req proto.ListResourcesRequest) (map[string][]types.KubeCluster, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ListKubernetesClustersWithFiltersAllClusters",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
pc, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
clusters, err := pc.GetSites(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
kubeClusters := make(map[string][]types.KubeCluster, 0)
for _, cluster := range clusters {
ac, err := pc.ConnectToCluster(ctx, cluster.Name)
if err != nil {
return nil, trace.Wrap(err)
}
kc, err := kubeutils.ListKubeClustersWithFilters(ctx, ac, req)
if err != nil {
return nil, trace.Wrap(err)
}
kubeClusters[cluster.Name] = kc
}
return kubeClusters, nil
}
// roleGetter retrieves roles for the current user
type roleGetter interface {
GetRoles(ctx context.Context) ([]types.Role, error)
}
// commandLimit determines how many commands may be executed in parallel.
// The limit will one of the following:
// - 1 if per session mfa is required
// - 1 if we cannot determine the users role set
// - half the max connection limit defined by the users role set
//
// Out of an abundance of caution we only use half the max connection
// limit to allow other connections to be established.
func commandLimit(ctx context.Context, getter roleGetter, mfaRequired bool) int {
if mfaRequired {
return 1
}
roles, err := getter.GetRoles(ctx)
if err != nil {
return 1
}
max := services.NewRoleSet(roles...).MaxConnections()
limit := max / 2
switch {
case max == 0:
return -1
case max == 1:
return 1
case limit <= 0:
return 1
default:
return int(limit)
}
}
// runCommandOnNodes executes a given bash command on a bunch of remote nodes.
func (tc *TeleportClient) runCommandOnNodes(ctx context.Context, siteName string, nodeAddresses []string, proxyClient *ProxyClient, command []string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runCommandOnNodes",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
clt, err := proxyClient.ConnectToCluster(ctx, siteName)
if err != nil {
return trace.Wrap(err)
}
defer clt.Close()
// Let's check if the first node requires mfa.
// If it's required, run commands sequentially to avoid
// race conditions and weird ux during mfa.
mfaRequiredCheck, err := clt.IsMFARequired(ctx, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{
Node: &proto.NodeLogin{
Node: nodeName(nodeAddresses[0]),
Login: proxyClient.hostLogin,
},
},
})
if err != nil {
return trace.Wrap(err)
}
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(commandLimit(ctx, clt, mfaRequiredCheck.Required))
for _, address := range nodeAddresses {
address := address
g.Go(func() error {
ctx, span := tc.Tracer.Start(
gctx,
"teleportClient/executingCommand",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("node", address)),
)
defer span.End()
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{
Addr: address,
Namespace: tc.Namespace,
Cluster: siteName,
MFACheck: mfaRequiredCheck,
},
tc.Config.HostLogin,
)
if err != nil {
fmt.Fprintln(tc.Stderr, err)
return trace.Wrap(err)
}
defer nodeClient.Close()
fmt.Printf("Running command on %v:\n", nodeName(address))
return trace.Wrap(tc.runCommand(ctx, nodeClient, command))
})
}
return trace.Wrap(g.Wait())
}
// runCommand executes a given bash command on an established NodeClient.
func (tc *TeleportClient) runCommand(ctx context.Context, nodeClient *NodeClient, command []string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runCommand",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
nodeSession, err := newSession(ctx, nodeClient, nil, tc.newSessionEnv(), tc.Stdin, tc.Stdout, tc.Stderr, tc.EnableEscapeSequences)
if err != nil {
return trace.Wrap(err)
}
defer nodeSession.Close()
if err := nodeSession.runCommand(ctx, types.SessionPeerMode, command, tc.OnShellCreated, tc.Config.InteractiveCommand); err != nil {
originErr := trace.Unwrap(err)
exitErr, ok := originErr.(*ssh.ExitError)
if ok {
tc.ExitStatus = exitErr.ExitStatus()
} else {
// if an error occurs, but no exit status is passed back, GoSSH returns
// a generic error like this. in this case the error message is printed
// to stderr by the remote process so we have to quietly return 1:
if strings.Contains(originErr.Error(), "exited without exit status") {
tc.ExitStatus = 1
}
}
return trace.Wrap(err)
}
return nil
}
func (tc *TeleportClient) newSessionEnv() map[string]string {
env := map[string]string{
teleport.SSHSessionWebproxyAddr: tc.WebProxyAddr,
}
if tc.SessionID != "" {
env[sshutils.SessionEnvVar] = tc.SessionID
}
return env
}
// runShell starts an interactive SSH session/shell.
// sessionID : when empty, creates a new shell. otherwise it tries to join the existing session.
func (tc *TeleportClient) runShell(ctx context.Context, nodeClient *NodeClient, mode types.SessionParticipantMode, sessToJoin types.SessionTracker, beforeStart func(io.Writer)) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runShell",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
env := tc.newSessionEnv()
env[teleport.EnvSSHJoinMode] = string(mode)
env[teleport.EnvSSHSessionReason] = tc.Config.Reason
env[teleport.EnvSSHSessionDisplayParticipantRequirements] = strconv.FormatBool(tc.Config.DisplayParticipantRequirements)
encoded, err := json.Marshal(&tc.Config.Invited)
if err != nil {
return trace.Wrap(err)
}
env[teleport.EnvSSHSessionInvited] = string(encoded)
nodeSession, err := newSession(ctx, nodeClient, sessToJoin, env, tc.Stdin, tc.Stdout, tc.Stderr, tc.EnableEscapeSequences)
if err != nil {
return trace.Wrap(err)
}
if err = nodeSession.runShell(ctx, mode, beforeStart, tc.OnShellCreated); err != nil {
switch e := trace.Unwrap(err).(type) {
case *ssh.ExitError:
tc.ExitStatus = e.ExitStatus()
case *ssh.ExitMissingError:
tc.ExitStatus = 1
}
return trace.Wrap(err)
}
if nodeSession.ExitMsg == "" {
fmt.Fprintln(tc.Stderr, "the connection was closed on the remote side on ", time.Now().Format(time.RFC822))
} else {
fmt.Fprintln(tc.Stderr, nodeSession.ExitMsg)
}
return nil
}
// getProxyLogin determines which SSH principal to use when connecting to proxy.
func (tc *TeleportClient) getProxySSHPrincipal() string {
if tc.ProxySSHPrincipal != "" {
return tc.ProxySSHPrincipal
}
// if we have any keys in the cache, pull the user's valid principals from it.
if tc.localAgent != nil {
signers, err := tc.localAgent.Signers()
if err == nil && len(signers) > 0 {
cert, ok := signers[0].PublicKey().(*ssh.Certificate)
if ok && len(cert.ValidPrincipals) > 0 {
return cert.ValidPrincipals[0]
}
}
}
proxyPrincipal := tc.Config.HostLogin
if len(tc.JumpHosts) > 0 && tc.JumpHosts[0].Username != "" {
log.Debugf("Setting proxy login to jump host's parameter user %q", tc.JumpHosts[0].Username)
proxyPrincipal = tc.JumpHosts[0].Username
}
return proxyPrincipal
}
const unconfiguredPublicAddrMsg = `WARNING:
The following error has occurred as Teleport does not recognize the address
that is being used to connect to it. This usually indicates that the
'public_addr' configuration option of the 'proxy_service' has not been
set to match the address you are hosting the proxy on.
If 'public_addr' is configured correctly, this could be an indicator of an
attempted man-in-the-middle attack.
`
// formatConnectToProxyErr adds additional user actionable advice to errors
// that are raised during ConnectToProxy.
func formatConnectToProxyErr(err error) error {
if err == nil {
return nil
}
// Handles the error that occurs when you connect to the Proxy SSH service
// and the Proxy does not have a correct `public_addr` configured, and the
// system is configured with non-multiplexed ports.
if utils.IsHandshakeFailedError(err) {
const principalStr = "not in the set of valid principals for given certificate"
if strings.Contains(err.Error(), principalStr) {
return trace.Wrap(err, unconfiguredPublicAddrMsg)
}
}
return err
}
// ConnectToProxyFunc is used in tests to override connection to proxy function.
type ConnectToProxyFunc func(ctx context.Context) (*ProxyClient, error)
// ConnectToProxy will dial to the proxy server and return a ProxyClient when
// successful. If the passed in context is canceled, this function will return
// a trace.ConnectionProblem right away.
func (tc *TeleportClient) ConnectToProxy(ctx context.Context) (*ProxyClient, error) {
if tc.Config.MockConnectToProxy != nil {
return tc.Config.MockConnectToProxy(ctx)
}
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ConnectToProxy",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("proxy", tc.Config.WebProxyAddr),
),
)
defer span.End()
var err error
var proxyClient *ProxyClient
// Use connectContext and the cancel function to signal when a response is
// returned from connectToProxy.
connectContext, cancel := context.WithCancel(ctx)
go func() {
defer cancel()
proxyClient, err = tc.connectToProxy(ctx)
}()
select {
// ConnectToProxy returned a result, return that back to the caller.
case <-connectContext.Done():
return proxyClient, trace.Wrap(formatConnectToProxyErr(err))
// The passed in context timed out. This is often due to the network being
// down and the user hitting Ctrl-C.
case <-ctx.Done():
return nil, trace.ConnectionProblem(ctx.Err(), "connection canceled")
}
}
// connectToProxy will dial to the proxy server and return a ProxyClient when
// successful.
func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, error) {
sshProxyAddr := tc.Config.SSHProxyAddr
hostKeyCallback := tc.HostKeyCallback
authMethods := append([]ssh.AuthMethod{}, tc.Config.AuthMethods...)
clusterName := func() string { return tc.SiteName }
if len(tc.JumpHosts) > 0 {
log.Debugf("Overriding SSH proxy to JumpHosts's address %q", tc.JumpHosts[0].Addr.String())
sshProxyAddr = tc.JumpHosts[0].Addr.Addr
if tc.localAgent != nil {
// Wrap host key and auth callbacks using clusterGuesser.
//
// clusterGuesser will use the host key callback to guess the target
// cluster based on the host certificate. It will then use the auth
// callback to load the appropriate SSH certificate for that cluster.
clusterGuesser := newProxyClusterGuesser(hostKeyCallback, tc.SignersForClusterWithReissue)
hostKeyCallback = clusterGuesser.hostKeyCallback
authMethods = append(authMethods, clusterGuesser.authMethod(ctx))
rootClusterName, err := tc.rootClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
clusterName = func() string {
// Only return the inferred cluster name if it's not the root
// cluster. If it's the root cluster proxy, tc.SiteName could
// be pointing at a leaf cluster and we don't want to override
// that.
if clusterGuesser.clusterName != rootClusterName {
return clusterGuesser.clusterName
}
return tc.SiteName
}
}
} else if tc.localAgent != nil {
// Load SSH certs for all clusters we have, in case we don't yet
// have a certificate for tc.SiteName (like during `tsh login leaf`).
signers, err := tc.localAgent.Signers()
// errNoLocalKeyStore is returned when running in the proxy. The proxy
// should be passing auth methods via tc.Config.AuthMethods.
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
if len(signers) > 0 {
authMethods = append(authMethods, ssh.PublicKeys(signers...))
}
}
if len(authMethods) == 0 {
return nil, trace.BadParameter("no SSH auth methods loaded, are you logged in?")
}
sshConfig := &ssh.ClientConfig{
User: tc.getProxySSHPrincipal(),
HostKeyCallback: hostKeyCallback,
Auth: authMethods,
}
sshClient, err := makeProxySSHClient(ctx, tc, sshConfig)
if err != nil {
return nil, trace.Wrap(err)
}
pc := &ProxyClient{
teleportClient: tc,
Client: sshClient,
proxyAddress: sshProxyAddr,
proxyPrincipal: sshConfig.User,
hostKeyCallback: sshConfig.HostKeyCallback,
authMethods: sshConfig.Auth,
hostLogin: tc.HostLogin,
siteName: clusterName(),
clientAddr: tc.ClientAddr,
Tracer: tc.Tracer,
}
// Create the auth.ClientI for the local auth server
// once per ProxyClient. This is an inexpensive
// operation since the actual dialing of the auth
// server is lazy - meaning it won't happen until the
// first use of the auth.ClientI. By establishing
// the auth.ClientI here we can ensure that any connections
// to the local cluster will end up reusing this auth.ClientI
// for the lifespan of the ProxyClient.
clt, err := pc.ConnectToCluster(ctx, pc.siteName)
if err != nil {
return nil, trace.Wrap(err)
}
pc.currentCluster = clt
return pc, nil
}
// confirmSSHConnectivityErrorMsg returns the error message for Proxy SSH
// connectivity errors.
func confirmSSHConnectivityErrorMsg(proxyAddress string) string {
return fmt.Sprintf("Unable to connect to ssh proxy at %v. "+
"Confirm connectivity and availability.", proxyAddress)
}
// makeProxySSHClient creates an SSH client by following steps:
// 1. If the current proxy supports TLS Routing and JumpHost address was not provided use TLSWrapper.
// 2. Check JumpHost raw SSH port or Teleport proxy address.
// In case of proxy web address check if the proxy supports TLS Routing and connect to the proxy with TLSWrapper
// 3. Dial sshProxyAddr with raw SSH Dialer where sshProxyAddress is proxy ssh address or JumpHost address if
// JumpHost address was provided.
func makeProxySSHClient(ctx context.Context, tc *TeleportClient, sshConfig *ssh.ClientConfig) (*tracessh.Client, error) {
// Use TLS Routing dialer only if proxy support TLS Routing and JumpHost was not set.
if tc.Config.TLSRoutingEnabled && len(tc.JumpHosts) == 0 {
log.Infof("Connecting to proxy=%v login=%q using TLS Routing", tc.Config.WebProxyAddr, sshConfig.User)
c, err := makeProxySSHClientWithTLSWrapper(ctx, tc, sshConfig, tc.Config.WebProxyAddr)
if err != nil {
return nil, trace.Wrap(err, confirmSSHConnectivityErrorMsg(tc.Config.WebProxyAddr))
}
log.Infof("Successful auth with proxy %v.", tc.Config.WebProxyAddr)
return c, nil
}
sshProxyAddr := tc.Config.SSHProxyAddr
// Handle situation where a Jump Host was set to proxy web address and Teleport supports TLS Routing.
if len(tc.JumpHosts) > 0 {
sshProxyAddr = tc.JumpHosts[0].Addr.Addr
// Check if JumpHost address is a proxy web address.
resp, err := webclient.Find(&webclient.Config{
Context: ctx,
ProxyAddr: sshProxyAddr,
Insecure: tc.InsecureSkipVerify,
ExtraHeaders: tc.ExtraProxyHeaders,
})
// If JumpHost address is a proxy web port and proxy supports TLSRouting dial proxy with TLSWrapper.
if err == nil && resp.Proxy.TLSRoutingEnabled {
log.Infof("Connecting to proxy=%v login=%q using TLS Routing JumpHost", sshProxyAddr, sshConfig.User)
c, err := makeProxySSHClientWithTLSWrapper(ctx, tc, sshConfig, sshProxyAddr)
if err != nil {
return nil, trace.Wrap(err, confirmSSHConnectivityErrorMsg(tc.Config.WebProxyAddr))
}
log.Infof("Successful auth with proxy %v.", sshProxyAddr)
return c, nil
}
}
log.Infof("Connecting to proxy=%v login=%q", sshProxyAddr, sshConfig.User)
client, err := makeProxySSHClientDirect(ctx, tc, sshConfig, sshProxyAddr)
if err != nil {
if utils.IsHandshakeFailedError(err) {
return nil, trace.AccessDenied(confirmSSHConnectivityErrorMsg(sshProxyAddr)+" Error: %v", err)
}
return nil, trace.Wrap(err, confirmSSHConnectivityErrorMsg(sshProxyAddr))
}
log.Infof("Successful auth with proxy %v.", sshProxyAddr)
return client, nil
}
func makeProxySSHClientDirect(ctx context.Context, tc *TeleportClient, sshConfig *ssh.ClientConfig, proxyAddr string) (*tracessh.Client, error) {
dialer := proxy.DialerFromEnvironment(tc.Config.SSHProxyAddr)
return dialer.Dial(ctx, "tcp", proxyAddr, sshConfig)
}
func makeProxySSHClientWithTLSWrapper(ctx context.Context, tc *TeleportClient, sshConfig *ssh.ClientConfig, proxyAddr string) (*tracessh.Client, error) {
tlsConfig, err := tc.LoadTLSConfig()
if err != nil {
return nil, trace.Wrap(err)
}
tlsConfig.NextProtos = []string{string(alpncommon.ProtocolProxySSH)}
dialer := proxy.DialerFromEnvironment(tc.Config.WebProxyAddr, proxy.WithALPNDialer(tlsConfig))
return dialer.Dial(ctx, "tcp", proxyAddr, sshConfig)
}
func (tc *TeleportClient) rootClusterName() (string, error) {
if tc.localAgent == nil {
return "", trace.NotFound("cannot load root cluster name without local agent")
}
tlsKey, err := tc.localAgent.GetCoreKey()
if err != nil {
return "", trace.Wrap(err)
}
rootClusterName, err := tlsKey.RootClusterName()
if err != nil {
return "", trace.Wrap(err)
}
return rootClusterName, nil
}
// proxyClusterGuesser matches client SSH certificates to the target cluster of
// an SSH proxy. It uses an ssh.HostKeyCallback to infer the cluster name from
// the proxy host certificate. It then passes that name to signersForCluster to
// get the SSH certificates for that cluster.
type proxyClusterGuesser struct {
clusterName string
nextHostKeyCallback ssh.HostKeyCallback
signersForCluster func(context.Context, string) ([]ssh.Signer, error)
}
func newProxyClusterGuesser(nextHostKeyCallback ssh.HostKeyCallback, signersForCluster func(context.Context, string) ([]ssh.Signer, error)) *proxyClusterGuesser {
return &proxyClusterGuesser{
nextHostKeyCallback: nextHostKeyCallback,
signersForCluster: signersForCluster,
}
}
func (g *proxyClusterGuesser) hostKeyCallback(hostname string, remote net.Addr, key ssh.PublicKey) error {
cert, ok := key.(*ssh.Certificate)
if !ok {
return trace.BadParameter("remote proxy did not present a host certificate")
}
g.clusterName = cert.Permissions.Extensions[utils.CertExtensionAuthority]
if g.clusterName == "" {
log.Debugf("Target SSH server %q does not have a cluster name embedded in their certificate; will use all available client certificates to authenticate", hostname)
}
if g.nextHostKeyCallback != nil {
return g.nextHostKeyCallback(hostname, remote, key)
}
return nil
}
func (g *proxyClusterGuesser) authMethod(ctx context.Context) ssh.AuthMethod {
return ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
return g.signersForCluster(ctx, g.clusterName)
})
}
// WithoutJumpHosts executes the given function with a Teleport client that has
// no JumpHosts set, i.e. presumably falling back to the proxy specified in the
// profile.
func (tc *TeleportClient) WithoutJumpHosts(fn func(tcNoJump *TeleportClient) error) error {
storedJumpHosts := tc.JumpHosts
tc.JumpHosts = nil
err := fn(tc)
tc.JumpHosts = storedJumpHosts
return trace.Wrap(err)
}
// Logout removes certificate and key for the currently logged in user from
// the filesystem and agent.
func (tc *TeleportClient) Logout() error {
if tc.localAgent == nil {
return nil
}
return tc.localAgent.DeleteKey()
}
// LogoutDatabase removes certificate for a particular database.
func (tc *TeleportClient) LogoutDatabase(dbName string) error {
if tc.localAgent == nil {
return nil
}
if tc.SiteName == "" {
return trace.BadParameter("cluster name must be set for database logout")
}
if dbName == "" {
return trace.BadParameter("please specify database name to log out of")
}
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithDBCerts{dbName})
}
// LogoutApp removes certificate for the specified app.
func (tc *TeleportClient) LogoutApp(appName string) error {
if tc.localAgent == nil {
return nil
}
if tc.SiteName == "" {
return trace.BadParameter("cluster name must be set for app logout")
}
if appName == "" {
return trace.BadParameter("please specify app name to log out of")
}
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithAppCerts{appName})
}
// LogoutAll removes all certificates for all users from the filesystem
// and agent.
func (tc *TeleportClient) LogoutAll() error {
if tc.localAgent == nil {
return nil
}
if err := tc.localAgent.DeleteKeys(); err != nil {
return trace.Wrap(err)
}
return nil
}
// PingAndShowMOTD pings the Teleport Proxy and displays the Message Of The Day if it's available.
func (tc *TeleportClient) PingAndShowMOTD(ctx context.Context) (*webclient.PingResponse, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/PingAndShowMOTD",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
pr, err := tc.Ping(ctx)
if err != nil {
return nil, trace.Wrap(err, "Teleport proxy not available at %s.", tc.WebProxyAddr)
}
if pr.Auth.HasMessageOfTheDay {
err = tc.ShowMOTD(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}
return pr, nil
}
// GetWebConfig retrieves Teleport proxy web config
func (tc *TeleportClient) GetWebConfig(ctx context.Context) (*webclient.WebConfig, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetWebConfig",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
cfg, err := GetWebConfig(ctx, tc.WebProxyAddr, tc.InsecureSkipVerify)
if err != nil {
return nil, trace.Wrap(err)
}
return cfg, nil
}
// Login logs the user into a Teleport cluster by talking to a Teleport proxy.
//
// The returned Key should typically be passed to ActivateKey in order to
// update local agent state.
//
// If the initial login fails due to a private key policy not being met, Login
// will automatically retry login with a private key that meets the required policy.
// This will initiate the same login flow again, aka prompt for password/otp/sso/mfa.
func (tc *TeleportClient) Login(ctx context.Context) (*Key, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/Login",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
// Ping the endpoint to see if it's up and find the type of authentication
// supported, also show the message of the day if available.
pr, err := tc.PingAndShowMOTD(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Get the SSHLoginFunc that matches client and cluster settings.
sshLoginFunc, err := tc.getSSHLoginFunc(pr)
if err != nil {
return nil, trace.Wrap(err)
}
key, err := tc.SSHLogin(ctx, sshLoginFunc)
if err != nil {
return nil, trace.Wrap(err)
}
// Use proxy identity if set in key response.
if key.Username != "" {
tc.Username = key.Username
if tc.localAgent != nil {
tc.localAgent.username = key.Username
}
}
return key, nil
}
// AttemptDeviceLogin attempts device authentication for the current device.
// It expects to receive the latest activated key, as acquired via
// [TeleportClient.Login], and augments the certificates within the key with
// device extensions.
//
// If successful, the new device certificates are automatically activated (using
// [TeleportClient.ActivateKey].)
//
// A nil response from this method doesn't mean that device authentication was
// successful, as skipping the ceremony is valid for various reasons (Teleport
// cluster doesn't support device authn, device wasn't enrolled, etc).
// Use [TeleportClient.DeviceLogin] if you want more control over process.
func (tc *TeleportClient) AttemptDeviceLogin(ctx context.Context, key *Key) error {
pingResp, err := tc.Ping(ctx)
if err != nil {
return trace.Wrap(err)
}
if !tc.dtAttemptLoginIgnorePing && pingResp.Auth.DeviceTrustDisabled {
log.Debug("Device Trust: skipping device authentication, device trust disabled")
return nil
}
newCerts, err := tc.DeviceLogin(ctx, &devicepb.UserCertificates{
// Augment the SSH certificate.
// The TLS certificate is already part of the connection.
SshAuthorizedKey: key.Cert,
})
if err != nil {
log.WithError(err).Debug("Device Trust: device authentication failed")
return nil // Swallowed on purpose.
}
log.Debug("Device Trust: acquired augmented user certificates")
cp := *key
cp.Cert = newCerts.SshAuthorizedKey
cp.TLSCert = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: newCerts.X509Der,
})
return trace.Wrap(tc.ActivateKey(ctx, &cp))
}
// DeviceLogin attempts to authenticate the current device with Teleport.
// The device must be previously registered and enrolled for the authentication
// to succeed (see `tsh device enroll`).
//
// DeviceLogin may fail for a variety of reasons, some of them legitimate
// (non-Enterprise cluster, Device Trust is disabled, etc). Because of that, a
// failure in this method may not warrant failing a broader action (for example,
// `tsh login`).
//
// Device Trust is a Teleport Enterprise feature.
func (tc *TeleportClient) DeviceLogin(ctx context.Context, certs *devicepb.UserCertificates) (*devicepb.UserCertificates, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
authClient, err := proxyClient.ConnectToRootCluster(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Allow tests to override the default authn function.
runCeremony := tc.dtAuthnRunCeremony
if runCeremony == nil {
runCeremony = dtauthn.RunCeremony
}
newCerts, err := runCeremony(ctx, authClient.DevicesClient(), certs)
if err != nil {
return nil, trace.Wrap(err)
}
return newCerts, nil
}
// getSSHLoginFunc returns an SSHLoginFunc that matches client and cluster settings.
func (tc *TeleportClient) getSSHLoginFunc(pr *webclient.PingResponse) (SSHLoginFunc, error) {
switch pr.Auth.Type {
case constants.Local:
switch pr.Auth.Local.Name {
case constants.PasswordlessConnector:
// Sanity check settings.
if !pr.Auth.AllowPasswordless {
return nil, trace.BadParameter("passwordless disallowed by cluster settings")
}
return tc.pwdlessLogin, nil
case constants.HeadlessConnector:
// Sanity check settings.
if !pr.Auth.AllowHeadless {
return nil, trace.BadParameter("headless disallowed by cluster settings")
}
if tc.AllowHeadless {
return func(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
return tc.headlessLogin(ctx, priv)
}, nil
}
log.Debug("Headless login is disabled for this command. Only 'tsh ls', 'tsh ssh', and 'tsh scp' are supported. Defaulting to local authentication methods.")
fallthrough
case constants.LocalConnector, "":
// if passwordless is enabled and there are passwordless credentials
// registered, we can try to go with passwordless login even though
// auth=local was selected.
if tc.canDefaultToPasswordless(pr) {
log.Debug("Trying passwordless login because credentials were found")
return tc.pwdlessLogin, nil
}
return func(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
return tc.localLogin(ctx, priv, pr.Auth.SecondFactor)
}, nil
default:
return nil, trace.BadParameter("unsupported authentication connector type: %q", pr.Auth.Local.Name)
}
case constants.OIDC:
return func(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
return tc.ssoLogin(ctx, priv, pr.Auth.OIDC.Name, constants.OIDC)
}, nil
case constants.SAML:
return func(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
return tc.ssoLogin(ctx, priv, pr.Auth.SAML.Name, constants.SAML)
}, nil
case constants.Github:
return func(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
return tc.ssoLogin(ctx, priv, pr.Auth.Github.Name, constants.Github)
}, nil
default:
return nil, trace.BadParameter("unsupported authentication type: %q", pr.Auth.Type)
}
}
// hasTouchIDCredentials provides indirection for tests.
var hasTouchIDCredentials = touchid.HasCredentials
// canDefaultToPasswordless checks without user interaction
// if there is any registered passwordless login.
func (tc *TeleportClient) canDefaultToPasswordless(pr *webclient.PingResponse) bool {
// Verify if client flags are compatible with passwordless.
allowedConnector := tc.AuthConnector == ""
allowedAttachment := tc.AuthenticatorAttachment == wancli.AttachmentAuto || tc.AuthenticatorAttachment == wancli.AttachmentPlatform
if !allowedConnector || !allowedAttachment || tc.PreferOTP {
return false
}
// Verify if server is compatible with passwordless.
if !pr.Auth.AllowPasswordless || pr.Auth.Webauthn == nil {
return false
}
// Only pass on the user if explicitly set, otherwise let the credential
// picker kick in.
user := ""
if tc.ExplicitUsername {
user = tc.Username
}
return hasTouchIDCredentials(pr.Auth.Webauthn.RPID, user)
}
// SSHLoginFunc is a function which carries out authn with an auth server and returns an auth response.
type SSHLoginFunc func(context.Context, *keys.PrivateKey) (*auth.SSHLoginResponse, error)
// SSHLogin uses the given login function to login the client. This function handles
// private key logic and parsing the resulting auth response.
func (tc *TeleportClient) SSHLogin(ctx context.Context, sshLoginFunc SSHLoginFunc) (*Key, error) {
priv, err := tc.GetNewLoginKey(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := sshLoginFunc(ctx, priv)
if err != nil {
// check if the error is a private key policy error, and relogin if it is.
if privateKeyPolicy, parseErr := keys.ParsePrivateKeyPolicyError(err); parseErr == nil {
// The current private key was rejected due to an unmet key policy requirement.
fmt.Fprintf(tc.Stderr, "Unmet private key policy %q.\n", privateKeyPolicy)
// Set the private key policy to the expected value and re-login.
tc.PrivateKeyPolicy = privateKeyPolicy
priv, err = tc.GetNewLoginKey(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
fmt.Fprintf(tc.Stderr, "Re-initiating login with YubiKey generated private key.\n")
response, err = sshLoginFunc(ctx, priv)
}
}
if err != nil {
return nil, trace.Wrap(err)
}
// Check that a host certificate for at least one cluster was returned.
if len(response.HostSigners) == 0 {
return nil, trace.BadParameter("bad response from the server: expected at least one certificate, got 0")
}
// extract the new certificate out of the response
key := NewKey(priv)
key.Cert = response.Cert
key.TLSCert = response.TLSCert
key.TrustedCerts = response.HostSigners
key.Username = response.Username
key.ProxyHost = tc.WebProxyHost()
if tc.KubernetesCluster != "" {
key.KubeTLSCerts[tc.KubernetesCluster] = response.TLSCert
}
if tc.DatabaseService != "" {
key.DBTLSCerts[tc.DatabaseService] = response.TLSCert
}
// Store the requested cluster name in the key.
key.ClusterName = tc.SiteName
if key.ClusterName == "" {
rootClusterName := key.TrustedCerts[0].ClusterName
key.ClusterName = rootClusterName
tc.SiteName = rootClusterName
}
return key, nil
}
// GetNewLoginKey gets a new private key for login.
func (tc *TeleportClient) GetNewLoginKey(ctx context.Context) (priv *keys.PrivateKey, err error) {
switch tc.PrivateKeyPolicy {
case keys.PrivateKeyPolicyHardwareKey:
log.Debugf("Attempting to login with YubiKey private key.")
priv, err = keys.GetOrGenerateYubiKeyPrivateKey(false)
case keys.PrivateKeyPolicyHardwareKeyTouch:
log.Debugf("Attempting to login with YubiKey private key with touch required.")
priv, err = keys.GetOrGenerateYubiKeyPrivateKey(true)
default:
log.Debugf("Attempting to login with a new RSA private key.")
priv, err = native.GeneratePrivateKey()
}
if err != nil {
return nil, trace.Wrap(err)
}
return priv, nil
}
// new SSHLogin generates a new SSHLogin using the given login key.
func (tc *TeleportClient) newSSHLogin(priv *keys.PrivateKey) (SSHLogin, error) {
attestationStatement, err := keys.GetAttestationStatement(priv)
if err != nil {
return SSHLogin{}, trace.Wrap(err)
}
return SSHLogin{
ProxyAddr: tc.WebProxyAddr,
PubKey: priv.MarshalSSHPublicKey(),
TTL: tc.KeyTTL,
Insecure: tc.InsecureSkipVerify,
Pool: loopbackPool(tc.WebProxyAddr),
Compatibility: tc.CertificateFormat,
RouteToCluster: tc.SiteName,
KubernetesCluster: tc.KubernetesCluster,
AttestationStatement: attestationStatement,
ExtraHeaders: tc.ExtraProxyHeaders,
}, nil
}
func (tc *TeleportClient) pwdlessLogin(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
// Only pass on the user if explicitly set, otherwise let the credential
// picker kick in.
user := ""
if tc.ExplicitUsername {
user = tc.Username
}
sshLogin, err := tc.newSSHLogin(priv)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := SSHAgentPasswordlessLogin(ctx, SSHLoginPasswordless{
SSHLogin: sshLogin,
User: user,
AuthenticatorAttachment: tc.AuthenticatorAttachment,
StderrOverride: tc.Stderr,
})
return response, trace.Wrap(err)
}
func (tc *TeleportClient) localLogin(ctx context.Context, priv *keys.PrivateKey, secondFactor constants.SecondFactorType) (*auth.SSHLoginResponse, error) {
var err error
var response *auth.SSHLoginResponse
// TODO(awly): mfa: ideally, clients should always go through mfaLocalLogin
// (with a nop MFA challenge if no 2nd factor is required). That way we can
// deprecate the direct login endpoint.
switch secondFactor {
case constants.SecondFactorOff, constants.SecondFactorOTP:
response, err = tc.directLogin(ctx, secondFactor, priv)
if err != nil {
return nil, trace.Wrap(err)
}
case constants.SecondFactorU2F, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional:
response, err = tc.mfaLocalLogin(ctx, priv)
if err != nil {
return nil, trace.Wrap(err)
}
default:
return nil, trace.BadParameter("unsupported second factor type: %q", secondFactor)
}
// Ignore username returned from proxy
response.Username = ""
return response, nil
}
// directLogin asks for a password + OTP token, makes a request to CA via proxy
func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType constants.SecondFactorType, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
password, err := tc.AskPassword(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Only ask for a second factor if it's enabled.
var otpToken string
if secondFactorType == constants.SecondFactorOTP {
otpToken, err = tc.AskOTP(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}
sshLogin, err := tc.newSSHLogin(priv)
if err != nil {
return nil, trace.Wrap(err)
}
// Ask the CA (via proxy) to sign our public key:
response, err := SSHAgentLogin(ctx, SSHLoginDirect{
SSHLogin: sshLogin,
User: tc.Username,
Password: password,
OTPToken: otpToken,
})
return response, trace.Wrap(err)
}
// mfaLocalLogin asks for a password and performs the challenge-response authentication
func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
password, err := tc.AskPassword(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
sshLogin, err := tc.newSSHLogin(priv)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := SSHAgentMFALogin(ctx, SSHLoginMFA{
SSHLogin: sshLogin,
User: tc.Username,
Password: password,
AuthenticatorAttachment: tc.AuthenticatorAttachment,
PreferOTP: tc.PreferOTP,
AllowStdinHijack: tc.AllowStdinHijack,
})
return response, trace.Wrap(err)
}
func (tc *TeleportClient) headlessLogin(ctx context.Context, priv *keys.PrivateKey) (*auth.SSHLoginResponse, error) {
headlessAuthenticationID := services.NewHeadlessAuthenticationID(priv.MarshalSSHPublicKey())
fmt.Fprintf(tc.Stdout, "Complete headless authentication in your local web browser:\ntsh headless approve --user=%v --proxy=%v %v\n", tc.Username, tc.WebProxyAddr, headlessAuthenticationID)
response, err := SSHAgentHeadlessLogin(ctx, SSHLoginHeadless{
SSHLogin: SSHLogin{
ProxyAddr: tc.WebProxyAddr,
PubKey: priv.MarshalSSHPublicKey(),
TTL: tc.KeyTTL,
Insecure: tc.InsecureSkipVerify,
Compatibility: tc.CertificateFormat,
KubernetesCluster: tc.KubernetesCluster,
},
User: tc.Username,
HeadlessAuthenticationID: headlessAuthenticationID,
})
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
// SSOLoginFunc is a function used in tests to mock SSO logins.
type SSOLoginFunc func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*auth.SSHLoginResponse, error)
// samlLogin opens browser window and uses OIDC or SAML redirect cycle with browser
func (tc *TeleportClient) ssoLogin(ctx context.Context, priv *keys.PrivateKey, connectorID string, protocol string) (*auth.SSHLoginResponse, error) {
if tc.MockSSOLogin != nil {
// sso login response is being mocked for testing purposes
return tc.MockSSOLogin(ctx, connectorID, priv, protocol)
}
sshLogin, err := tc.newSSHLogin(priv)
if err != nil {
return nil, trace.Wrap(err)
}
// ask the CA (via proxy) to sign our public key:
response, err := SSHAgentSSOLogin(ctx, SSHLoginSSO{
SSHLogin: sshLogin,
ConnectorID: connectorID,
Protocol: protocol,
BindAddr: tc.BindAddr,
Browser: tc.Browser,
}, nil)
return response, trace.Wrap(err)
}
// ActivateKey saves the target session cert into the local
// keystore (and into the ssh-agent) for future use.
func (tc *TeleportClient) ActivateKey(ctx context.Context, key *Key) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ActivateKey",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
if tc.localAgent == nil {
// skip activation if no local agent is present
return nil
}
// save the cert to the local storage (~/.tsh usually):
if err := tc.localAgent.AddKey(key); err != nil {
return trace.Wrap(err)
}
// Connect to the Auth Server of the root cluster and fetch the known hosts.
rootClusterName := key.TrustedCerts[0].ClusterName
if err := tc.UpdateTrustedCA(ctx, rootClusterName); err != nil {
if len(tc.JumpHosts) == 0 {
return trace.Wrap(err)
}
errViaJumphost := err
// If JumpHosts was pointing at the leaf cluster (e.g. during 'tsh ssh
// -J leaf.example.com'), this could've caused the above error. Try to
// fetch CAs without JumpHosts to force it to use the root cluster.
if err := tc.WithoutJumpHosts(func(tc *TeleportClient) error {
return tc.UpdateTrustedCA(ctx, rootClusterName)
}); err != nil {
return trace.NewAggregate(errViaJumphost, err)
}
}
return nil
}
// Ping makes a ping request to the proxy, and updates tc based on the
// response. The successful ping response is cached, multiple calls to Ping
// will return the original response and skip the round-trip.
//
// Ping can be called for its side-effect of applying the proxy-provided
// settings (such as various listening addresses).
func (tc *TeleportClient) Ping(ctx context.Context) (*webclient.PingResponse, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/Ping",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
// If, at some point, there's a need to bypass this caching, consider
// adding a bool argument. At the time of writing this we always want to
// cache.
if tc.lastPing != nil {
return tc.lastPing, nil
}
pr, err := webclient.Ping(&webclient.Config{
Context: ctx,
ProxyAddr: tc.WebProxyAddr,
Insecure: tc.InsecureSkipVerify,
Pool: loopbackPool(tc.WebProxyAddr),
ConnectorName: tc.AuthConnector,
ExtraHeaders: tc.ExtraProxyHeaders,
})
if err != nil {
return nil, trace.Wrap(err)
}
// If version checking was requested and the server advertises a minimum version.
if tc.CheckVersions && pr.MinClientVersion != "" {
if err := utils.CheckVersion(teleport.Version, pr.MinClientVersion); err != nil && trace.IsBadParameter(err) {
fmt.Fprintf(tc.Config.Stderr, `
WARNING
Detected potentially incompatible client and server versions.
Minimum client version supported by the server is %v but you are using %v.
Please upgrade tsh to %v or newer or use the --skip-version-check flag to bypass this check.
Future versions of tsh will fail when incompatible versions are detected.
`, pr.MinClientVersion, teleport.Version, pr.MinClientVersion)
}
}
// Update tc with proxy and auth settings specified in Ping response.
if err := tc.applyProxySettings(pr.Proxy); err != nil {
return nil, trace.Wrap(err)
}
tc.applyAuthSettings(pr.Auth)
tc.lastPing = pr
return pr, nil
}
// ShowMOTD fetches the cluster MotD, displays it (if any) and waits for
// confirmation from the user.
func (tc *TeleportClient) ShowMOTD(ctx context.Context) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/ShowMOTD",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
motd, err := webclient.GetMOTD(
&webclient.Config{
Context: ctx,
ProxyAddr: tc.WebProxyAddr,
Insecure: tc.InsecureSkipVerify,
Pool: loopbackPool(tc.WebProxyAddr),
ExtraHeaders: tc.ExtraProxyHeaders,
})
if err != nil {
return trace.Wrap(err)
}
if motd.Text != "" {
fmt.Fprintf(tc.Config.Stderr, "%s\nPress [ENTER] to continue.\n", motd.Text)
// We're re-using the password reader for user acknowledgment for
// aesthetic purposes, because we want to hide any garbage the
// use might enter at the prompt. Whatever the user enters will
// be simply discarded, and the user can still CTRL+C out if they
// disagree.
_, err := prompt.Stdin().ReadPassword(context.Background())
if err != nil {
return trace.Wrap(err)
}
}
return nil
}
// GetTrustedCA returns a list of host certificate authorities
// trusted by the cluster client is authenticated with.
func (tc *TeleportClient) GetTrustedCA(ctx context.Context, clusterName string) ([]types.CertAuthority, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetTrustedCA",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
defer span.End()
// Connect to the proxy.
if !tc.Config.ProxySpecified() {
return nil, trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
// Get a client to the Auth Server.
clt, err := proxyClient.ConnectToCluster(ctx, clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
// Get the list of host certificates that this cluster knows about.
return clt.GetCertAuthorities(ctx, types.HostCA, false)
}
// UpdateTrustedCA connects to the Auth Server and fetches all host certificates
// and updates ~/.tsh/keys/proxy/certs.pem and ~/.tsh/known_hosts.
func (tc *TeleportClient) UpdateTrustedCA(ctx context.Context, clusterName string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/UpdateTrustedCA",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
defer span.End()
if tc.localAgent == nil {
return trace.BadParameter("TeleportClient.UpdateTrustedCA called on a client without localAgent")
}
// Get the list of host certificates that this cluster knows about.
hostCerts, err := tc.GetTrustedCA(ctx, clusterName)
if err != nil {
return trace.Wrap(err)
}
trustedCerts := auth.AuthoritiesToTrustedCerts(hostCerts)
// Update the CA pool and known hosts for all CAs the cluster knows about.
err = tc.localAgent.SaveTrustedCerts(trustedCerts)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// applyProxySettings updates configuration changes based on the advertised
// proxy settings, overriding existing fields in tc.
func (tc *TeleportClient) applyProxySettings(proxySettings webclient.ProxySettings) error {
// Kubernetes proxy settings.
if proxySettings.Kube.Enabled {
switch {
// PublicAddr is the first preference.
case proxySettings.Kube.PublicAddr != "":
if _, err := utils.ParseAddr(proxySettings.Kube.PublicAddr); err != nil {
return trace.BadParameter(
"failed to parse value received from the server: %q, contact your administrator for help",
proxySettings.Kube.PublicAddr)
}
tc.KubeProxyAddr = proxySettings.Kube.PublicAddr
// ListenAddr is the second preference.
case proxySettings.Kube.ListenAddr != "":
addr, err := utils.ParseAddr(proxySettings.Kube.ListenAddr)
if err != nil {
return trace.BadParameter(
"failed to parse value received from the server: %q, contact your administrator for help",
proxySettings.Kube.ListenAddr)
}
// If ListenAddr host is 0.0.0.0 or [::], replace it with something
// routable from the web endpoint.
if net.ParseIP(addr.Host()).IsUnspecified() {
webProxyHost, _ := tc.WebProxyHostPort()
tc.KubeProxyAddr = net.JoinHostPort(webProxyHost, strconv.Itoa(addr.Port(defaults.KubeListenPort)))
} else {
tc.KubeProxyAddr = proxySettings.Kube.ListenAddr
}
// If neither PublicAddr nor TunnelAddr are passed, use the web
// interface hostname with default k8s port as a guess.
default:
webProxyHost, _ := tc.WebProxyHostPort()
tc.KubeProxyAddr = net.JoinHostPort(webProxyHost, strconv.Itoa(defaults.KubeListenPort))
}
} else {
// Zero the field, in case there was a previous value set (e.g. loaded
// from profile directory).
tc.KubeProxyAddr = ""
}
// Read in settings for HTTP endpoint of the proxy.
if proxySettings.SSH.PublicAddr != "" {
addr, err := utils.ParseAddr(proxySettings.SSH.PublicAddr)
if err != nil {
return trace.BadParameter(
"failed to parse value received from the server: %q, contact your administrator for help",
proxySettings.SSH.PublicAddr)
}
tc.WebProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(defaults.HTTPListenPort)))
if tc.localAgent != nil {
// Update local agent (that reads/writes to ~/.tsh) with the new address
// of the web proxy. This will control where the keys are stored on disk
// after login.
tc.localAgent.UpdateProxyHost(addr.Host())
}
}
// Read in settings for the SSH endpoint of the proxy.
//
// If listen_addr is set, take host from ProxyWebHost and port from what
// was set. This is to maintain backward compatibility when Teleport only
// supported public_addr.
if proxySettings.SSH.ListenAddr != "" {
addr, err := utils.ParseAddr(proxySettings.SSH.ListenAddr)
if err != nil {
return trace.BadParameter(
"failed to parse value received from the server: %q, contact your administrator for help",
proxySettings.SSH.ListenAddr)
}
webProxyHost, _ := tc.WebProxyHostPort()
tc.SSHProxyAddr = net.JoinHostPort(webProxyHost, strconv.Itoa(addr.Port(defaults.SSHProxyListenPort)))
}
// If ssh_public_addr is set, override settings from listen_addr.
if proxySettings.SSH.SSHPublicAddr != "" {
addr, err := utils.ParseAddr(proxySettings.SSH.SSHPublicAddr)
if err != nil {
return trace.BadParameter(
"failed to parse value received from the server: %q, contact your administrator for help",
proxySettings.SSH.SSHPublicAddr)
}
tc.SSHProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(defaults.SSHProxyListenPort)))
}
// Read Postgres proxy settings.
switch {
case proxySettings.DB.PostgresPublicAddr != "":
addr, err := utils.ParseAddr(proxySettings.DB.PostgresPublicAddr)
if err != nil {
return trace.BadParameter("failed to parse Postgres public address received from server: %q, contact your administrator for help",
proxySettings.DB.PostgresPublicAddr)
}
tc.PostgresProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(tc.WebProxyPort())))
// Listen address port applies if set and not in TLS routing mode.
case proxySettings.DB.PostgresListenAddr != "" && !proxySettings.TLSRoutingEnabled:
addr, err := utils.ParseAddr(proxySettings.DB.PostgresListenAddr)
if err != nil {
return trace.BadParameter("failed to parse Postgres listen address received from server: %q, contact your administrator for help",
proxySettings.DB.PostgresListenAddr)
}
tc.PostgresProxyAddr = net.JoinHostPort(tc.WebProxyHost(), strconv.Itoa(addr.Port(defaults.PostgresListenPort)))
default:
webProxyHost, webProxyPort := tc.WebProxyHostPort()
tc.PostgresProxyAddr = net.JoinHostPort(webProxyHost, strconv.Itoa(webProxyPort))
}
// Read Mongo proxy settings.
switch {
case proxySettings.DB.MongoPublicAddr != "":
addr, err := utils.ParseAddr(proxySettings.DB.MongoPublicAddr)
if err != nil {
return trace.BadParameter("failed to parse Mongo public address received from server: %q, contact your administrator for help",
proxySettings.DB.MongoPublicAddr)
}
tc.MongoProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(tc.WebProxyPort())))
case proxySettings.DB.MongoListenAddr != "":
addr, err := utils.ParseAddr(proxySettings.DB.MongoListenAddr)
if err != nil {
return trace.BadParameter("failed to parse Mongo listen address received from server: %q, contact your administrator for help",
proxySettings.DB.MongoListenAddr)
}
tc.MongoProxyAddr = net.JoinHostPort(tc.WebProxyHost(), strconv.Itoa(addr.Port(defaults.MongoListenPort)))
}
// Read MySQL proxy settings if enabled on the server.
switch {
case proxySettings.DB.MySQLPublicAddr != "":
addr, err := utils.ParseAddr(proxySettings.DB.MySQLPublicAddr)
if err != nil {
return trace.BadParameter("failed to parse MySQL public address received from server: %q, contact your administrator for help",
proxySettings.DB.MySQLPublicAddr)
}
tc.MySQLProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(defaults.MySQLListenPort)))
case proxySettings.DB.MySQLListenAddr != "":
addr, err := utils.ParseAddr(proxySettings.DB.MySQLListenAddr)
if err != nil {
return trace.BadParameter("failed to parse MySQL listen address received from server: %q, contact your administrator for help",
proxySettings.DB.MySQLListenAddr)
}
tc.MySQLProxyAddr = net.JoinHostPort(tc.WebProxyHost(), strconv.Itoa(addr.Port(defaults.MySQLListenPort)))
}
tc.TLSRoutingEnabled = proxySettings.TLSRoutingEnabled
if tc.TLSRoutingEnabled {
// If proxy supports TLS Routing all k8s requests will be sent to the WebProxyAddr where TLS Routing will identify
// k8s requests by "kube." SNI prefix and route to the kube proxy service.
tc.KubeProxyAddr = tc.WebProxyAddr
}
return nil
}
// applyAuthSettings updates configuration changes based on the advertised
// authentication settings, overriding existing fields in tc.
func (tc *TeleportClient) applyAuthSettings(authSettings webclient.AuthenticationSettings) {
tc.LoadAllCAs = authSettings.LoadAllCAs
// Update the private key policy from auth settings if it is stricter than the saved setting.
if authSettings.PrivateKeyPolicy != "" && authSettings.PrivateKeyPolicy.VerifyPolicy(tc.PrivateKeyPolicy) != nil {
tc.PrivateKeyPolicy = authSettings.PrivateKeyPolicy
}
}
// AddTrustedCA adds a new CA as trusted CA for this client, used in tests
func (tc *TeleportClient) AddTrustedCA(ctx context.Context, ca types.CertAuthority) error {
_, span := tc.Tracer.Start(
ctx,
"teleportClient/AddTrustedCA",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
if tc.localAgent == nil {
return trace.BadParameter("TeleportClient.AddTrustedCA called on a client without localAgent")
}
err := tc.localAgent.SaveTrustedCerts(auth.AuthoritiesToTrustedCerts([]types.CertAuthority{ca}))
if err != nil {
return trace.Wrap(err)
}
return nil
}
// AddKey adds a key to the client's local agent, used in tests.
func (tc *TeleportClient) AddKey(key *Key) error {
if tc.localAgent == nil {
return trace.BadParameter("TeleportClient.AddKey called on a client without localAgent")
}
if key.ClusterName == "" {
key.ClusterName = tc.SiteName
}
return tc.localAgent.AddKey(key)
}
// SendEvent adds a events.EventFields to the channel.
func (tc *TeleportClient) SendEvent(ctx context.Context, e events.EventFields) error {
// Try and send the event to the eventsCh. If blocking, keep blocking until
// the passed in context in canceled.
select {
case tc.eventsCh <- e:
return nil
case <-ctx.Done():
return trace.Wrap(ctx.Err())
}
}
// EventsChannel returns a channel that can be used to listen for events that
// occur for this session.
func (tc *TeleportClient) EventsChannel() <-chan events.EventFields {
return tc.eventsCh
}
// loopbackPool reads trusted CAs if it finds it in a predefined location
// and will work only if target proxy address is loopback
func loopbackPool(proxyAddr string) *x509.CertPool {
if !apiutils.IsLoopback(proxyAddr) {
log.Debugf("not using loopback pool for remote proxy addr: %v", proxyAddr)
return nil
}
log.Debugf("attempting to use loopback pool for local proxy addr: %v", proxyAddr)
certPool, err := x509.SystemCertPool()
if err != nil {
log.Debugf("could not open system cert pool, using empty cert pool instead: %v", err)
certPool = x509.NewCertPool()
}
certPath := filepath.Join(defaults.DataDir, defaults.SelfSignedCertPath)
log.Debugf("reading self-signed certs from: %v", certPath)
pemByte, err := os.ReadFile(certPath)
if err != nil {
log.Debugf("could not open any path in: %v", certPath)
return nil
}
for {
var block *pem.Block
block, pemByte = pem.Decode(pemByte)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Debugf("could not parse cert in: %v, err: %v", certPath, err)
return nil
}
certPool.AddCert(cert)
}
log.Debugf("using local pool for loopback proxy: %v, err: %v", certPath, err)
return certPool
}
// connectToSSHAgent connects to the system SSH agent and returns an agent.Agent.
func connectToSSHAgent() agent.ExtendedAgent {
socketPath := os.Getenv(teleport.SSHAuthSock)
conn, err := agentconn.Dial(socketPath)
if err != nil {
log.Errorf("[KEY AGENT] Unable to connect to SSH agent on socket: %q.", socketPath)
return nil
}
log.Infof("[KEY AGENT] Connected to the system agent: %q", socketPath)
return agent.NewClient(conn)
}
// Username returns the current user's username
func Username() (string, error) {
u, err := user.Current()
if err != nil {
return "", trace.Wrap(err)
}
username := u.Username
// If on Windows, strip the domain name.
if runtime.GOOS == constants.WindowsOS {
idx := strings.LastIndex(username, "\\")
if idx > -1 {
username = username[idx+1:]
}
}
return username, nil
}
// AskOTP prompts the user to enter the OTP token.
func (tc *TeleportClient) AskOTP(ctx context.Context) (token string, err error) {
return prompt.Password(ctx, tc.Stderr, prompt.Stdin(), "Enter your OTP token")
}
// AskPassword prompts the user to enter the password
func (tc *TeleportClient) AskPassword(ctx context.Context) (pwd string, err error) {
return prompt.Password(
ctx, tc.Stderr, prompt.Stdin(), fmt.Sprintf("Enter password for Teleport user %v", tc.Config.Username))
}
// LoadTLSConfig returns the user's TLS configuration, either from static
// configuration or from its key store.
func (tc *TeleportClient) LoadTLSConfig() (*tls.Config, error) {
if tc.TLS != nil {
return tc.TLS.Clone(), nil
}
tlsKey, err := tc.localAgent.GetCoreKey()
if err != nil {
return nil, trace.Wrap(err, "failed to fetch TLS key for %v", tc.Username)
}
var clusters []string
if tc.LoadAllCAs {
clusters, err = tc.localAgent.GetClusterNames()
if err != nil {
return nil, trace.Wrap(err)
}
} else {
rootCluster, err := tlsKey.RootClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
clusters = []string{rootCluster}
if tc.SiteName != "" && rootCluster != tc.SiteName {
// In case of establishing connection to leaf cluster the client validate
// ssh cert against root cluster proxy cert and leaf cluster cert.
clusters = append(clusters, tc.SiteName)
}
}
tlsConfig, err := tlsKey.TeleportClientTLSConfig(nil /* cipherSuites */, clusters)
if err != nil {
return nil, trace.Wrap(err, "failed to generate client TLS config")
}
tlsConfig.InsecureSkipVerify = tc.InsecureSkipVerify
return tlsConfig, nil
}
// LoadTLSConfigForClusters returns the client's TLS configuration, either from static
// configuration or from its key store. If loaded from the key store, CA certs will be
// loaded for the given clusters only.
func (tc *TeleportClient) LoadTLSConfigForClusters(clusters []string) (*tls.Config, error) {
if tc.TLS != nil {
return tc.TLS.Clone(), nil
}
tlsKey, err := tc.localAgent.GetCoreKey()
if err != nil {
return nil, trace.Wrap(err, "failed to fetch TLS key for %v", tc.Username)
}
tlsConfig, err := tlsKey.TeleportClientTLSConfig(nil /* cipherSuites */, clusters)
if err != nil {
return nil, trace.Wrap(err, "failed to generate client TLS config")
}
tlsConfig.InsecureSkipVerify = tc.InsecureSkipVerify
return tlsConfig, nil
}
// ParseLabelSpec parses a string like 'name=value,"long name"="quoted value"` into a map like
// { "name" -> "value", "long name" -> "quoted value" }
func ParseLabelSpec(spec string) (map[string]string, error) {
var tokens []string
openQuotes := false
var tokenStart, assignCount int
specLen := len(spec)
// tokenize the label spec:
for i, ch := range spec {
endOfToken := false
// end of line?
if i+utf8.RuneLen(ch) == specLen {
i += utf8.RuneLen(ch)
endOfToken = true
}
switch ch {
case '"':
openQuotes = !openQuotes
case '=', ',', ';':
if !openQuotes {
endOfToken = true
if ch == '=' {
assignCount++
}
}
}
if endOfToken && i > tokenStart {
tokens = append(tokens, strings.TrimSpace(strings.Trim(spec[tokenStart:i], `"`)))
tokenStart = i + 1
}
}
// simple validation of tokenization: must have an even number of tokens (because they're pairs)
// and the number of such pairs must be equal the number of assignments
if len(tokens)%2 != 0 || assignCount != len(tokens)/2 {
return nil, fmt.Errorf("invalid label spec: '%s', should be 'key=value'", spec)
}
// break tokens in pairs and put into a map:
labels := make(map[string]string)
for i := 0; i < len(tokens); i += 2 {
labels[tokens[i]] = tokens[i+1]
}
return labels, nil
}
// ParseSearchKeywords parses a string ie: foo,bar,"quoted value"` into a slice of
// strings: ["foo", "bar", "quoted value"].
// Almost a replica to ParseLabelSpec, but with few modifications such as
// allowing a custom delimiter. Defaults to comma delimiter if not defined.
func ParseSearchKeywords(spec string, customDelimiter rune) []string {
delimiter := customDelimiter
if delimiter == 0 {
delimiter = rune(',')
}
var tokens []string
openQuotes := false
var tokenStart int
specLen := len(spec)
// tokenize the label search:
for i, ch := range spec {
endOfToken := false
if i+utf8.RuneLen(ch) == specLen {
i += utf8.RuneLen(ch)
endOfToken = true
}
switch ch {
case '"':
openQuotes = !openQuotes
case delimiter:
if !openQuotes {
endOfToken = true
}
}
if endOfToken && i > tokenStart {
tokens = append(tokens, strings.TrimSpace(strings.Trim(spec[tokenStart:i], `"`)))
tokenStart = i + 1
}
}
return tokens
}
// Executes the given command on the client machine (localhost). If no command is given,
// executes shell
func runLocalCommand(command []string) error {
if len(command) == 0 {
user, err := user.Current()
if err != nil {
return trace.Wrap(err)
}
shell, err := shell.GetLoginShell(user.Username)
if err != nil {
return trace.Wrap(err)
}
command = []string{shell}
}
cmd := exec.Command(command[0], command[1:]...)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
return cmd.Run()
}
// String returns the same string spec which can be parsed by ParsePortForwardSpec.
func (fp ForwardedPorts) String() (retval []string) {
for _, p := range fp {
retval = append(retval, p.ToString())
}
return retval
}
// ParsePortForwardSpec parses parameter to -L flag, i.e. strings like "[ip]:80:remote.host:3000"
// The opposite of this function (spec generation) is ForwardedPorts.String()
func ParsePortForwardSpec(spec []string) (ports ForwardedPorts, err error) {
if len(spec) == 0 {
return ports, nil
}
const errTemplate = "invalid port forwarding spec '%s': expected format `80:remote.host:80`"
ports = make([]ForwardedPort, len(spec))
for i, str := range spec {
parts := strings.Split(str, ":")
if len(parts) < 3 || len(parts) > 4 {
return nil, trace.BadParameter(errTemplate, str)
}
if len(parts) == 3 {
parts = append([]string{"127.0.0.1"}, parts...)
}
p := &ports[i]
p.SrcIP = parts[0]
p.SrcPort, err = strconv.Atoi(parts[1])
if err != nil {
return nil, trace.BadParameter(errTemplate, str)
}
p.DestHost = parts[2]
p.DestPort, err = strconv.Atoi(parts[3])
if err != nil {
return nil, trace.BadParameter(errTemplate, str)
}
}
return ports, nil
}
// String returns the same string spec which can be parsed by
// ParseDynamicPortForwardSpec.
func (fp DynamicForwardedPorts) String() (retval []string) {
for _, p := range fp {
retval = append(retval, p.ToString())
}
return retval
}
// ParseDynamicPortForwardSpec parses the dynamic port forwarding spec
// passed in the -D flag. The format of the dynamic port forwarding spec
// is [bind_address:]port.
func ParseDynamicPortForwardSpec(spec []string) (DynamicForwardedPorts, error) {
result := make(DynamicForwardedPorts, 0, len(spec))
for _, str := range spec {
// Check whether this is only the port number, like "1080".
// net.SplitHostPort would fail on that unless there's a colon in
// front.
if !strings.Contains(str, ":") {
str = ":" + str
}
host, port, err := net.SplitHostPort(str)
if err != nil {
return nil, trace.Wrap(err)
}
// If no host is provided, bind to localhost.
if host == "" {
host = defaults.Localhost
}
srcPort, err := strconv.Atoi(port)
if err != nil {
return nil, trace.Wrap(err)
}
result = append(result, DynamicForwardedPort{
SrcIP: host,
SrcPort: srcPort,
})
}
return result, nil
}
// InsecureSkipHostKeyChecking is used when the user passes in
// "StrictHostKeyChecking yes".
func InsecureSkipHostKeyChecking(host string, remote net.Addr, key ssh.PublicKey) error {
return nil
}
// isFIPS returns if the binary was build with BoringCrypto, which implies
// FedRAMP/FIPS 140-2 mode for tsh.
func isFIPS() bool {
return modules.GetModules().IsBoringBinary()
}
// playSession plays session in the terminal
func playSession(sessionEvents []events.EventFields, stream []byte) error {
term, err := terminal.New(nil, nil, nil)
if err != nil {
return trace.Wrap(err)
}
defer term.Close()
// configure terminal for direct unbuffered echo-less input:
if term.IsAttached() {
err := term.InitRaw(true)
if err != nil {
return trace.Wrap(err)
}
}
player := newSessionPlayer(sessionEvents, stream, term)
errorCh := make(chan error)
// keys:
const (
keyCtrlC = 3
keyCtrlD = 4
keySpace = 32
keyLeft = 68
keyRight = 67
keyUp = 65
keyDown = 66
)
// playback control goroutine
go func() {
defer player.EndPlayback()
var key [1]byte
for {
_, err := term.Stdin().Read(key[:])
if err != nil {
errorCh <- err
return
}
switch key[0] {
// Ctrl+C or Ctrl+D
case keyCtrlC, keyCtrlD:
return
// Space key
case keySpace:
player.TogglePause()
// <- arrow
case keyLeft, keyDown:
player.Rewind()
// -> arrow
case keyRight, keyUp:
player.Forward()
}
}
}()
// player starts playing in its own goroutine
player.Play()
// wait for keypresses loop to end
select {
case <-player.stopC:
fmt.Println("\n\nend of session playback")
return nil
case err := <-errorCh:
return trace.Wrap(err)
}
}
func findActiveDatabases(key *Key) ([]tlsca.RouteToDatabase, error) {
dbCerts, err := key.DBTLSCertificates()
if err != nil {
return nil, trace.Wrap(err)
}
var databases []tlsca.RouteToDatabase
for _, cert := range dbCerts {
tlsID, err := tlsca.FromSubject(cert.Subject, time.Time{})
if err != nil {
return nil, trace.Wrap(err)
}
// If the cert expiration time is less than 5s consider cert as expired and don't add
// it to the user profile as an active database.
if time.Until(cert.NotAfter) < 5*time.Second {
continue
}
if tlsID.RouteToDatabase.ServiceName != "" {
databases = append(databases, tlsID.RouteToDatabase)
}
}
return databases, nil
}
// getDesktopEventWebURL returns the web UI URL users can access to
// watch a desktop session recording in the browser
func getDesktopEventWebURL(proxyHost string, cluster string, sid *session.ID, events []events.EventFields) string {
if len(events) < 1 {
return ""
}
start := events[0].GetTimestamp()
end := events[len(events)-1].GetTimestamp()
duration := end.Sub(start)
return fmt.Sprintf("https://%s/web/cluster/%s/session/%s?recordingType=desktop&durationMs=%d", proxyHost, cluster, sid, duration/time.Millisecond)
}
// SearchSessionEvents allows searching for session events with a full pagination support.
func (tc *TeleportClient) SearchSessionEvents(ctx context.Context, fromUTC, toUTC time.Time, pageSize int, order types.EventOrder, max int) ([]apievents.AuditEvent, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/SearchSessionEvents",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.Int("page_size", pageSize),
attribute.String("from", fromUTC.Format(time.RFC3339)),
attribute.String("to", toUTC.Format(time.RFC3339)),
),
)
defer span.End()
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
authClient := proxyClient.CurrentCluster()
defer authClient.Close()
sessions, err := GetPaginatedSessions(ctx, fromUTC, toUTC,
pageSize, order, max, authClient)
if err != nil {
return nil, trace.Wrap(err)
}
return sessions, nil
}
func parseMFAMode(in string) (wancli.AuthenticatorAttachment, error) {
switch in {
case "auto", "":
return wancli.AttachmentAuto, nil
case "platform":
return wancli.AttachmentPlatform, nil
case "cross-platform":
return wancli.AttachmentCrossPlatform, nil
default:
return 0, trace.BadParameter("unsupported mfa mode %q", in)
}
}
// NewKubernetesServiceClient connects to the proxy and returns an authenticated gRPC
// client to the Kubernetes service.
func (tc *TeleportClient) NewKubernetesServiceClient(ctx context.Context, clusterName string) (kubeproto.KubeServiceClient, error) {
if !tc.TLSRoutingEnabled {
return nil, trace.BadParameter("kube service is not supported if TLS routing is not enabled")
}
// get tlsConfig to dial to proxy.
tlsConfig, err := tc.LoadTLSConfig()
if err != nil {
return nil, trace.Wrap(err)
}
// Set the ALPN protocols to use when dialing the proxy gRPC mTLS endpoint.
tlsConfig.NextProtos = []string{string(alpncommon.ProtocolProxyGRPCSecure), http2.NextProtoTLS}
clt, err := client.New(ctx, client.Config{
Addrs: []string{tc.Config.WebProxyAddr},
DialInBackground: false,
Credentials: []client.Credentials{
client.LoadTLS(tlsConfig),
},
})
if err != nil {
return nil, trace.Wrap(err)
}
return kubeproto.NewKubeServiceClient(clt.GetConnection()), nil
}
// RootClusterCACertPool returns a *x509.CertPool with the root cluster CA.
func (tc *TeleportClient) RootClusterCACertPool(ctx context.Context) (*x509.CertPool, error) {
_, span := tc.Tracer.Start(
ctx,
"teleportClient/RootClusterCACertPool",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
key, err := tc.localAgent.GetCoreKey()
if err != nil {
return nil, trace.Wrap(err)
}
rootClusterName, err := key.RootClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
pool, err := key.clientCertPool(rootClusterName)
return pool, trace.Wrap(err)
}
// HeadlessApprove handles approval of a headless authentication request.
func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthenticationID string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/HeadlessApprove",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("proxy", tc.Config.WebProxyAddr),
),
)
defer span.End()
// connect to proxy first:
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
}
defer proxyClient.Close()
headlessAuthn, err := proxyClient.currentCluster.GetHeadlessAuthentication(ctx, headlessAuthenticationID)
if err != nil {
return trace.Wrap(err)
}
if headlessAuthn.State != types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING {
return trace.Errorf("cannot approve a headless authentication from a non-pending state: %v", headlessAuthn.State.Stringify())
}
confirmationPrompt := fmt.Sprintf("Headless login attempt from IP address %q requires approval.\nContact your administrator if you didn't initiate this login attempt.\nApprove?", headlessAuthn.ClientIpAddress)
ok, err := prompt.Confirmation(ctx, tc.Stdout, prompt.Stdin(), confirmationPrompt)
if err != nil {
return trace.Wrap(err)
}
if !ok {
err = proxyClient.currentCluster.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED, nil)
return trace.Wrap(err)
}
chall, err := proxyClient.currentCluster.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
})
if err != nil {
return trace.Wrap(err)
}
resp, err := tc.PromptMFAChallenge(ctx, tc.WebProxyAddr, chall, nil)
if err != nil {
return trace.Wrap(err)
}
err = proxyClient.currentCluster.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED, resp)
return trace.Wrap(err)
}