diff --git a/lib/utils/aws/signing.go b/lib/utils/aws/signing.go index 3e2ac701b7f34..3c2647d286ecc 100644 --- a/lib/utils/aws/signing.go +++ b/lib/utils/aws/signing.go @@ -146,6 +146,13 @@ func (s *SigningService) SignRequest(ctx context.Context, req *http.Request, sig reqCopy := req.Clone(ctx) reqCopy.Body = io.NopCloser(req.Body) + // Only keep the headers signed in the original request for signing. This + // not only avoids signing extra headers injected by Teleport along the + // way, but also preserves the signing logic of the original AWS client. + // + // For example, Athena ODBC driver sends query requests with "Expect: + // 100-continue" headers without being signed, otherwise the Athena service + // would reject the requests. unsignedHeaders := removeUnsignedHeaders(reqCopy) credentials := s.GetSigningCredentials(s.Session, signCtx.Expiry, signCtx.SessionName, signCtx.AWSRoleArn, signCtx.AWSExternalID) signer := NewSigner(credentials, signCtx.SigningName) diff --git a/tool/tsh/app.go b/tool/tsh/app.go index 81f94fba9e24a..bf00215ea4334 100644 --- a/tool/tsh/app.go +++ b/tool/tsh/app.go @@ -60,10 +60,10 @@ func onAppLogin(cf *CLIConf) error { return trace.Wrap(err) } - var arn string + var awsRoleARN string if app.IsAWSConsole() { var err error - arn, err = getARNFromFlags(cf, profile, app) + awsRoleARN, err = getARNFromFlags(cf, profile, app) if err != nil { return trace.Wrap(err) } @@ -83,7 +83,7 @@ func onAppLogin(cf *CLIConf) error { Username: tc.Username, PublicAddr: app.GetPublicAddr(), ClusterName: tc.SiteName, - AWSRoleARN: arn, + AWSRoleARN: awsRoleARN, AzureIdentity: azureIdentity, } @@ -99,7 +99,7 @@ func onAppLogin(cf *CLIConf) error { SessionID: ws.GetName(), PublicAddr: app.GetPublicAddr(), ClusterName: tc.SiteName, - AWSRoleARN: arn, + AWSRoleARN: awsRoleARN, AzureIdentity: azureIdentity, }, AccessRequests: profile.ActiveRequests.AccessRequests, @@ -117,6 +117,7 @@ func onAppLogin(cf *CLIConf) error { return awsCliTpl.Execute(os.Stdout, map[string]string{ "awsAppName": app.GetName(), "awsCmd": "s3 ls", + "awsRoleARN": awsRoleARN, }) } if app.IsAzureCloud() { @@ -152,7 +153,7 @@ func onAppLogin(cf *CLIConf) error { "appName": app.GetName(), }) } - curlCmd, err := formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster, arn, azureIdentity) + curlCmd, err := formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster, awsRoleARN, azureIdentity) if err != nil { return trace.Wrap(err) } @@ -187,9 +188,16 @@ Then connect to the application through this proxy. // awsCliTpl is the message that gets printed to a user upon successful login // into an AWS Console application. var awsCliTpl = template.Must(template.New("").Parse( - `Logged into AWS app {{.awsAppName}}. Example AWS CLI command: + `Logged into AWS app "{{.awsAppName}}". +Your IAM role: + {{.awsRoleARN}} + +Example AWS CLI command: tsh aws {{.awsCmd}} + +Or start a local proxy: + tsh proxy aws --app {{.awsAppName}} `)) // azureCliTpl is the message that gets printed to a user upon successful login diff --git a/tool/tsh/proxy.go b/tool/tsh/proxy.go index 1d5d95562e6a9..b3bb7fc8fa106 100644 --- a/tool/tsh/proxy.go +++ b/tool/tsh/proxy.go @@ -697,9 +697,21 @@ func onProxyCommandAWS(cf *CLIConf) error { "randomPort": cf.LocalProxyPort == "", } - template := awsHTTPSProxyTemplate - if cf.AWSEndpointURLMode { + var template *template.Template + switch { + case cf.Format == awsProxyFormatAthenaODBC: + if cf.AWSEndpointURLMode { + return trace.BadParameter("format %q is not supported in --endpoint-url mode", cf.Format) + } + + templateData["proxyHost"], templateData["proxyPort"], _ = net.SplitHostPort(awsApp.GetForwardProxyAddr()) + templateData["proxyScheme"] = "http" + template = awsProxyAthenaODBCTemplate + + case cf.AWSEndpointURLMode: template = awsEndpointURLProxyTemplate + default: + template = awsHTTPSProxyTemplate } if err = template.Execute(os.Stdout, templateData); err != nil { @@ -821,19 +833,36 @@ const ( envVarFormatUnix = "unix" envVarFormatWindowsCommandPrompt = "command-prompt" envVarFormatWindowsPowershell = "powershell" + + awsProxyFormatAthenaODBC = "athena-odbc" ) -var envVarFormats = []string{ - envVarFormatUnix, - envVarFormatWindowsCommandPrompt, - envVarFormatWindowsPowershell, - envVarFormatText, -} +var ( + envVarFormats = []string{ + envVarFormatUnix, + envVarFormatWindowsCommandPrompt, + envVarFormatWindowsPowershell, + envVarFormatText, + } + + awsProxyServiceFormats = []string{awsProxyFormatAthenaODBC} + + awsProxyFormats = append(envVarFormats, awsProxyServiceFormats...) +) func envVarFormatFlagDescription() string { return fmt.Sprintf( - "Optional format to print the commands for setting environment variables, one of: %s.", + "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, ", "), ) } @@ -924,3 +953,24 @@ In addition to the endpoint URL, use the following credentials to connect to the {{ envVarCommand .format "AWS_SECRET_ACCESS_KEY" .envVars.AWS_SECRET_ACCESS_KEY}} {{ envVarCommand .format "AWS_CA_BUNDLE" .envVars.AWS_CA_BUNDLE}} `)) + +// awsProxyAthenaODBCTemplate is the message that gets printed to a user when an +// AWS proxy is used for Athena ODBC driver. +var awsProxyAthenaODBCTemplate = template.Must(template.New("").Funcs(awsTemplateFuncs).Parse( + `Started AWS proxy on {{.envVars.HTTPS_PROXY}}. +{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag. +{{end}} +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;AwsRegion=us-east-1;S3OutputLocation=s3://example-bucket/athena/output/;Workgroup=example-workgroup;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}} +`)) diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index 06c2246cacf5b..13bdc4fa7ff03 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -691,7 +691,7 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error { proxyAWS.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").StringVar(&cf.AppName) proxyAWS.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort) proxyAWS.Flag("endpoint-url", "Run local proxy to serve as an AWS endpoint URL. If not specified, local proxy serves as an HTTPS proxy.").Short('e').BoolVar(&cf.AWSEndpointURLMode) - proxyAWS.Flag("format", envVarFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, envVarFormats...) + proxyAWS.Flag("format", awsProxyFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, awsProxyFormats...) proxyAzure := proxy.Command("azure", "Start local proxy for Azure access.") proxyAzure.Flag("app", "Optional Name of the Azure application to use if logged into multiple.").StringVar(&cf.AppName)