mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 17:23:22 +00:00
AWS session audit log (#13288)
This commit is contained in:
parent
9c7d9134e2
commit
c596dd7d9b
File diff suppressed because it is too large
Load diff
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue