Files
teleport/tool/tsh/common/proxy.go
T
rosstimothy 5870768c88 Stop using text/template and html/template to enable DCE (#65401)
All existing template usage was converted to make use of
github.com/DataDog/datadog-agent/pkg/template. The DataDog package
is a fork of the stdlib template packages with method calling
removed so that reflect.MethodByName does not prevent DCE.

There were a handful of method calls in our templates that were trivially
changed to use custom functions defined via FuncMaps or specifying the
calculated value to the template instead of calling a function in the template.

A future change will add linter rules to prevent new text/template or
html/imports from landing. They have not been included here so as
to not to break enterprise code while it is migrated.
2026-04-22 15:54:43 +00:00

1030 lines
31 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package common
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509/pkix"
"fmt"
"net"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"unicode"
template "github.com/DataDog/datadog-agent/pkg/template/text"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
libclient "github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/db/dbcmd"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
// onProxyCommandSSH creates a local ssh proxy, dialing a node and transferring
// data through stdin and stdout, to be used as an OpenSSH and PuTTY proxy
// command.
func onProxyCommandSSH(cf *CLIConf, initFunc ClientInitFunc) error {
tc, err := initFunc(cf)
if err != nil {
return trace.Wrap(err)
}
sshFunc := func() error {
clt, err := tc.ConnectToCluster(cf.Context)
if err != nil {
return trace.Wrap(err)
}
targetHost, targetPort, err := net.SplitHostPort(tc.Host)
if err != nil {
targetHost = tc.Host
targetPort = strconv.Itoa(tc.HostPort)
}
targetHost = cleanTargetHost(targetHost, tc.WebProxyHost(), clt.ClusterName())
tc.Host = targetHost
port, err := strconv.Atoi(targetPort)
if err != nil {
return trace.Wrap(err)
}
tc.HostPort = port
target, err := tc.GetTargetNode(cf.Context, clt.AuthClient, nil)
if err != nil {
return trace.Wrap(err)
}
conn, _, err := clt.DialHostWithResumption(cf.Context, target.Addr, clt.ClusterName(), tc.LocalAgent().ExtendedAgent)
if err != nil {
return trace.Wrap(err)
}
defer conn.Close()
return trace.Wrap(utils.ProxyConn(cf.Context, utils.CombinedStdio{}, conn))
}
if !cf.Relogin {
return trace.Wrap(sshFunc())
}
return trace.Wrap(libclient.RetryWithRelogin(cf.Context, tc, sshFunc))
}
// cleanTargetHost cleans the targetHost and remote site and proxy suffixes.
// Before the `cut -d` command was used for this purpose but to support multi-platform OpenSSH clients the logic
// it was moved tsh proxy ssh command.
// For more details please look at: Generate Windows-compatible OpenSSH config https://github.com/gravitational/teleport/pull/7848
func cleanTargetHost(targetHost, proxyHost, siteName string) string {
targetHost = strings.TrimSuffix(targetHost, "."+proxyHost)
targetHost = strings.TrimSuffix(targetHost, "."+siteName)
return targetHost
}
// formatCommand formats command making it suitable for the end user to copy the command and paste it into terminal.
func formatCommand(cmd *exec.Cmd) string {
// environment variables
env := strings.Join(cmd.Env, " ")
var args []string
for _, arg := range cmd.Args {
// escape the potential quotes within
arg = strings.ReplaceAll(arg, `"`, `\"`)
// if there is whitespace within, surround with quotes
if strings.IndexFunc(arg, unicode.IsSpace) != -1 {
args = append(args, fmt.Sprintf(`"%s"`, arg))
} else {
args = append(args, arg)
}
}
argsfmt := strings.Join(args, " ")
if len(env) > 0 {
return fmt.Sprintf("%s %s", env, argsfmt)
}
return argsfmt
}
func onProxyCommandDB(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}
profile, err := tc.ProfileStatus()
if err != nil {
return trace.Wrap(err)
}
routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore)
if err != nil {
return trace.Wrap(err)
}
dbInfo, err := getDatabaseInfo(cf, tc, routes)
if err != nil {
return trace.Wrap(err)
}
// When proxying without the `--tunnel` flag, we need to:
// 1. check if --tunnel is required.
// 2. check if db login is required.
// These steps are not needed with `--tunnel`, because the local proxy tunnel
// will manage database certificates itself and reissue them as needed.
requires := getDBLocalProxyRequirement(tc, dbInfo.RouteToDatabase)
if requires.tunnel && !cf.LocalProxyTunnel {
// Some scenarios require a local proxy tunnel, e.g.:
// - Snowflake, DynamoDB protocol
// - Hardware-backed private key policy
return trace.BadParameter("%s", formatDbCmdUnsupported(cf, dbInfo.RouteToDatabase, requires.tunnelReasons...))
}
if err := maybeDatabaseLogin(cf, tc, profile, dbInfo, requires); err != nil {
return trace.Wrap(err)
}
rootCluster, err := tc.RootClusterName(cf.Context)
if err != nil {
return trace.Wrap(err)
}
if cf.LocalProxyAddr != "" && cf.LocalProxyPort != "" {
return trace.BadParameter("either the address or port for the local proxy can be specified, not both")
}
addr := "localhost:0"
randomPort := true
if cf.LocalProxyAddr != "" {
host, _, err := net.SplitHostPort(cf.LocalProxyAddr)
if err != nil {
return trace.Wrap(err)
}
if !cf.InsecureListenAnywhere && !apiutils.IsLoopback(host) {
return trace.BadParameter("only loopback addresses are allowed for listener without --insecure-listen-anywhere switch, got %q", cf.LocalProxyAddr)
}
randomPort = false
addr = cf.LocalProxyAddr
} else if cf.LocalProxyPort != "" {
randomPort = false
addr = fmt.Sprintf("127.0.0.1:%s", cf.LocalProxyPort)
}
listener, err := createLocalProxyListener(addr, dbInfo.RouteToDatabase, profile)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := listener.Close(); err != nil {
logger.WarnContext(cf.Context, "Failed to close listener", "error", err)
}
}()
proxyOpts, err := prepareLocalProxyOptions(&localProxyConfig{
cf: cf,
tc: tc,
profile: profile,
dbInfo: dbInfo,
tunnel: cf.LocalProxyTunnel,
})
if err != nil {
return trace.Wrap(err)
}
lp, err := alpnproxy.NewLocalProxy(makeBasicLocalProxyConfig(cf.Context, tc, listener, cf.InsecureSkipVerify), proxyOpts...)
if err != nil {
return trace.Wrap(err)
}
go func() {
<-cf.Context.Done()
lp.Close()
}()
if cf.LocalProxyTunnel {
addr, err := utils.ParseAddr(lp.GetAddr())
if err != nil {
return trace.Wrap(err)
}
opts, err := makeDatabaseCommandOptions(cf.Context, tc, dbInfo,
dbcmd.WithLocalProxy("localhost", addr.Port(0), ""),
dbcmd.WithNoTLS(),
dbcmd.WithPrintFormat(),
dbcmd.WithTolerateMissingCLIClient(),
)
if err != nil {
return trace.Wrap(err)
}
cb, err := dbcmd.NewCmdBuilder(tc, profile, dbInfo.RouteToDatabase, rootCluster, dbInfo.getDatabaseForDBCmd, opts...)
if err != nil {
return trace.Wrap(err)
}
commands, err := cb.GetConnectCommandAlternatives(cf.Context)
if err != nil {
return trace.Wrap(err)
}
// shared template arguments
templateArgs := map[string]any{
"database": dbInfo.ServiceName,
"type": defaults.ReadableDatabaseProtocol(dbInfo.Protocol),
"cluster": tc.SiteName,
"address": listener.Addr().String(),
"randomPort": randomPort,
}
maybeAddGCPMetadataTplArgs(cf.Context, tc, dbInfo, templateArgs)
tmpl := chooseProxyCommandTemplate(templateArgs, commands, dbInfo)
err = tmpl.Execute(os.Stdout, templateArgs)
if err != nil {
return trace.Wrap(err)
}
} else {
err = dbProxyTpl.Execute(os.Stdout, map[string]any{
"database": dbInfo.ServiceName,
"address": listener.Addr().String(),
"ca": profile.CACertPathForCluster(rootCluster),
"cert": profile.DatabaseCertPathForCluster(cf.SiteName, dbInfo.ServiceName),
"key": profile.DatabaseKeyPathForCluster(cf.SiteName, dbInfo.ServiceName),
"randomPort": randomPort,
"databaseUser": dbInfo.Username,
"databaseName": dbInfo.Database,
})
if err != nil {
return trace.Wrap(err)
}
}
defer lp.Close()
if err := lp.Start(cf.Context); err != nil {
return trace.Wrap(err)
}
return nil
}
func maybeAddDBUserPassword(ctx context.Context, tc *libclient.TeleportClient, dbInfo *databaseInfo, opts []dbcmd.ConnectCommandFunc) ([]dbcmd.ConnectCommandFunc, error) {
if dbInfo.Protocol == defaults.ProtocolCassandra {
db, err := dbInfo.GetDatabase(ctx, tc)
if err != nil {
return nil, trace.Wrap(err)
}
if db.IsAWSHosted() {
// Cassandra client always prompt for password, so we need to provide it
// Provide an auto generated random password to skip the prompt in case of
// connection to AWS hosted cassandra.
password, err := utils.CryptoRandomHex(16)
if err != nil {
return nil, trace.Wrap(err)
}
return append(opts, dbcmd.WithPassword(password)), nil
}
}
return opts, nil
}
func requiresGCPMetadata(protocol string) bool {
return protocol == defaults.ProtocolSpanner
}
func makeDatabaseCommandOptions(ctx context.Context, tc *libclient.TeleportClient, dbInfo *databaseInfo, extraOpts ...dbcmd.ConnectCommandFunc) ([]dbcmd.ConnectCommandFunc, error) {
var err error
opts := append([]dbcmd.ConnectCommandFunc{
dbcmd.WithLogger(logger),
}, extraOpts...)
if opts, err = maybeAddDBUserPassword(ctx, tc, dbInfo, opts); err != nil {
return nil, trace.Wrap(err)
}
return maybeAddOracleOptions(ctx, tc, dbInfo, opts), nil
}
func maybeAddGCPMetadataTplArgs(ctx context.Context, tc *libclient.TeleportClient, dbInfo *databaseInfo, templateArgs map[string]any) {
if !requiresGCPMetadata(dbInfo.Protocol) {
return
}
templateArgs["gcpProject"] = "<project>"
templateArgs["gcpInstance"] = "<instance>"
db, err := dbInfo.GetDatabase(ctx, tc)
if err == nil {
gcp := db.GetGCP()
templateArgs["gcpProject"] = gcp.ProjectID
templateArgs["gcpInstance"] = gcp.InstanceID
}
}
func maybeAddOracleOptions(ctx context.Context, tc *libclient.TeleportClient, dbInfo *databaseInfo, opts []dbcmd.ConnectCommandFunc) []dbcmd.ConnectCommandFunc {
// Skip for non-Oracle protocols.
if dbInfo.Protocol != defaults.ProtocolOracle {
return opts
}
// TODO(Tener): DELETE IN 20.0.0 - all agents should now contain improved Oracle engine.
// minimum version to support TCPS-less connection.
cutoffVersion := semver.Version{
Major: 17,
Minor: 2,
Patch: 0,
PreRelease: "",
}
devV17Version := semver.Version{
Major: 17,
Minor: 0,
Patch: 0,
PreRelease: "dev",
}
dbServers, err := getDatabaseServers(ctx, tc, dbInfo.ServiceName)
if err != nil {
// log, but treat this error as non-fatal.
logger.WarnContext(ctx, "Error getting database servers", "error", err)
return opts
}
var oldServers, newServers int
for _, server := range dbServers {
ver, err := semver.NewVersion(server.GetTeleportVersion())
if err != nil {
logger.DebugContext(ctx, "Failed to parse teleport version", "version", server.GetTeleportVersion(), "error", err)
continue
}
if ver.Equal(devV17Version) {
newServers++
} else {
if ver.LessThan(cutoffVersion) {
oldServers++
} else {
newServers++
}
}
}
logger.DebugContext(ctx, "Retrieved agents for database with Oracle support",
"database", dbInfo.ServiceName,
"total", len(dbServers),
"old_count", oldServers,
"new_count", newServers,
)
if oldServers > 0 {
logger.WarnContext(ctx, "Detected outdated database agent, for improved client support upgrade all database agents in your cluster to a newer version",
"lowest_supported_version", cutoffVersion,
)
}
opts = append(opts, dbcmd.WithOracleOpts(oldServers == 0, newServers > 0))
return opts
}
type templateCommandItem struct {
Description string
Command string
}
func chooseProxyCommandTemplate(templateArgs map[string]any, commands []dbcmd.CommandAlternative, dbInfo *databaseInfo) *template.Template {
templateArgs["command"] = formatCommand(commands[0].Command)
// protocol-specific templates
if dbInfo.Protocol == defaults.ProtocolOracle {
// the JDBC connection string should always be found,
// but the order of commands is important as only the first command will actually be shown.
jdbcConnectionString := ""
ixFound := -1
for ix, cmd := range commands {
for _, arg := range cmd.Command.Args {
if strings.Contains(arg, "jdbc:oracle:") {
jdbcConnectionString = arg
ixFound = ix
}
}
}
templateArgs["jdbcConnectionString"] = jdbcConnectionString
templateArgs["canUseTCP"] = ixFound > 0
return dbProxyOracleAuthTpl
}
if dbInfo.Protocol == defaults.ProtocolSpanner {
templateArgs["databaseName"] = "<database>"
if dbInfo.Database != "" {
templateArgs["databaseName"] = dbInfo.Database
}
return dbProxySpannerAuthTpl
}
// there is only one command, use plain template.
if len(commands) == 1 {
return dbProxyAuthTpl
}
// multiple command options, use a different template.
var commandsArg []templateCommandItem
for _, cmd := range commands {
commandsArg = append(commandsArg, templateCommandItem{cmd.Description, formatCommand(cmd.Command)})
}
delete(templateArgs, "command")
templateArgs["commands"] = commandsArg
return dbProxyAuthMultiTpl
}
func alpnProtocolForApp(app types.Application) alpncommon.Protocol {
if app.IsTCP() {
return alpncommon.ProtocolTCP
}
return alpncommon.ProtocolHTTP
}
func onProxyCommandApp(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}
portMapping, err := libclient.ParsePortMapping(cf.LocalProxyPortMapping)
if err != nil {
return trace.Wrap(err)
}
profile, err := tc.ProfileStatus()
if err != nil {
return trace.Wrap(err)
}
var (
appInfo *appInfo
app types.Application
)
if err := libclient.RetryWithRelogin(cf.Context, tc, func() error {
var err error
clusterClient, err := tc.ConnectToCluster(cf.Context)
if err != nil {
return trace.Wrap(err)
}
defer clusterClient.Close()
appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, matchGCPApp)
if err != nil {
return trace.Wrap(err)
}
app, err = appInfo.GetApp(cf.Context, clusterClient.AuthClient)
return trace.Wrap(err)
}); err != nil {
return trace.Wrap(err)
}
if err := checkProxyMCPCompatibility(cf.command, app); err != nil {
return trace.Wrap(err)
}
proxyApp, err := newLocalProxyAppWithPortMapping(cf.Context, tc, profile, appInfo.RouteToApp, app, portMapping, cf.InsecureSkipVerify)
if err != nil {
return trace.Wrap(err)
}
if err := proxyApp.StartLocalProxy(cf.Context, alpnproxy.WithALPNProtocol(alpnProtocolForApp(app))); err != nil {
return trace.Wrap(err)
}
appName := cf.AppName
if portMapping.TargetPort != 0 {
appName = net.JoinHostPort(appName, strconv.Itoa(portMapping.TargetPort))
}
fmt.Printf("Proxying connections to %s on %v\n", appName, proxyApp.GetAddr())
// If target port is not equal to zero, the user must know about the port flag.
if portMapping.LocalPort == 0 && portMapping.TargetPort == 0 {
fmt.Println("To avoid port randomization, you can choose the listening port using the --port flag.")
}
defer func() {
if err := proxyApp.Close(); err != nil {
logger.ErrorContext(cf.Context, "Failed to close app proxy", "error", err)
}
}()
// Proxy connections until the client terminates the command.
<-cf.Context.Done()
return nil
}
func checkProxyMCPCompatibility(command string, app types.Application) error {
if !app.IsMCP() {
switch command {
case "proxy mcp":
return trace.BadParameter("%q is not an MCP application", app.GetName())
default:
// tsh proxy app
return nil
}
}
mcpTransport := types.GetMCPServerTransportType(app.GetURI())
switch mcpTransport {
case types.MCPTransportHTTP:
return nil
default:
return trace.BadParameter("MCP applications with %s transport are not supported. Please see 'tsh mcp config --help' for more details.", mcpTransport)
}
}
// onProxyCommandAWS creates local proxes for AWS apps.
func onProxyCommandAWS(cf *CLIConf) error {
if err := checkProxyAWSFormatCompatibility(cf); err != nil {
return trace.Wrap(err)
}
awsApp, err := pickAWSApp(cf)
if err != nil {
return trace.Wrap(err)
}
err = awsApp.StartLocalProxies(cf.Context)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := awsApp.Close(); err != nil {
logger.ErrorContext(cf.Context, "Failed to close AWS app", "error", err)
}
}()
if err := printProxyAWSTemplate(cf, awsApp); err != nil {
return trace.Wrap(err)
}
<-cf.Context.Done()
return nil
}
type awsAppInfo interface {
GetAppName() string
GetEnvVars() (map[string]string, error)
GetForwardProxyAddr() string
}
func printProxyAWSTemplate(cf *CLIConf, awsApp awsAppInfo) error {
envVars, err := awsApp.GetEnvVars()
if err != nil {
return trace.Wrap(err)
}
templateData := map[string]any{
"envVars": envVars,
"format": cf.Format,
"randomPort": cf.LocalProxyPort == "",
"appName": awsApp.GetAppName(),
"region": getEnvOrDefault(awsRegionEnvVar, "<region>"),
"keystore": getEnvOrDefault(awsKeystoreEnvVar, "<keystore>"),
"workgroup": getEnvOrDefault(awsWorkgroupEnvVar, "<workgroup>"),
}
if proxyAddr := awsApp.GetForwardProxyAddr(); proxyAddr != "" {
proxyHost, proxyPort, err := net.SplitHostPort(proxyAddr)
if err != nil {
return trace.Wrap(err)
}
templateData["proxyScheme"] = "http"
templateData["proxyHost"] = proxyHost
templateData["proxyPort"] = proxyPort
}
templates := []string{awsProxyHeaderTemplate}
switch {
case cf.Format == awsProxyFormatAthenaODBC:
templates = append(templates, awsProxyAthenaODBCTemplate)
case cf.Format == awsProxyFormatAthenaJDBC:
templates = append(templates, awsProxyJDBCHeaderFooterTemplate, awsProxyAthenaJDBCTemplate)
case cf.AWSEndpointURLMode:
return trace.BadParameter("--endpoint-url is no longer supported, use HTTPS proxy instead (default mode)")
default:
templates = append(templates, awsHTTPSProxyTemplate)
}
combined := template.New("").Funcs(cloudTemplateFuncs)
for _, text := range templates {
combined, err = combined.Parse(text)
if err != nil {
return trace.Wrap(err)
}
}
return trace.Wrap(combined.Execute(cf.Stdout(), templateData))
}
func checkProxyAWSFormatCompatibility(cf *CLIConf) error {
if cf.AWSEndpointURLMode {
return trace.BadParameter("--endpoint-url is no longer supported, use HTTPS proxy instead (default mode)")
}
return nil
}
// onProxyCommandAzure creates local proxes for Azure apps.
func onProxyCommandAzure(cf *CLIConf) error {
azApp, err := pickAzureApp(cf)
if err != nil {
return trace.Wrap(err)
}
err = azApp.StartLocalProxies(cf.Context)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := azApp.Close(); err != nil {
logger.ErrorContext(cf.Context, "Failed to close Azure app", "error", err)
}
}()
envVars, err := azApp.GetEnvVars()
if err != nil {
return trace.Wrap(err)
}
if err = printCloudTemplate(envVars, cf.Format, cf.LocalProxyPort == "", types.CloudAzure); err != nil {
return trace.Wrap(err)
}
<-cf.Context.Done()
return nil
}
// onProxyCommandGCloud creates local proxies for GCP apps.
func onProxyCommandGCloud(cf *CLIConf) error {
gcpApp, err := pickGCPApp(cf)
if err != nil {
return trace.Wrap(err)
}
err = gcpApp.StartLocalProxies(cf.Context)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := gcpApp.Close(); err != nil {
logger.ErrorContext(cf.Context, "Failed to close GCP app", "error", err)
}
}()
envVars, err := gcpApp.GetEnvVars()
if err != nil {
return trace.Wrap(err)
}
if err = printCloudTemplate(envVars, cf.Format, cf.LocalProxyPort == "", types.CloudGCP); err != nil {
return trace.Wrap(err)
}
<-cf.Context.Done()
return nil
}
func loadAppCertificate(tc *libclient.TeleportClient, appName string) (tls.Certificate, error) {
keyRing, err := tc.LocalAgent().GetKeyRing(tc.SiteName, libclient.WithAppCerts{})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
appCert, err := keyRing.AppTLSCert(appName)
if trace.IsNotFound(err) {
return tls.Certificate{}, trace.NotFound("please login into the application first: 'tsh apps login %v'", appName)
} else if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
return appCert, nil
}
func loadDBCertificate(tc *libclient.TeleportClient, dbName string) (tls.Certificate, error) {
keyRing, err := tc.LocalAgent().GetKeyRing(tc.SiteName, libclient.WithDBCerts{})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
dbCert, err := keyRing.DBTLSCert(dbName)
if trace.IsNotFound(err) {
return tls.Certificate{}, trace.NotFound("please login into the database first. 'tsh db login'")
} else if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
return dbCert, nil
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func makeBasicLocalProxyConfig(ctx context.Context, tc *libclient.TeleportClient, listener net.Listener, insecure bool) alpnproxy.LocalProxyConfig {
return alpnproxy.LocalProxyConfig{
RemoteProxyAddr: tc.WebProxyAddr,
InsecureSkipVerify: insecure,
ParentContext: ctx,
Listener: listener,
ALPNConnUpgradeRequired: tc.TLSRoutingConnUpgradeRequired,
}
}
func generateDBLocalProxyCert(signer crypto.Signer, profile *libclient.ProfileStatus) error {
path := profile.DatabaseLocalCAPath()
if utils.FileExists(path) {
return nil
}
certPem, err := tlsca.GenerateSelfSignedCAWithConfig(tlsca.GenerateCAConfig{
Entity: pkix.Name{
CommonName: "localhost",
Organization: []string{"Teleport"},
},
Signer: signer,
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP(defaults.Localhost)},
TTL: defaults.CATTL,
})
if err != nil {
return trace.Wrap(err)
}
if err := os.WriteFile(profile.DatabaseLocalCAPath(), certPem, teleport.FileMaskOwnerOnly); err != nil {
return trace.ConvertSystemError(err)
}
return nil
}
const dbProxyConnectAd = `Teleport Connect is a desktop app that can manage database proxies for you.
Learn more at https://goteleport.com/docs/connect-your-client/teleport-connect/#connecting-to-a-database
`
// dbProxyTpl is the message that gets printed to a user when a database proxy is started.
var dbProxyTpl = template.Must(template.New("").Parse(`Started DB proxy on {{.address}}
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
` + dbProxyConnectAd + `
Use the following credentials to connect to the {{.database}} proxy:
ca_file={{.ca}}
cert_file={{.cert}}
key_file={{.key}}
Your database user is "{{.databaseUser}}".{{if .databaseName}} The target database name is "{{.databaseName}}".{{end}}
`))
// dbProxyAuthTpl is the message that's printed for an authenticated db proxy.
var dbProxyAuthTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
` + dbProxyConnectAd + `
Use the following command to connect to the database or to the address above using other database GUI/CLI clients:
$ {{.command}}
`))
// dbProxySpannerAuthTpl is the message that's printed for an authenticated spanner db proxy.
var dbProxySpannerAuthTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
` + dbProxyConnectAd + `
Use the following command to connect to the database or to the address above using other database GUI/CLI clients:
$ {{.command}}
Or use the following JDBC connection string to connect with other GUI/CLI clients:
jdbc:cloudspanner://{{.address}}/projects/{{.gcpProject}}/instances/{{.gcpInstance}}/databases/{{.databaseName}};usePlainText=true
`))
var dbProxyOracleAuthTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
Use the following command to connect to the Oracle database server using CLI:
$ {{.command}}
{{if .canUseTCP }}Other clients can use:
- a direct connection to {{.address}} without a username and password
- a custom JDBC connection string: {{.jdbcConnectionString}}
{{else }}You can also connect using Oracle JDBC connection string:
{{.jdbcConnectionString}}
Note: for improved client compatibility, upgrade your Teleport cluster. For details rerun this command with --debug.
{{- end }}
`))
// dbProxyAuthMultiTpl is the message that's printed for an authenticated db proxy if there are multiple command options.
var dbProxyAuthMultiTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
` + dbProxyConnectAd + `
Use one of the following commands to connect to the database or to the address above using other database GUI/CLI clients:
{{range $item := .commands}}
* {{$item.Description}}:
$ {{$item.Command}}
{{end}}
`))
const (
envVarFormatText = "text"
envVarFormatUnix = "unix"
envVarFormatWindowsCommandPrompt = "command-prompt"
envVarFormatWindowsPowershell = "powershell"
awsProxyFormatAthenaODBC = "athena-odbc"
awsProxyFormatAthenaJDBC = "athena-jdbc"
)
var (
envVarFormats = []string{
envVarFormatUnix,
envVarFormatWindowsCommandPrompt,
envVarFormatWindowsPowershell,
envVarFormatText,
}
awsProxyServiceFormats = []string{
awsProxyFormatAthenaODBC,
awsProxyFormatAthenaJDBC,
}
awsProxyFormats = append(envVarFormats, awsProxyServiceFormats...)
)
func envVarFormatFlagDescription() string {
return fmt.Sprintf(
"Optional format to print the commands for setting environment variables, one of: %s. Default is %s.",
strings.Join(envVarFormats, ", "),
envVarDefaultFormat(),
)
}
func awsProxyFormatFlagDescription() string {
return fmt.Sprintf(
"%s Or specify a service format, one of: %s.",
envVarFormatFlagDescription(),
strings.Join(awsProxyServiceFormats, ", "),
)
}
func envVarDefaultFormat() string {
if runtime.GOOS == constants.WindowsOS {
return envVarFormatWindowsPowershell
}
return envVarFormatUnix
}
// envVarCommand returns the command to set environment variables based on the
// format.
//
// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
func envVarCommand(format, key, value string) (string, error) {
switch format {
case envVarFormatUnix:
return fmt.Sprintf(`export %s="%s"`, key, value), nil
case envVarFormatWindowsCommandPrompt:
return fmt.Sprintf("set %s=%s", key, value), nil
case envVarFormatWindowsPowershell:
return fmt.Sprintf(`$Env:%s="%s"`, key, value), nil
case envVarFormatText:
return fmt.Sprintf("%s=%s", key, value), nil
default:
return "", trace.BadParameter("unsupported format %q", format)
}
}
var cloudTemplateFuncs = template.FuncMap{
"envVarCommand": envVarCommand,
}
// awsProxyHeaderTemplate contains common header used for AWS proxy.
const awsProxyHeaderTemplate = `
{{define "header"}}
Started AWS proxy on {{.envVars.HTTPS_PROXY}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
{{end}}
`
// awsProxyJDBCHeaderFooterTemplate contains common header and footer for AWS
// proxy in JDBC formats.
const awsProxyJDBCHeaderFooterTemplate = `
{{define "jdbc-header" }}
{{- template "header" . -}}
First, add the following certificate to your keystore:
{{.envVars.AWS_CA_BUNDLE}}
For example, to import the certificate using "keytool":
keytool -noprompt -importcert -alias teleport-{{.appName}} -file {{.envVars.AWS_CA_BUNDLE}} -keystore {{.keystore}}
{{end}}
{{define "jdbc-footer" }}
Note that a new certificate might be generated for a new app session. If you
encounter the "remote error: tls: unknown certificate" error, make sure your
keystore is up-to-date.
{{end}}
`
// awsHTTPSProxyTemplate is the message that gets printed to a user when an
// HTTPS proxy is started.
var awsHTTPSProxyTemplate = `{{- template "header" . -}}
Use the following credentials and HTTPS proxy setting to connect to the proxy:
{{ envVarCommand .format "AWS_ACCESS_KEY_ID" .envVars.AWS_ACCESS_KEY_ID}}
{{ envVarCommand .format "AWS_SECRET_ACCESS_KEY" .envVars.AWS_SECRET_ACCESS_KEY}}
{{ envVarCommand .format "AWS_CA_BUNDLE" .envVars.AWS_CA_BUNDLE}}
{{ envVarCommand .format "HTTPS_PROXY" .envVars.HTTPS_PROXY}}
`
// awsProxyAthenaODBCTemplate is the message that gets printed to a user when an
// AWS proxy is used for Athena ODBC driver.
var awsProxyAthenaODBCTemplate = `{{- template "header" . -}}
Set the following properties for the Athena ODBC data source:
[Teleport AWS Athena Access]
AuthenticationType = IAM Credentials
UID = {{.envVars.AWS_ACCESS_KEY_ID}}
PWD = {{.envVars.AWS_SECRET_ACCESS_KEY}}
UseProxy = 1;
ProxyScheme = {{.proxyScheme}};
ProxyHost = {{.proxyHost}};
ProxyPort = {{.proxyPort}};
TrustedCerts = {{.envVars.AWS_CA_BUNDLE}}
Here is a sample connection string using the above credentials and proxy settings:
DRIVER=Simba Amazon Athena ODBC Connector;AuthenticationType=IAM Credentials;UID={{.envVars.AWS_ACCESS_KEY_ID}};PWD={{.envVars.AWS_SECRET_ACCESS_KEY}};UseProxy=1;ProxyScheme={{.proxyScheme}};ProxyHost={{.proxyHost}};ProxyPort={{.proxyPort}};TrustedCerts={{.envVars.AWS_CA_BUNDLE}};AWSRegion={{.region}};Workgroup={{.workgroup}}
`
// awsProxyAthenaJDBCTemplate is the message that gets printed to a user when
// an AWS proxy is used for Athena JDBC driver.
var awsProxyAthenaJDBCTemplate = `{{- template "jdbc-header" . -}}
Then, set the following properties in the JDBC connection URL:
User = {{.envVars.AWS_ACCESS_KEY_ID}}
Password = {{.envVars.AWS_SECRET_ACCESS_KEY}}
ProxyHost = {{.proxyHost}};
ProxyPort = {{.proxyPort}};
Here is a sample JDBC connection URL using the above credentials and proxy settings:
jdbc:awsathena://User={{.envVars.AWS_ACCESS_KEY_ID}};Password={{.envVars.AWS_SECRET_ACCESS_KEY}};ProxyHost={{.proxyHost}};ProxyPort={{.proxyPort}};AwsRegion={{.region}};Workgroup={{.workgroup}}
{{- template "jdbc-footer" -}}
`
func printCloudTemplate(envVars map[string]string, format string, randomPort bool, cloudName string) error {
templateData := map[string]any{
"envVars": envVars,
"format": format,
"randomPort": randomPort,
"cloudName": cloudName,
}
err := cloudHTTPSProxyTemplate.Execute(os.Stdout, templateData)
return trace.Wrap(err)
}
// cloudHTTPSProxyTemplate is the message that gets printed to a user when a cloud HTTPS proxy is started.
var cloudHTTPSProxyTemplate = template.Must(template.New("").Funcs(cloudTemplateFuncs).Parse(
`Started {{.cloudName}} proxy on {{.envVars.HTTPS_PROXY}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
Use the following credentials and HTTPS proxy setting to connect to the proxy:
{{- $fmt := .format }}
{{ range $key, $value := .envVars}}
{{envVarCommand $fmt $key $value}}
{{- end}}
`))