AWS session audit log (#13288)

This commit is contained in:
STeve (Xin) Huang 2022-08-03 15:44:54 -04:00 committed by GitHub
parent 9c7d9134e2
commit c596dd7d9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1071 additions and 651 deletions

File diff suppressed because it is too large Load diff

View file

@ -1484,6 +1484,19 @@ message AppSessionRequest {
// App is a common application resource metadata.
AppMetadata App = 6
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
// AWS contains extra AWS metadata of the request.
AWSRequestMetadata AWS = 7
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
}
// AWSRequestMetadata contains extra AWS metadata of an AppSessionRequest.
message AWSRequestMetadata {
// AWSRegion is the requested AWS region.
string AWSRegion = 1 [ (gogoproto.jsontag) = "aws_region,omitempty" ];
// AWSService is the requested AWS service name.
string AWSService = 2 [ (gogoproto.jsontag) = "aws_service,omitempty" ];
// AWSHost is the requested host of the AWS service.
string AWSHost = 3 [ (gogoproto.jsontag) = "aws_host,omitempty" ];
}
// DatabaseMetadata contains common database information.

View file

@ -18,6 +18,7 @@ package aws
import (
"bytes"
"context"
"fmt"
"net/http"
@ -34,7 +35,9 @@ import (
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/srv/app/common"
appcommon "github.com/gravitational/teleport/lib/srv/app/common"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
@ -57,7 +60,7 @@ func NewSigningService(config SigningServiceConfig) (*SigningService, error) {
if err != nil {
return nil, trace.Wrap(err)
}
svc.fwd = fwd
svc.Forwarder = fwd
return svc, nil
}
@ -67,8 +70,8 @@ type SigningService struct {
// SigningServiceConfig is the SigningService configuration.
SigningServiceConfig
// fwd signs and forwards the request to AWS API.
fwd *forward.Forwarder
// Forwarder signs and forwards the request to AWS API.
*forward.Forwarder
}
// SigningServiceConfig is the SigningService configuration.
@ -119,11 +122,6 @@ func (s *SigningServiceConfig) CheckAndSetDefaults() error {
return nil
}
// Handle handles the AWS CLI request.
func (s *SigningService) Handle(rw http.ResponseWriter, r *http.Request) {
s.fwd.ServeHTTP(rw, r)
}
// RoundTrip handles incoming requests and forwards them to the proper AWS API.
// Handling steps:
// 1) Decoded Authorization Header. Authorization Header example:
@ -157,9 +155,38 @@ func (s *SigningService) RoundTrip(req *http.Request) (*http.Response, error) {
if err != nil {
return nil, trace.Wrap(err)
}
if err := s.emitAuditEvent(req.Context(), signedReq, resp, sessionCtx, resolvedEndpoint); err != nil {
s.Log.WithError(err).Warn("Failed to emit audit event.")
}
return resp, nil
}
// emitAuditEvent writes details of the AWS request to audit stream.
func (s *SigningService) emitAuditEvent(ctx context.Context, req *http.Request, resp *http.Response, sessionCtx *common.SessionContext, endpoint *endpoints.ResolvedEndpoint) error {
event := &apievents.AppSessionRequest{
Metadata: apievents.Metadata{
Type: events.AppSessionRequestEvent,
Code: events.AppSessionRequestCode,
},
Method: req.Method,
Path: req.URL.Path,
RawQuery: req.URL.RawQuery,
StatusCode: uint32(resp.StatusCode),
AppMetadata: apievents.AppMetadata{
AppURI: sessionCtx.App.GetURI(),
AppPublicAddr: sessionCtx.App.GetPublicAddr(),
AppName: sessionCtx.App.GetName(),
},
AWSRequestMetadata: apievents.AWSRequestMetadata{
AWSRegion: endpoint.SigningRegion,
AWSService: endpoint.SigningName,
AWSHost: req.Host,
},
}
return trace.Wrap(sessionCtx.Emitter.EmitAuditEvent(ctx, event))
}
func (s *SigningService) formatForwardResponseError(rw http.ResponseWriter, r *http.Request, err error) {
switch trace.Unwrap(err).(type) {
case *trace.BadParameterError:

View file

@ -35,7 +35,9 @@ import (
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/srv/app/common"
"github.com/gravitational/teleport/lib/tlsca"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
@ -127,6 +129,7 @@ func TestAWSSignerHandler(t *testing.T) {
require.Equal(t, tc.wantAuthCredKeyID, awsAuthHeader.KeyID)
require.Equal(t, tc.wantAuthCredService, awsAuthHeader.Service)
}
suite := createSuite(t, handler)
s3Client := s3.New(tc.awsClientSession, &aws.Config{
@ -136,6 +139,20 @@ func TestAWSSignerHandler(t *testing.T) {
for _, check := range tc.checks {
check(t, resp, err)
}
// Validate audit event.
if err == nil {
require.Len(t, suite.emitter.C(), 1)
event := <-suite.emitter.C()
appSessionEvent, ok := event.(*events.AppSessionRequest)
require.True(t, ok)
require.Equal(t, tc.wantHost, appSessionEvent.AWSHost)
require.Equal(t, tc.wantAuthCredService, appSessionEvent.AWSService)
require.Equal(t, tc.wantAuthCredRegion, appSessionEvent.AWSRegion)
} else {
require.Len(t, suite.emitter.C(), 0)
}
})
}
}
@ -146,12 +163,13 @@ func staticAWSCredentials(client.ConfigProvider, *common.SessionContext) *creden
type suite struct {
*httptest.Server
identity *tlsca.Identity
app types.Application
emitter *eventstest.ChannelEmitter
}
func createSuite(t *testing.T, handler http.HandlerFunc) *suite {
emitter := eventstest.NewChannelEmitter(1)
user := auth.LocalUser{Username: "user"}
app, err := types.NewAppV3(types.Metadata{
Name: "awsconsole",
@ -190,10 +208,12 @@ func createSuite(t *testing.T, handler http.HandlerFunc) *suite {
request = common.WithSessionContext(request, &common.SessionContext{
Identity: &user.Identity,
App: app,
Emitter: emitter,
})
svc.Handle(writer, request)
svc.ServeHTTP(writer, request)
})
server := httptest.NewServer(mux)
t.Cleanup(func() {
server.Close()
@ -203,5 +223,6 @@ func createSuite(t *testing.T, handler http.HandlerFunc) *suite {
Server: server,
identity: &user.Identity,
app: app,
emitter: emitter,
}
}

View file

@ -1,9 +1,12 @@
/*
Copyright 2022 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.
@ -17,9 +20,11 @@ import (
"context"
"net/http"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/tlsca"
)
// SessionContext contains common context parameters for an App session.
@ -28,6 +33,8 @@ type SessionContext struct {
Identity *tlsca.Identity
// App is the requested identity.
App types.Application
// Emitter is the audit log emitter.
Emitter events.Emitter
}
// WithSessionContext adds session context to provided request.

View file

@ -658,51 +658,64 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
return trace.Wrap(err)
}
// Distinguish between AWS console access originated from Teleport Proxy WebUI and
// access from AWS CLI where the request is already singed by the AWS Signature Version 4 algorithm.
// AWS CLI, automatically use SigV4 for all services that support it (All services expect Amazon SimpleDB
// but this AWS service has been deprecated)
if aws.IsSignedByAWSSigV4(r) && app.IsAWSConsole() {
// TODO(greedy52) create a proper sessionChunk for AWS requests to
// record audit events.
sessionCtx := &common.SessionContext{
Identity: identity,
App: app,
switch {
case app.IsAWSConsole():
// Requests from AWS applications are singed by AWS Signature Version
// 4 algorithm. AWS CLI and AWS SDKs automatically use SigV4 for all
// services that support it (All services expect Amazon SimpleDB but
// this AWS service has been deprecated)
if aws.IsSignedByAWSSigV4(r) {
return s.serveSession(w, r, identity, app, s.withAWSForwarder)
}
// Sign the request based on RouteToApp.AWSRoleARN user identity and route signed request to the AWS API.
s.awsSigner.Handle(w, common.WithSessionContext(r, sessionCtx))
return nil
// Request for AWS console access originated from Teleport Proxy WebUI
// is not signed by SigV4.
return s.serveAWSWebConsole(w, r, identity, app)
default:
return s.serveSession(w, r, identity, app, s.withJWTTokenForwarder)
}
// If this application is AWS management console, generate a sign-in URL
// and redirect the user to it.
if app.IsAWSConsole() {
s.log.Debugf("Redirecting %v to AWS mananement console with role %v.",
identity.Username, identity.RouteToApp.AWSRoleARN)
url, err := s.c.Cloud.GetAWSSigninURL(AWSSigninRequest{
Identity: identity,
TargetURL: app.GetURI(),
Issuer: app.GetPublicAddr(),
ExternalID: app.GetAWSExternalID(),
})
if err != nil {
return trace.Wrap(err)
}
http.Redirect(w, r, url.SigninURL, http.StatusFound)
return nil
}
}
// serveAWSWebConsole generates a sign-in URL for AWS management console and
// redirects the user to it.
func (s *Server) serveAWSWebConsole(w http.ResponseWriter, r *http.Request, identity *tlsca.Identity, app types.Application) error {
s.log.Debugf("Redirecting %v to AWS mananement console with role %v.",
identity.Username, identity.RouteToApp.AWSRoleARN)
url, err := s.c.Cloud.GetAWSSigninURL(AWSSigninRequest{
Identity: identity,
TargetURL: app.GetURI(),
Issuer: app.GetPublicAddr(),
ExternalID: app.GetAWSExternalID(),
})
if err != nil {
return trace.Wrap(err)
}
http.Redirect(w, r, url.SigninURL, http.StatusFound)
return nil
}
// serveSession finds the app session and forwards the request.
func (s *Server) serveSession(w http.ResponseWriter, r *http.Request, identity *tlsca.Identity, app types.Application, opts ...sessionOpt) error {
// Fetch a cached request forwarder (or create one) that lives about 5
// minutes. Used to stream session chunks to the Audit Log.
session, err := s.getSession(r.Context(), identity, app)
session, err := s.getSession(r.Context(), identity, app, opts...)
if err != nil {
return trace.Wrap(err)
}
defer session.release()
// Create session context.
sessionCtx := &common.SessionContext{
Identity: identity,
App: app,
Emitter: session.streamWriter,
}
// Forward request to the target application.
session.fwd.ServeHTTP(w, r)
session.fwd.ServeHTTP(w, common.WithSessionContext(r, sessionCtx))
return nil
}
@ -787,7 +800,7 @@ func (s *Server) authorizeContext(ctx context.Context) (*tlsca.Identity, types.A
// will return a cached session, otherwise will create one.
// The in-flight request count is automatically incremented on the session.
// The caller must call session.release() after finishing its use
func (s *Server) getSession(ctx context.Context, identity *tlsca.Identity, app types.Application) (*sessionChunk, error) {
func (s *Server) getSession(ctx context.Context, identity *tlsca.Identity, app types.Application, opts ...sessionOpt) (*sessionChunk, error) {
session, err := s.cache.get(identity.RouteToApp.SessionID)
// If a cached forwarder exists, return it right away.
if err == nil && session.acquire() == nil {
@ -795,7 +808,7 @@ func (s *Server) getSession(ctx context.Context, identity *tlsca.Identity, app t
}
// Create a new session with a recorder and forwarder in it.
session, err = s.newSessionChunk(ctx, identity, app)
session, err = s.newSessionChunk(ctx, identity, app, opts...)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -85,11 +85,14 @@ type sessionChunk struct {
log *logrus.Entry
}
// sessionOpt defines an option function for creating sessionChunk.
type sessionOpt func(context.Context, *sessionChunk, *tlsca.Identity, types.Application) error
// newSessionChunk creates a new chunk session.
// The session chunk is created with inflight=1,
// and as such expects `release()` to eventually be called
// by the caller of this function.
func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity, app types.Application) (*sessionChunk, error) {
func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity, app types.Application, opts ...sessionOpt) (*sessionChunk, error) {
sess := &sessionChunk{
id: uuid.New().String(),
closeC: make(chan struct{}),
@ -114,6 +117,26 @@ func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity,
return nil, trace.Wrap(err)
}
for _, opt := range opts {
if err = opt(ctx, sess, identity, app); err != nil {
return nil, trace.Wrap(err)
}
}
// Put the session chunk in the cache so that upcoming requests can use it for
// 5 minutes or the time until the certificate expires, whichever comes first.
ttl := utils.MinTTL(identity.Expires.Sub(s.c.Clock.Now()), 5*time.Minute)
err = s.cache.set(identity.RouteToApp.SessionID, sess, ttl)
if err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
}
// withJWTTokenForwarder is a sessionOpt that creates a forwarder that attaches
// a generated JWT token to all requests.
func (s *Server) withJWTTokenForwarder(ctx context.Context, sess *sessionChunk, identity *tlsca.Identity, app types.Application) error {
// Request a JWT token that will be attached to all requests.
jwt, err := s.c.AuthClient.GenerateAppToken(ctx, types.GenerateAppTokenRequest{
Username: identity.Username,
@ -122,7 +145,7 @@ func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity,
Expires: identity.Expires,
})
if err != nil {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
// Add JWT token to the traits so it can be used in headers templating.
@ -145,7 +168,7 @@ func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity,
user: identity.Username,
})
if err != nil {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
sess.fwd, err = forward.New(
@ -156,18 +179,16 @@ func (s *Server) newSessionChunk(ctx context.Context, identity *tlsca.Identity,
forward.WebsocketDial(transport.ws.dialer),
)
if err != nil {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
return nil
}
// Put the session chunk in the cache so that upcoming requests can use it for
// 5 minutes or the time until the certificate expires, whichever comes first.
ttl := utils.MinTTL(identity.Expires.Sub(s.c.Clock.Now()), 5*time.Minute)
err = s.cache.set(identity.RouteToApp.SessionID, sess, ttl)
if err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
// withAWSForwarder is a sessionOpt that uses forwarder of the AWS signning
// service.
func (s *Server) withAWSForwarder(ctx context.Context, sess *sessionChunk, identity *tlsca.Identity, app types.Application) error {
sess.fwd = s.awsSigner.Forwarder
return nil
}
// acquire() increments in-flight request count by 1.

View file

@ -803,6 +803,7 @@ func (id Identity) GetUserMetadata() events.UserMetadata {
return events.UserMetadata{
User: id.Username,
Impersonator: id.Impersonator,
AWSRoleARN: id.RouteToApp.AWSRoleARN,
AccessRequests: id.ActiveRequests,
}
}