Files
teleport/lib/srv/app/aws/handler.go
T
STeve (Xin) Huang 2115f58fec Align AWS assume-role request duration with cert expiration (#21670)
* Limit AWS access AssumeRole duration

* add info header

* add UT

* minor refactor

* beef up user message
2023-02-16 14:57:12 +00:00

304 lines
10 KiB
Go

/*
Copyright 2021 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 aws
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"strings"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/gravitational/oxy/forward"
oxyutils "github.com/gravitational/oxy/utils"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/srv/app/common"
"github.com/gravitational/teleport/lib/utils"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
)
// signerHandler is an http.Handler for signing and forwarding requests to AWS API.
type signerHandler struct {
// fwd is a Forwarder used to forward signed requests to AWS API.
fwd *forward.Forwarder
// SignerHandlerConfig is the configuration for the handler.
SignerHandlerConfig
// closeContext is the app server close context.
closeContext context.Context
}
// SignerHandlerConfig is the awsSignerHandler configuration.
type SignerHandlerConfig struct {
// Log is a logger for the handler.
Log logrus.FieldLogger
// RoundTripper is an http.RoundTripper instance used for requests.
RoundTripper http.RoundTripper
// SigningService is used to sign requests before forwarding them.
*awsutils.SigningService
// Clock is used to override time in tests.
Clock clockwork.Clock
}
// CheckAndSetDefaults validates the AwsSignerHandlerConfig.
func (cfg *SignerHandlerConfig) CheckAndSetDefaults() error {
if cfg.SigningService == nil {
return trace.BadParameter("missing SigningService")
}
if cfg.RoundTripper == nil {
tr, err := defaults.Transport()
if err != nil {
return trace.Wrap(err)
}
cfg.RoundTripper = tr
}
if cfg.Log == nil {
cfg.Log = logrus.WithField(trace.Component, "aws:signer")
}
if cfg.Clock == nil {
cfg.Clock = clockwork.NewRealClock()
}
return nil
}
// NewAWSSignerHandler creates a new request handler for signing and forwarding requests to AWS API.
func NewAWSSignerHandler(ctx context.Context, config SignerHandlerConfig) (http.Handler, error) {
if err := config.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
handler := &signerHandler{
SignerHandlerConfig: config,
closeContext: ctx,
}
fwd, err := forward.New(
forward.RoundTripper(config.RoundTripper),
forward.ErrorHandler(oxyutils.ErrorHandlerFunc(handler.formatForwardResponseError)),
// Explicitly passing false here to be clear that we always want the host
// header to be the same as the outbound request's URL host.
forward.PassHostHeader(false),
)
if err != nil {
return nil, trace.Wrap(err)
}
handler.fwd = fwd
return handler, nil
}
// formatForwardResponseError converts an error to a status code and writes the code to a response.
func (s *signerHandler) formatForwardResponseError(rw http.ResponseWriter, r *http.Request, err error) {
s.Log.WithError(err).Debugf("Failed to process request.")
common.SetTeleportAPIErrorHeader(rw, err)
// Convert trace error type to HTTP and write response.
code := trace.ErrorToCode(err)
http.Error(rw, http.StatusText(code), code)
}
// ServeHTTP handles incoming requests by signing them and then forwarding them to the proper AWS API.
func (s *signerHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if err := s.serveHTTP(w, req); err != nil {
s.formatForwardResponseError(w, req, err)
return
}
}
// serveHTTP is a helper to simplify error handling in ServeHTTP.
func (s *signerHandler) serveHTTP(w http.ResponseWriter, req *http.Request) error {
sessCtx, err := common.GetSessionContext(req)
if err != nil {
return trace.Wrap(err)
}
// Handle requests signed with real credentials of assumed roles by the AWS
// client. Headers will be restored and the request will be forwarded to
// AWS without re-signing.
if req.Header.Get(common.TeleportAWSAssumedRole) != "" {
return trace.Wrap(s.serveRequestByAssumedRole(sessCtx, w, req))
}
// Handle requests signed with the default local proxy credentials. The
// request will be re-signed with real credentials by assuming the
// requested role of this AWS app.
return trace.Wrap(s.serveCommonRequest(sessCtx, w, req))
}
func (s *signerHandler) serveCommonRequest(sessCtx *common.SessionContext, w http.ResponseWriter, req *http.Request) error {
// It's important that we resolve the endpoint before modifying the request headers,
// as they may be needed to resolve the endpoint correctly.
re, err := resolveEndpoint(req)
if err != nil {
return trace.Wrap(err)
}
unsignedReq, err := s.rewriteCommonRequest(sessCtx, w, req, re)
if err != nil {
return trace.Wrap(err)
}
signedReq, err := s.SignRequest(s.closeContext, unsignedReq,
&awsutils.SigningCtx{
SigningName: re.SigningName,
SigningRegion: re.SigningRegion,
Expiry: sessCtx.Identity.Expires,
SessionName: sessCtx.Identity.Username,
AWSRoleArn: sessCtx.Identity.RouteToApp.AWSRoleARN,
AWSExternalID: sessCtx.App.GetAWSExternalID(),
})
if err != nil {
return trace.Wrap(err)
}
recorder := httplib.NewResponseStatusRecorder(w)
s.fwd.ServeHTTP(recorder, signedReq)
s.emitAudit(sessCtx, unsignedReq, uint32(recorder.Status()), re)
return nil
}
// serveRequestByAssumedRole forwards the requests signed with real credentials
// of an assumed role to AWS.
func (s *signerHandler) serveRequestByAssumedRole(sessCtx *common.SessionContext, w http.ResponseWriter, req *http.Request) error {
re, err := resolveEndpointByXForwardedHost(req, common.TeleportAWSAssumedRoleAuthorization)
if err != nil {
return trace.Wrap(err)
}
req, err = s.rewriteRequestByAssumedRole(req, re)
if err != nil {
return trace.Wrap(err)
}
reqCloneForAudit, err := cloneRequest(req)
if err != nil {
return trace.Wrap(err)
}
recorder := httplib.NewResponseStatusRecorder(w)
s.fwd.ServeHTTP(recorder, req)
s.emitAudit(sessCtx, reqCloneForAudit, uint32(recorder.Status()), re)
return nil
}
func (s *signerHandler) emitAudit(sessCtx *common.SessionContext, req *http.Request, status uint32, re *endpoints.ResolvedEndpoint) {
var auditErr error
if isDynamoDBEndpoint(re) {
auditErr = sessCtx.Audit.OnDynamoDBRequest(s.closeContext, sessCtx, req, status, re)
} else {
auditErr = sessCtx.Audit.OnRequest(s.closeContext, sessCtx, req, status, re)
}
if auditErr != nil {
// log but don't return the error, because we already handed off request/response handling to the oxy forwarder.
s.Log.WithError(auditErr).Warn("Failed to emit audit event.")
}
}
// rewriteRequest clones a request to rewrite the url.
func rewriteRequest(ctx context.Context, r *http.Request, re *endpoints.ResolvedEndpoint) (*http.Request, error) {
u, err := urlForResolvedEndpoint(r, re)
if err != nil {
return nil, trace.Wrap(err)
}
// clone the request for rewriting
outReq := r.Clone(ctx)
if outReq.URL == nil {
outReq.URL = u
} else {
outReq.URL.Scheme = "https"
outReq.URL.Host = u.Host
}
outReq.Body = io.NopCloser(io.LimitReader(r.Body, teleport.MaxHTTPRequestSize))
// need to rewrite the host header as well. The oxy forwarder will do this for us,
// since we use the PassHostHeader(false) option, but if host is a signed header
// then we must make the host match the URL host before signing the request or AWS
// will reject the request for failing sigv4 verification.
outReq.Host = u.Host
return outReq, nil
}
// rewriteCommonRequest updates request signed with the default local proxy credentials.
func (s *signerHandler) rewriteCommonRequest(sessCtx *common.SessionContext, w http.ResponseWriter, r *http.Request, re *endpoints.ResolvedEndpoint) (*http.Request, error) {
req, err := rewriteRequest(s.closeContext, r, re)
if err != nil {
return nil, trace.Wrap(err)
}
if strings.EqualFold(re.SigningName, sts.EndpointsID) {
if err := updateAssumeRoleDuration(sessCtx.Identity, w, req, s.Clock); err != nil {
return nil, trace.Wrap(err)
}
}
return req, nil
}
// rewriteRequestByAssumedRole updates headers and url for requests by assumed roles.
func (s *signerHandler) rewriteRequestByAssumedRole(r *http.Request, re *endpoints.ResolvedEndpoint) (*http.Request, error) {
req, err := rewriteRequest(s.closeContext, r, re)
if err != nil {
return nil, trace.Wrap(err)
}
if r.Host != req.Host {
return nil, trace.BadParameter("resolved host %q does not match requested host %q", req.Host, r.Host)
}
// Remove the special header before sending the request to AWS.
assumedRole := req.Header.Get(common.TeleportAWSAssumedRole)
req.Header.Del(common.TeleportAWSAssumedRole)
// Put back the original authorization header.
utils.RenameHeader(req.Header, common.TeleportAWSAssumedRoleAuthorization, awsutils.AuthorizationHeader)
return common.WithAWSAssumedRole(req, assumedRole), nil
}
// urlForResolvedEndpoint creates a URL based on input request and resolved endpoint.
func urlForResolvedEndpoint(r *http.Request, re *endpoints.ResolvedEndpoint) (*url.URL, error) {
resolvedURL, err := url.Parse(re.URL)
if err != nil {
return nil, trace.Wrap(err)
}
// Replaces scheme and host. Keeps original path etc.
clone := *r.URL
if resolvedURL.Host != "" {
clone.Host = resolvedURL.Host
}
if resolvedURL.Scheme != "" {
clone.Scheme = resolvedURL.Scheme
}
return &clone, nil
}
// cloneRequest makes a clone of the request with a deep-cloned body.
func cloneRequest(req *http.Request) (*http.Request, error) {
body, err := utils.GetAndReplaceRequestBody(req)
if err != nil {
return nil, trace.Wrap(err)
}
clone := req.Clone(req.Context())
clone.Body = io.NopCloser(bytes.NewReader(body))
return clone, nil
}