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
25 changes: 24 additions & 1 deletion api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2427,11 +2427,34 @@ message SessionRecordingConfigV2 {
];
}

// KeyLabel combines a label that can be used to identify one or more keys with a keystore type that
// determines where the keys can be found.
message KeyLabel {
// Type represents which keystore should be searched when looking up keys by label.
string type = 1 [(gogoproto.jsontag) = "type"];
// Label is a value that can be used with the related keystore in order to find relevant keys.
string label = 2 [(gogoproto.jsontag) = "label"];
}

// ManualKeyManagementConfig defines whether or not recording encryption keys should be managed externally
// and how to query those keys.
message ManualKeyManagementConfig {
// Enabled controls whether or recording encryption keys should be managed externally.
bool enabled = 1 [(gogoproto.jsontag) = "enabled,omitempty"];
// ActiveKeys describe which keys should be queried for active recording encryption and replay.
repeated KeyLabel active_keys = 2 [(gogoproto.jsontag) = "active_key,omitempty"];
// RotatedKeys describe which keys should be queried for historical replay.
repeated KeyLabel rotated_keys = 3 [(gogoproto.jsontag) = "rotated_key,omitempty"];
}

// SessionRecordingEncryptionConfig configures if and how session recordings
// should be encrypted.
message SessionRecordingEncryptionConfig {
// Enabled controls whether or not session recordings should be encrypted.
bool enabled = 1 [(gogoproto.jsontag) = "enabled"];
bool enabled = 1 [(gogoproto.jsontag) = "enabled,omitempty"];
// ManualKeyManagement defines whether or not recording encryption keys should be managed externally
// and how to query those keys.
ManualKeyManagementConfig manual_key_management = 2 [(gogoproto.jsontag) = "manual_key_management,omitempty"];
}

// SessionRecordingConfigSpecV2 is the actual data we care about
Expand Down
21 changes: 16 additions & 5 deletions api/types/sessionrecording.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type SessionRecordingConfig interface {
// GetEncrypted gets if session recordings should be encrypted or not.
GetEncrypted() bool

// GetEncryptionConfig gets the encryption config from the session recording config.
GetEncryptionConfig() *SessionRecordingEncryptionConfig

// GetEncryptionKeys gets the encryption keys for the session recording config.
GetEncryptionKeys() []*AgeEncryptionKey

Expand All @@ -56,6 +59,8 @@ type SessionRecordingConfig interface {

// Clone returns a copy of the resource.
Clone() SessionRecordingConfig
// CheckAndSetDefaults verifies the constraints for a SessionRecordingConfig
CheckAndSetDefaults() error
}

// NewSessionRecordingConfigFromConfigFile is a convenience method to create
Expand Down Expand Up @@ -176,12 +181,17 @@ func (c *SessionRecordingConfigV2) SetProxyChecksHostKeys(t bool) {

// GetEncrypted gets if session recordings should be encrypted or not.
func (c *SessionRecordingConfigV2) GetEncrypted() bool {
encryption := c.Spec.Encryption
if encryption == nil {
return false
encryption := c.GetEncryptionConfig()
return encryption != nil && encryption.Enabled
}

// GetEncryptionConfig gets the encryption config from the session recording config.
func (c *SessionRecordingConfigV2) GetEncryptionConfig() *SessionRecordingEncryptionConfig {
if c == nil {
return nil
}

return encryption.Enabled
return c.Spec.Encryption
Comment thread
eriktate marked this conversation as resolved.
}

// GetEncryptionKeys gets the encryption keys for the session recording config.
Expand Down Expand Up @@ -218,7 +228,8 @@ func (c *SessionRecordingConfigV2) SetEncryptionKeys(keys iter.Seq[*AgeEncryptio

}

if !keysChanged || len(newKeys) == 0 || len(existingKeys) == len(addedKeys) {
shouldUpdate := len(addedKeys) > 0 && (keysChanged || len(existingKeys) != len(addedKeys))
if !shouldUpdate {
Comment on lines +231 to +232
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously failing to remove keys after rotation

return false
}

Expand Down
187 changes: 187 additions & 0 deletions api/types/sessionrecording_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package types_test

import (
"slices"
"testing"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
)

func TestSetEncryptionKeys(t *testing.T) {
cases := []struct {
name string
initialKeys []*types.AgeEncryptionKey
newKeys []*types.AgeEncryptionKey
expectChange bool
}{
{
name: "adding new keys to empty list",
expectChange: true,
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
}, {
name: "adding new keys to existing list",
expectChange: true,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
{
PublicKey: []byte("789"),
},
},
}, {
name: "replacing existing keys",
expectChange: true,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("321"),
},
{
PublicKey: []byte("654"),
},
},
}, {
name: "removing from existing keys",
expectChange: true,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
{
PublicKey: []byte("789"),
},
},
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
}, {
name: "try to remove all keys",
expectChange: false,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
{
PublicKey: []byte("789"),
},
},
}, {
name: "no change",
expectChange: false,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
}, {
name: "adding duplicates",
expectChange: false,
initialKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
},
newKeys: []*types.AgeEncryptionKey{
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("123"),
},
{
PublicKey: []byte("456"),
},
{
PublicKey: []byte("456"),
},
},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
src := &types.SessionRecordingConfigV2{
Status: &types.SessionRecordingConfigStatus{
EncryptionKeys: c.initialKeys,
},
}

keysChanged := src.SetEncryptionKeys(slices.Values(c.newKeys))
require.Equal(t, c.expectChange, keysChanged)
if keysChanged {
require.Equal(t, c.newKeys, src.Status.EncryptionKeys)
} else {
require.Equal(t, c.initialKeys, src.Status.EncryptionKeys)
}
})
}
}
Loading
Loading