Skip to content
Merged
7 changes: 7 additions & 0 deletions lib/utils/aws/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions tool/tsh/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -83,7 +83,7 @@ func onAppLogin(cf *CLIConf) error {
Username: tc.Username,
PublicAddr: app.GetPublicAddr(),
ClusterName: tc.SiteName,
AWSRoleARN: arn,
AWSRoleARN: awsRoleARN,
AzureIdentity: azureIdentity,
}

Expand All @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
68 changes: 59 additions & 9 deletions tool/tsh/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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, ", "),
)
}

Expand Down Expand Up @@ -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}}
Comment thread
greedy52 marked this conversation as resolved.
`))
2 changes: 1 addition & 1 deletion tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down