Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,7 @@ message OneOf {
events.SFTP SFTP = 91;
events.UpgradeWindowStartUpdate UpgradeWindowStartUpdate = 92;
events.AppSessionEnd AppSessionEnd = 93;
events.SessionRecordingAccess SessionRecordingAccess = 94;
}
}

Expand Down Expand Up @@ -3643,3 +3644,22 @@ message UpgradeWindowStartUpdate {
(gogoproto.jsontag) = ""
];
}

// SessionRecordingAccess is emitted when a session recording is accessed, allowing
// session views to be included in the audit log
message SessionRecordingAccess {
// Metadata is a common event metadata.
Metadata Metadata = 1 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// SessionID is the ID of the session.
string SessionID = 2 [(gogoproto.jsontag) = "sid"];
// UserMetadata is a common user event metadata.
UserMetadata UserMetadata = 3 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
}
1,425 changes: 891 additions & 534 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/types/events/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_UpgradeWindowStartUpdate{
UpgradeWindowStartUpdate: e,
}
case *SessionRecordingAccess:
out.Event = &OneOf_SessionRecordingAccess{
SessionRecordingAccess: e,
}
case *Unknown:
out.Event = &OneOf_Unknown{
Unknown: e,
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/reference/audit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ backend uses the local filesystem of an Auth Service host.
For High Availability configurations, users can refer to our
[DynamoDB](./backends.mdx#dynamodb) or [Firestore](./backends.mdx#firestore)
chapters for information on how to configure the SSH events and recorded
sessions to be stored on network storage.
sessions to be stored on network storage.

It is even possible to store audit logs in multiple places at the same time.
For more information on how to configure the audit log, refer to the `storage`
Expand Down Expand Up @@ -129,6 +129,7 @@ The possible event types are:
| session.disk | A list of files opened during the session. *Requires Enhanced Session Recording*. |
| session.network | A list of network connections made during the session. *Requires Enhanced Session Recording*. |
| session.command | A list of commands ran during the session. *Requires Enhanced Session Recording*. |
| session.recording.access | A session recording has been accessed. |
| exec | Remote command has been executed via SSH, like `tsh ssh root@node ls /`. The following fields will be logged: `{"command": "ls /", "exitCode": 0, "exitError": ""}` |
| scp | Remote file copy has been executed. The following fields will be logged: `{"path": "/path/to/file.txt", "len": 32344, "action": "read" }` |
| resize | Terminal has been resized. |
Expand Down
47 changes: 42 additions & 5 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ const (
//
// All spans received will have a `teleport.forwarded.for` attribute added to them with the value being one of
// two things depending on the role of the forwarder:
// 1) User forwarded: `teleport.forwarded.for: alice`
// 2) Instance forwarded: `teleport.forwarded.for: Proxy.clustername:Proxy,Node,Instance`
// 1. User forwarded: `teleport.forwarded.for: alice`
// 2. Instance forwarded: `teleport.forwarded.for: Proxy.clustername:Proxy,Node,Instance`
//
// This allows upstream consumers of the spans to be able to identify forwarded spans and act on them accordingly.
func (a *ServerWithRoles) Export(ctx context.Context, req *collectortracev1.ExportTraceServiceRequest) (*collectortracev1.ExportTraceServiceResponse, error) {
Expand Down Expand Up @@ -3090,6 +3090,18 @@ func (a *ServerWithRoles) GetSessionEvents(namespace string, sid session.ID, aft
return nil, trace.Wrap(err)
}

// emit a session recording view event for the audit log
if err := a.authServer.emitter.EmitAuditEvent(a.authServer.closeCtx, &apievents.SessionRecordingAccess{
Metadata: apievents.Metadata{
Type: events.SessionRecordingAccessEvent,
Code: events.SessionRecordingAccessCode,
},
SessionID: sid.String(),
UserMetadata: a.context.Identity.GetIdentity().GetUserMetadata(),
}); err != nil {
return nil, trace.Wrap(err)
}

return a.alog.GetSessionEvents(namespace, sid, afterN, includePrintEvents)
}

Expand Down Expand Up @@ -4388,10 +4400,35 @@ func (a *ServerWithRoles) ReplaceRemoteLocks(ctx context.Context, clusterName st
// channel if one is encountered. Otherwise the event channel is closed when the stream ends.
// The event channel is not closed on error to prevent race conditions in downstream select statements.
func (a *ServerWithRoles) StreamSessionEvents(ctx context.Context, sessionID session.ID, startIndex int64) (chan apievents.AuditEvent, chan error) {
if err := a.actionForKindSession(apidefaults.Namespace, types.VerbList, sessionID); err != nil {
c, e := make(chan apievents.AuditEvent), make(chan error, 1)
createErrorChannel := func(err error) (chan apievents.AuditEvent, chan error) {
e := make(chan error, 1)
e <- trace.Wrap(err)
return c, e
return nil, e
}

if err := a.actionForKindSession(apidefaults.Namespace, types.VerbList, sessionID); err != nil {
return createErrorChannel(err)
}

// StreamSessionEvents can be called internally, and when that happens we don't want to emit an event.
shouldEmitAuditEvent := true
if role, ok := a.context.Identity.(BuiltinRole); ok {
if role.IsServer() {
shouldEmitAuditEvent = false
}
}

if shouldEmitAuditEvent {
if err := a.authServer.emitter.EmitAuditEvent(a.authServer.closeCtx, &apievents.SessionRecordingAccess{
Metadata: apievents.Metadata{
Type: events.SessionRecordingAccessEvent,
Code: events.SessionRecordingAccessCode,
},
SessionID: sessionID.String(),
UserMetadata: a.context.Identity.GetIdentity().GetUserMetadata(),
}); err != nil {
return createErrorChannel(err)
}
}

return a.alog.StreamSessionEvents(ctx, sessionID, startIndex)
Expand Down
104 changes: 104 additions & 0 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/native"
libdefaults "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
Expand Down Expand Up @@ -1418,6 +1419,109 @@ func TestGetAndList_Nodes(t *testing.T) {
require.Empty(t, cmp.Diff(testResources[0:1], resp.Resources))
}

// TestStreamSessionEvents_User ensures that when a user streams a session's events, it emits an audit event.
func TestStreamSessionEvents_User(t *testing.T) {
t.Parallel()

ctx := context.Background()
srv := newTestTLSServer(t)

username := "user"
user, _, err := CreateUserAndRole(srv.Auth(), username, nil)
require.NoError(t, err)

identity := TestUser(user.GetName())
clt, err := srv.NewClient(identity)
require.NoError(t, err)

// ignore the response as we don't want the events or the error (the session will not exist)
_, _ = clt.StreamSessionEvents(ctx, "44c6cea8-362f-11ea-83aa-125400432324", 0)

// we need to wait for a short period to ensure the event is returned
time.Sleep(500 * time.Millisecond)

searchEvents, _, err := srv.AuthServer.AuditLog.SearchEvents(
srv.Clock().Now().Add(-time.Hour),
srv.Clock().Now().Add(time.Hour),
defaults.Namespace,
[]string{events.SessionRecordingAccessEvent},
1,
types.EventOrderDescending,
"",
)
require.NoError(t, err)

event := searchEvents[0].(*apievents.SessionRecordingAccess)
require.Equal(t, username, event.User)
}

// TestStreamSessionEvents_Builtin ensures that when a builtin role streams a session's events, it does not emit
// an audit event.
func TestStreamSessionEvents_Builtin(t *testing.T) {
t.Parallel()

ctx := context.Background()
srv := newTestTLSServer(t)

identity := TestBuiltin(types.RoleProxy)
clt, err := srv.NewClient(identity)
require.NoError(t, err)

// ignore the response as we don't want the events or the error (the session will not exist)
_, _ = clt.StreamSessionEvents(ctx, "44c6cea8-362f-11ea-83aa-125400432324", 0)

// we need to wait for a short period to ensure the event is returned
time.Sleep(500 * time.Millisecond)

searchEvents, _, err := srv.AuthServer.AuditLog.SearchEvents(
srv.Clock().Now().Add(-time.Hour),
srv.Clock().Now().Add(time.Hour),
defaults.Namespace,
[]string{events.SessionRecordingAccessEvent},
1,
types.EventOrderDescending,
"",
)
require.NoError(t, err)

require.Equal(t, 0, len(searchEvents))
}

// TestGetSessionEvents ensures that when a user streams a session's events, it emits an audit event.
func TestGetSessionEvents(t *testing.T) {
t.Parallel()

srv := newTestTLSServer(t)

username := "user"
user, _, err := CreateUserAndRole(srv.Auth(), username, nil)
require.NoError(t, err)

identity := TestUser(user.GetName())
clt, err := srv.NewClient(identity)
require.NoError(t, err)

// ignore the response as we don't want the events or the error (the session will not exist)
_, _ = clt.GetSessionEvents(defaults.Namespace, "44c6cea8-362f-11ea-83aa-125400432324", 0, false)

// we need to wait for a short period to ensure the event is returned
time.Sleep(500 * time.Millisecond)

searchEvents, _, err := srv.AuthServer.AuditLog.SearchEvents(
srv.Clock().Now().Add(-time.Hour),
srv.Clock().Now().Add(time.Hour),
defaults.Namespace,
[]string{events.SessionRecordingAccessEvent},
1,
types.EventOrderDescending,
"",
)
require.NoError(t, err)

event := searchEvents[0].(*apievents.SessionRecordingAccess)
require.Equal(t, username, event.User)
}

// TestAPILockedOut tests Auth API when there are locks involved.
func TestAPILockedOut(t *testing.T) {
t.Parallel()
Expand Down
3 changes: 3 additions & 0 deletions lib/events/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,9 @@ const (
// is updated. Used only for teleport cloud.
UpgradeWindowStartUpdateEvent = "upgradewindowstart.update"

// SessionRecordingAccessEvent is emitted when a session recording is accessed
SessionRecordingAccessEvent = "session.recording.access"

// UnknownEvent is any event received that isn't recognized as any other event type.
UnknownEvent = apievents.UnknownEvent
)
Expand Down
22 changes: 11 additions & 11 deletions lib/events/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,25 +96,25 @@ const (
SessionUploadCode = "T2005I"
// SessionDataCode is the session data event code.
SessionDataCode = "T2006I"

// AppCreateCode is the app.create event code.
AppCreateCode = "TAP03I"
// AppUpdateCode is the app.update event code.
AppUpdateCode = "TAP04I"
// AppDeleteCode is the app.delete event code.
AppDeleteCode = "TAP05I"

// AppSessionStartCode is the application session start code.
AppSessionStartCode = "T2007I"
// AppSessionEndCode is the application session end event code.
AppSessionEndCode = "T2011I"
// AppSessionChunkCode is the application session chunk create code.
AppSessionChunkCode = "T2008I"
// AppSessionRequestCode is the application request/response code.
AppSessionRequestCode = "T2009I"

// SessionConnectCode is the session connect event code.
SessionConnectCode = "T2010I"
// AppSessionEndCode is the application session end event code.
AppSessionEndCode = "T2011I"
// SessionRecordingAccessCode is the session recording view data event code.
SessionRecordingAccessCode = "T2012I"

// AppCreateCode is the app.create event code.
AppCreateCode = "TAP03I"
// AppUpdateCode is the app.update event code.
AppUpdateCode = "TAP04I"
// AppDeleteCode is the app.delete event code.
AppDeleteCode = "TAP05I"

// DatabaseSessionStartCode is the database session start event code.
DatabaseSessionStartCode = "TDB00I"
Expand Down
2 changes: 2 additions & 0 deletions lib/events/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ func FromEventFields(fields EventFields) (events.AuditEvent, error) {
e = &events.SFTP{}
case UpgradeWindowStartUpdateEvent:
e = &events.UpgradeWindowStartUpdate{}
case SessionRecordingAccessEvent:
e = &events.SessionRecordingAccess{}
case UnknownEvent:
e = &events.Unknown{}
default:
Expand Down