mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 10:10:29 +08:00
adding rotation process to Manager and exposing with new RPCs and (#57577)
checking access to manual keys before persisting and init
This commit is contained in:
@@ -2485,9 +2485,9 @@ 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"];
|
||||
repeated KeyLabel active_keys = 2 [(gogoproto.jsontag) = "active_keys,omitempty"];
|
||||
// RotatedKeys describe which keys should be queried for historical replay.
|
||||
repeated KeyLabel rotated_keys = 3 [(gogoproto.jsontag) = "rotated_key,omitempty"];
|
||||
repeated KeyLabel rotated_keys = 3 [(gogoproto.jsontag) = "rotated_keys,omitempty"];
|
||||
}
|
||||
|
||||
// SessionRecordingEncryptionConfig configures if and how session recordings
|
||||
|
||||
+1523
-1523
File diff suppressed because it is too large
Load Diff
@@ -56,11 +56,11 @@ Optional:
|
||||
|
||||
Optional:
|
||||
|
||||
- `active_key` (Attributes List) ActiveKeys describe which keys should be queried for active recording encryption and replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementactive_key))
|
||||
- `active_keys` (Attributes List) ActiveKeys describe which keys should be queried for active recording encryption and replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementactive_keys))
|
||||
- `enabled` (Boolean) Enabled controls whether or recording encryption keys should be managed externally.
|
||||
- `rotated_key` (Attributes List) RotatedKeys describe which keys should be queried for historical replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementrotated_key))
|
||||
- `rotated_keys` (Attributes List) RotatedKeys describe which keys should be queried for historical replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementrotated_keys))
|
||||
|
||||
### Nested Schema for `spec.encryption.manual_key_management.active_key`
|
||||
### Nested Schema for `spec.encryption.manual_key_management.active_keys`
|
||||
|
||||
Optional:
|
||||
|
||||
@@ -68,7 +68,7 @@ Optional:
|
||||
- `type` (String) Type represents which keystore should be searched when looking up keys by label.
|
||||
|
||||
|
||||
### Nested Schema for `spec.encryption.manual_key_management.rotated_key`
|
||||
### Nested Schema for `spec.encryption.manual_key_management.rotated_keys`
|
||||
|
||||
Optional:
|
||||
|
||||
|
||||
@@ -78,11 +78,11 @@ Optional:
|
||||
|
||||
Optional:
|
||||
|
||||
- `active_key` (Attributes List) ActiveKeys describe which keys should be queried for active recording encryption and replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementactive_key))
|
||||
- `active_keys` (Attributes List) ActiveKeys describe which keys should be queried for active recording encryption and replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementactive_keys))
|
||||
- `enabled` (Boolean) Enabled controls whether or recording encryption keys should be managed externally.
|
||||
- `rotated_key` (Attributes List) RotatedKeys describe which keys should be queried for historical replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementrotated_key))
|
||||
- `rotated_keys` (Attributes List) RotatedKeys describe which keys should be queried for historical replay. (see [below for nested schema](#nested-schema-for-specencryptionmanual_key_managementrotated_keys))
|
||||
|
||||
### Nested Schema for `spec.encryption.manual_key_management.active_key`
|
||||
### Nested Schema for `spec.encryption.manual_key_management.active_keys`
|
||||
|
||||
Optional:
|
||||
|
||||
@@ -90,7 +90,7 @@ Optional:
|
||||
- `type` (String) Type represents which keystore should be searched when looking up keys by label.
|
||||
|
||||
|
||||
### Nested Schema for `spec.encryption.manual_key_management.rotated_key`
|
||||
### Nested Schema for `spec.encryption.manual_key_management.rotated_keys`
|
||||
|
||||
Optional:
|
||||
|
||||
|
||||
@@ -2119,7 +2119,7 @@ func GenSchemaSessionRecordingConfigV2(ctx context.Context) (github_com_hashicor
|
||||
},
|
||||
"manual_key_management": {
|
||||
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
|
||||
"active_key": {
|
||||
"active_keys": {
|
||||
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
|
||||
"label": {
|
||||
Description: "Label is a value that can be used with the related keystore in order to find relevant keys.",
|
||||
@@ -2140,7 +2140,7 @@ func GenSchemaSessionRecordingConfigV2(ctx context.Context) (github_com_hashicor
|
||||
Optional: true,
|
||||
Type: github_com_hashicorp_terraform_plugin_framework_types.BoolType,
|
||||
},
|
||||
"rotated_key": {
|
||||
"rotated_keys": {
|
||||
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
|
||||
"label": {
|
||||
Description: "Label is a value that can be used with the related keystore in order to find relevant keys.",
|
||||
@@ -22635,7 +22635,7 @@ func CopySessionRecordingConfigV2FromTerraform(_ context.Context, tf github_com_
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.Attrs["active_key"]
|
||||
a, ok := tf.Attrs["active_keys"]
|
||||
if !ok {
|
||||
diags.Append(attrReadMissingDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.active_keys"})
|
||||
} else {
|
||||
@@ -22698,7 +22698,7 @@ func CopySessionRecordingConfigV2FromTerraform(_ context.Context, tf github_com_
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.Attrs["rotated_key"]
|
||||
a, ok := tf.Attrs["rotated_keys"]
|
||||
if !ok {
|
||||
diags.Append(attrReadMissingDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.rotated_keys"})
|
||||
} else {
|
||||
@@ -23238,7 +23238,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.AttrTypes["active_key"]
|
||||
a, ok := tf.AttrTypes["active_keys"]
|
||||
if !ok {
|
||||
diags.Append(attrWriteMissingDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.active_keys"})
|
||||
} else {
|
||||
@@ -23246,7 +23246,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
if !ok {
|
||||
diags.Append(attrWriteConversionFailureDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.active_keys", "github.com/hashicorp/terraform-plugin-framework/types.ListType"})
|
||||
} else {
|
||||
c, ok := tf.Attrs["active_key"].(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
c, ok := tf.Attrs["active_keys"].(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
if !ok {
|
||||
c = github_com_hashicorp_terraform_plugin_framework_types.List{
|
||||
|
||||
@@ -23265,7 +23265,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.ActiveKeys))
|
||||
}
|
||||
for k, a := range obj.ActiveKeys {
|
||||
v, ok := tf.Attrs["active_key"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
|
||||
v, ok := tf.Attrs["active_keys"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
|
||||
if !ok {
|
||||
v = github_com_hashicorp_terraform_plugin_framework_types.Object{
|
||||
|
||||
@@ -23335,12 +23335,12 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
}
|
||||
}
|
||||
c.Unknown = false
|
||||
tf.Attrs["active_key"] = c
|
||||
tf.Attrs["active_keys"] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.AttrTypes["rotated_key"]
|
||||
a, ok := tf.AttrTypes["rotated_keys"]
|
||||
if !ok {
|
||||
diags.Append(attrWriteMissingDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.rotated_keys"})
|
||||
} else {
|
||||
@@ -23348,7 +23348,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
if !ok {
|
||||
diags.Append(attrWriteConversionFailureDiag{"SessionRecordingConfigV2.Spec.encryption.manual_key_management.rotated_keys", "github.com/hashicorp/terraform-plugin-framework/types.ListType"})
|
||||
} else {
|
||||
c, ok := tf.Attrs["rotated_key"].(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
c, ok := tf.Attrs["rotated_keys"].(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
if !ok {
|
||||
c = github_com_hashicorp_terraform_plugin_framework_types.List{
|
||||
|
||||
@@ -23367,7 +23367,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.RotatedKeys))
|
||||
}
|
||||
for k, a := range obj.RotatedKeys {
|
||||
v, ok := tf.Attrs["rotated_key"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
|
||||
v, ok := tf.Attrs["rotated_keys"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
|
||||
if !ok {
|
||||
v = github_com_hashicorp_terraform_plugin_framework_types.Object{
|
||||
|
||||
@@ -23437,7 +23437,7 @@ func CopySessionRecordingConfigV2ToTerraform(ctx context.Context, obj *github_co
|
||||
}
|
||||
}
|
||||
c.Unknown = false
|
||||
tf.Attrs["rotated_key"] = c
|
||||
tf.Attrs["rotated_keys"] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -267,11 +267,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (as *Server, err error) {
|
||||
}
|
||||
|
||||
recordingEncryptionManager, err := recordingencryption.NewManager(closeCtx, recordingencryption.ManagerConfig{
|
||||
Backend: localRecordingEncryption,
|
||||
Cache: localRecordingEncryption,
|
||||
ClusterConfig: cfg.ClusterConfiguration,
|
||||
KeyStore: cfg.KeyStore,
|
||||
Logger: cfg.Logger,
|
||||
Backend: localRecordingEncryption,
|
||||
Cache: localRecordingEncryption,
|
||||
ClusterConfig: cfg.ClusterConfiguration,
|
||||
KeyStore: cfg.KeyStore,
|
||||
Logger: cfg.Logger,
|
||||
InitialSessionRecordingConfig: cfg.SessionRecordingConfig,
|
||||
LockConfig: backend.RunWhileLockedConfig{
|
||||
LockConfiguration: backend.LockConfiguration{
|
||||
Backend: cfg.Backend,
|
||||
@@ -281,7 +282,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (as *Server, err error) {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
return nil, trace.Wrap(err, "initializing session recording encryption")
|
||||
}
|
||||
|
||||
cfg.RecordingEncryption = recordingEncryptionManager
|
||||
|
||||
@@ -5824,6 +5824,7 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
|
||||
recordingEncryptionService, err := recordingencryptionv1.NewService(recordingencryptionv1.ServiceConfig{
|
||||
Authorizer: cfg.Authorizer,
|
||||
Uploader: cfg.AuthServer.Services,
|
||||
KeyRotater: cfg.AuthServer.Services,
|
||||
Logger: cfg.AuthServer.logger.With(teleport.ComponentKey, teleport.ComponentRecordingEncryption),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
|
||||
"github.com/gravitational/teleport/lib/auth/migration"
|
||||
"github.com/gravitational/teleport/lib/auth/recordingencryption"
|
||||
"github.com/gravitational/teleport/lib/auth/recordingencryption/recordingencryptionv1"
|
||||
"github.com/gravitational/teleport/lib/auth/state"
|
||||
"github.com/gravitational/teleport/lib/auth/summarizer"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
@@ -92,6 +93,7 @@ type VersionStorage interface {
|
||||
type RecordingEncryptionManager interface {
|
||||
services.RecordingEncryption
|
||||
recordingencryption.KeyUnwrapper
|
||||
recordingencryptionv1.KeyRotater
|
||||
SetCache(cache recordingencryption.Cache)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,14 @@ type Cache interface {
|
||||
|
||||
// ManagerConfig captures all of the dependencies required to instantiate a Manager.
|
||||
type ManagerConfig struct {
|
||||
Backend services.RecordingEncryption
|
||||
ClusterConfig services.ClusterConfigurationInternal
|
||||
KeyStore KeyStore
|
||||
Cache Cache
|
||||
Logger *slog.Logger
|
||||
LockConfig backend.RunWhileLockedConfig
|
||||
ManualKeyManagementConfig *types.ManualKeyManagementConfig
|
||||
Backend services.RecordingEncryption
|
||||
ClusterConfig services.ClusterConfigurationInternal
|
||||
KeyStore KeyStore
|
||||
Cache Cache
|
||||
Logger *slog.Logger
|
||||
LockConfig backend.RunWhileLockedConfig
|
||||
ManualKeyManagementConfig *types.ManualKeyManagementConfig
|
||||
InitialSessionRecordingConfig types.SessionRecordingConfig
|
||||
}
|
||||
|
||||
// NewManager returns a new Manager using the given [ManagerConfig].
|
||||
@@ -79,7 +80,7 @@ func NewManager(ctx context.Context, cfg ManagerConfig) (*Manager, error) {
|
||||
cfg.Logger = slog.With(teleport.ComponentKey, "recording-encryption-manager")
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
mgr := &Manager{
|
||||
RecordingEncryption: cfg.Backend,
|
||||
ClusterConfigurationInternal: cfg.ClusterConfig,
|
||||
|
||||
@@ -89,7 +90,18 @@ func NewManager(ctx context.Context, cfg ManagerConfig) (*Manager, error) {
|
||||
lockConfig: cfg.LockConfig,
|
||||
logger: cfg.Logger,
|
||||
manualKeyConfig: cfg.ManualKeyManagementConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if cfg.InitialSessionRecordingConfig != nil {
|
||||
manualKeyCfg := getManualKeyManagement(cfg.InitialSessionRecordingConfig.GetEncryptionConfig())
|
||||
if manualKeyCfg != nil {
|
||||
if _, err := mgr.ensureManualEncryptionKeys(ctx, *manualKeyCfg); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// A Manager wraps a services.RecordingEncryption and KeyStore in order to provide more complex operations
|
||||
@@ -111,25 +123,11 @@ type Manager struct {
|
||||
// CreateSessionRecordingConfig creates a new session recording configuration. If encryption is enabled then an
|
||||
// accessible encryption key pair will be confirmed. Either creating one if none exists, doing nothing if one is
|
||||
// accessible, or returning an error if none are accessible.
|
||||
func (m *Manager) CreateSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (sessionRecordingConfig types.SessionRecordingConfig, err error) {
|
||||
err = backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
encryptionCfg := cfg.GetEncryptionConfig()
|
||||
if encryptionCfg != nil && encryptionCfg.Enabled {
|
||||
encryption, err := m.ensureRecordingEncryptionKey(ctx, *encryptionCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = cfg.SetEncryptionKeys(getAgeEncryptionKeys(encryption.GetSpec().ActiveKeyPairs))
|
||||
}
|
||||
|
||||
sessionRecordingConfig, err = m.ClusterConfigurationInternal.CreateSessionRecordingConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
func (m *Manager) CreateSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (types.SessionRecordingConfig, error) {
|
||||
sessionRecordingConfig, err := m.modifySessionRecordingConfig(ctx, cfg, m.ClusterConfigurationInternal.CreateSessionRecordingConfig)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return sessionRecordingConfig, trace.Wrap(err)
|
||||
}
|
||||
@@ -137,25 +135,11 @@ func (m *Manager) CreateSessionRecordingConfig(ctx context.Context, cfg types.Se
|
||||
// UpdateSessionRecordingConfig updates an existing session recording configuration. If encryption is enabled
|
||||
// then an accessible encryption key pair will be confirmed. Either creating one if none exists, doing nothing
|
||||
// if one is accessible, or returning an error if none are accessible.
|
||||
func (m *Manager) UpdateSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (sessionRecordingConfig types.SessionRecordingConfig, err error) {
|
||||
err = backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
encryptionCfg := cfg.GetEncryptionConfig()
|
||||
if encryptionCfg != nil && encryptionCfg.Enabled {
|
||||
encryption, err := m.ensureRecordingEncryptionKey(ctx, *encryptionCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = cfg.SetEncryptionKeys(getAgeEncryptionKeys(encryption.GetSpec().ActiveKeyPairs))
|
||||
}
|
||||
|
||||
sessionRecordingConfig, err = m.ClusterConfigurationInternal.UpdateSessionRecordingConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
func (m *Manager) UpdateSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (types.SessionRecordingConfig, error) {
|
||||
sessionRecordingConfig, err := m.modifySessionRecordingConfig(ctx, cfg, m.ClusterConfigurationInternal.UpdateSessionRecordingConfig)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return sessionRecordingConfig, trace.Wrap(err)
|
||||
}
|
||||
@@ -163,25 +147,11 @@ func (m *Manager) UpdateSessionRecordingConfig(ctx context.Context, cfg types.Se
|
||||
// UpsertSessionRecordingConfig creates a new session recording configuration or overwrites an existing one. If
|
||||
// encryption is enabled then an accessible encryption key pair will be confirmed. Either creating one if none
|
||||
// exists, doing nothing if one is accessible, or returning an error if none are accessible.
|
||||
func (m *Manager) UpsertSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (sessionRecordingConfig types.SessionRecordingConfig, err error) {
|
||||
err = backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
encryptionCfg := cfg.GetEncryptionConfig()
|
||||
if encryptionCfg != nil && encryptionCfg.Enabled {
|
||||
encryption, err := m.ensureRecordingEncryptionKey(ctx, *encryptionCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = cfg.SetEncryptionKeys(getAgeEncryptionKeys(encryption.GetSpec().ActiveKeyPairs))
|
||||
}
|
||||
|
||||
sessionRecordingConfig, err = m.ClusterConfigurationInternal.UpsertSessionRecordingConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
func (m *Manager) UpsertSessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig) (types.SessionRecordingConfig, error) {
|
||||
sessionRecordingConfig, err := m.modifySessionRecordingConfig(ctx, cfg, m.ClusterConfigurationInternal.UpsertSessionRecordingConfig)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return sessionRecordingConfig, trace.Wrap(err)
|
||||
}
|
||||
@@ -191,28 +161,42 @@ func (m *Manager) SetCache(cache Cache) {
|
||||
m.cache = cache
|
||||
}
|
||||
|
||||
// fingerprintedDecrypter wraps a crypto.Decrypter with its public key fingerprint.
|
||||
type fingerprintedDecrypter struct {
|
||||
fingerprint string
|
||||
decrypter crypto.Decrypter
|
||||
}
|
||||
|
||||
func (m *Manager) ensureManualEncryptionKeys(manualKeyCfg types.ManualKeyManagementConfig) (*recordingencryptionv1.RecordingEncryption, error) {
|
||||
// ensureManualEncryptionKeys searches for accessible encryption keys in the configured key store using the key labels
|
||||
// configured in the [ManualKeyManagementConfig]
|
||||
func (m *Manager) ensureManualEncryptionKeys(ctx context.Context, manualKeyCfg types.ManualKeyManagementConfig) (*recordingencryptionv1.RecordingEncryption, error) {
|
||||
m.manualKeyConfig = &manualKeyCfg
|
||||
activeLabels := manualKeyCfg.ActiveKeys
|
||||
rotatedLabels := manualKeyCfg.RotatedKeys
|
||||
|
||||
// using the Manager's context here because we cache the resulting keys and want their lifetimes
|
||||
// to be at least as long as the Manager
|
||||
activeDecrypters, err := m.keyStore.FindDecryptersByLabels(m.ctx, activeLabels...)
|
||||
activeDecrypters, err := m.keyStore.FindDecryptersByLabels(ctx, activeLabels...)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
rotatedDecrypters, err := m.keyStore.FindDecryptersByLabels(m.ctx, rotatedLabels...)
|
||||
if len(activeDecrypters) == 0 {
|
||||
return nil, trace.BadParameter("No accessible key found for manual_key_management.active_keys")
|
||||
}
|
||||
|
||||
rotatedDecrypters, err := m.keyStore.FindDecryptersByLabels(ctx, rotatedLabels...)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if len(rotatedDecrypters) == 0 && len(rotatedLabels) > 0 {
|
||||
// Failure to retrieve rotated keys should only prevent replay of historical recordings
|
||||
// related to those keys, which isn't catastrophic in the way that missing active keys are.
|
||||
// We should log the failure but otherwise press on.
|
||||
m.logger.WarnContext(ctx, "No accessible rotated keys were found which may prevent replaying historical session recordings. Please ensure the Teleport Auth Service has access to the keystore and keys defined in manual_key_management.rotated_keys")
|
||||
}
|
||||
|
||||
var fingerprinted []fingerprintedDecrypter
|
||||
for _, decrypter := range slices.Concat(rotatedDecrypters, activeDecrypters) {
|
||||
fp, err := Fingerprint(decrypter.Public())
|
||||
@@ -252,58 +236,101 @@ func (m *Manager) ensureManualEncryptionKeys(manualKeyCfg types.ManualKeyManagem
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ensureRecordingEncryptionKey returns the configured RecordingEncryption resource if it exists with an
|
||||
// accessible key. If no keys exist, a new key pair will be provisioned. An error is returned if keys exist
|
||||
// but none are accessible.
|
||||
func (m *Manager) ensureRecordingEncryptionKey(ctx context.Context, encryptionCfg types.SessionRecordingEncryptionConfig) (*recordingencryptionv1.RecordingEncryption, error) {
|
||||
if encryptionCfg.ManualKeyManagement != nil && encryptionCfg.ManualKeyManagement.Enabled {
|
||||
return m.ensureManualEncryptionKeys(*encryptionCfg.ManualKeyManagement)
|
||||
}
|
||||
|
||||
m.manualKeyConfig = nil
|
||||
persistFn := m.RecordingEncryption.UpdateRecordingEncryption
|
||||
encryption, err := m.RecordingEncryption.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
return encryption, trace.Wrap(err)
|
||||
}
|
||||
encryption = &recordingencryptionv1.RecordingEncryption{
|
||||
Spec: &recordingencryptionv1.RecordingEncryptionSpec{},
|
||||
}
|
||||
persistFn = m.RecordingEncryption.CreateRecordingEncryption
|
||||
}
|
||||
|
||||
activePairs := encryption.GetSpec().ActiveKeyPairs
|
||||
// ensureActiveKeyPair checks that there is at least one accessible key in the list of active pairs given.
|
||||
// If there are no active keys found, a new one will be provisioned and returned with the original set.
|
||||
// An active but inaccessible key results in an error.
|
||||
func (m *Manager) ensureActiveKeyPair(ctx context.Context, activePairs []*recordingencryptionv1.KeyPair) (newActivePairs []*recordingencryptionv1.KeyPair, keyGenerated bool, err error) {
|
||||
var foundActiveKey bool
|
||||
if len(activePairs) > 0 {
|
||||
for _, pair := range activePairs {
|
||||
if pair.State != recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE {
|
||||
continue
|
||||
}
|
||||
|
||||
foundActiveKey = true
|
||||
// fetch the decrypter to ensure we have access to it
|
||||
if _, err := m.keyStore.GetDecrypter(ctx, pair.KeyPair); err != nil {
|
||||
fp, _ := fingerprintPEM(pair.KeyPair.PublicKey)
|
||||
m.logger.DebugContext(ctx, "key not accessible", "fingerprint", fp)
|
||||
continue
|
||||
}
|
||||
return encryption, nil
|
||||
return activePairs, false, nil
|
||||
}
|
||||
|
||||
return nil, trace.AccessDenied("active key not accessible: %v", err)
|
||||
// during a key rotation there may be no active keys which would mean reaching
|
||||
// this point isn't an error
|
||||
if foundActiveKey {
|
||||
return nil, false, trace.AccessDenied("active key not accessible")
|
||||
}
|
||||
}
|
||||
|
||||
// no keys present, need to generate the initial active keypair
|
||||
// no active keys present, need to generate one
|
||||
encryptionPair, err := m.keyStore.NewEncryptionKeyPair(ctx, cryptosuites.RecordingKeyWrapping)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "generating wrapping key")
|
||||
return nil, false, trace.Wrap(err, "generating wrapping key")
|
||||
}
|
||||
|
||||
wrappedKey := recordingencryptionv1.KeyPair{
|
||||
fp, _ := fingerprintPEM(encryptionPair.PublicKey)
|
||||
m.logger.InfoContext(ctx, "no active encryption keys, generated new pair", "public_fingerprint", fp)
|
||||
|
||||
return append(activePairs, &recordingencryptionv1.KeyPair{
|
||||
KeyPair: encryptionPair,
|
||||
State: recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE,
|
||||
}), true, nil
|
||||
}
|
||||
|
||||
// resolveRecordingEncryption returns the configured [RecordingEncryption] resource if it exists with an
|
||||
// accessible key. If no active keys can be found and manual key management is disabled, a new key pair
|
||||
// will be provisioned. An error is returned if an active key is found but is not accessible. It is also
|
||||
// possible for the returned [RecordingEncryption] to be nil if the [SessionRecordingConfig] disables
|
||||
// encryption altogether.
|
||||
func (m *Manager) resolveRecordingEncryption(ctx context.Context, sessionRecordingCfg types.SessionRecordingConfig, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
|
||||
// If encryption is disabled then we can skip resolving the RecordingEncryption.
|
||||
if !sessionRecordingCfg.GetEncrypted() {
|
||||
return encryption, nil
|
||||
}
|
||||
encryption.Spec.ActiveKeyPairs = []*recordingencryptionv1.KeyPair{&wrappedKey}
|
||||
encryptionCfg := sessionRecordingCfg.GetEncryptionConfig()
|
||||
manualKeyCfg := getManualKeyManagement(encryptionCfg)
|
||||
if manualKeyCfg != nil {
|
||||
encryption, err := m.ensureManualEncryptionKeys(ctx, *manualKeyCfg)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return encryption, nil
|
||||
}
|
||||
|
||||
m.manualKeyConfig = nil
|
||||
persistFn := m.RecordingEncryption.UpdateRecordingEncryption
|
||||
forcePersist := encryption != nil
|
||||
if encryption == nil {
|
||||
var err error
|
||||
encryption, err = m.RecordingEncryption.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
encryption = &recordingencryptionv1.RecordingEncryption{
|
||||
Spec: &recordingencryptionv1.RecordingEncryptionSpec{},
|
||||
}
|
||||
persistFn = m.RecordingEncryption.CreateRecordingEncryption
|
||||
}
|
||||
}
|
||||
|
||||
activePairs, keyGenerated, err := m.ensureActiveKeyPair(ctx, encryption.GetSpec().GetActiveKeyPairs())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if !forcePersist && !keyGenerated {
|
||||
return encryption, nil
|
||||
}
|
||||
|
||||
encryption.Spec.ActiveKeyPairs = activePairs
|
||||
encryption, err = persistFn(ctx, encryption)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
fp, _ := fingerprintPEM(encryptionPair.PublicKey)
|
||||
m.logger.InfoContext(ctx, "no active keys, generated initial recording encryption pair", "public_fingerprint", fp)
|
||||
return encryption, nil
|
||||
}
|
||||
|
||||
@@ -316,6 +343,54 @@ func (m *Manager) unwrapKeyUsingCache(in UnwrapInput) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Manager) findDecrypter(ctx context.Context, fingerprint string) (crypto.Decrypter, error) {
|
||||
encryption, err := m.cache.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
for _, key := range activePairs {
|
||||
if key.GetKeyPair() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
activeFP, err := fingerprintPEM(key.KeyPair.PublicKey)
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to fingerprint active public key", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if activeFP != fingerprint {
|
||||
continue
|
||||
}
|
||||
|
||||
decrypter, err := m.keyStore.GetDecrypter(ctx, key.KeyPair)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return decrypter, nil
|
||||
}
|
||||
|
||||
// fallback to rotated keys if decrypter isn't found in active keys
|
||||
rotatedKey, err := m.GetRotatedKey(ctx, fingerprint)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
decrypter, err := m.keyStore.GetDecrypter(ctx, rotatedKey.GetSpec().GetEncryptionKeyPair())
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return nil, trace.NotFound("no accessible decrypter found for fingerprint %q", fingerprint)
|
||||
}
|
||||
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return decrypter, nil
|
||||
}
|
||||
|
||||
// UnwrapKey searches for the private key compatible with the provided public key fingerprint and uses it to unwrap
|
||||
// a wrapped file key.
|
||||
func (m *Manager) UnwrapKey(ctx context.Context, in UnwrapInput) ([]byte, error) {
|
||||
@@ -326,7 +401,7 @@ func (m *Manager) UnwrapKey(ctx context.Context, in UnwrapInput) ([]byte, error)
|
||||
|
||||
// a cache miss or unwrap failure for manually managed keys needs to attempt a refresh and try again
|
||||
if m.manualKeyConfig != nil && m.manualKeyConfig.Enabled {
|
||||
if _, err := m.ensureManualEncryptionKeys(*m.manualKeyConfig); err != nil {
|
||||
if _, err := m.ensureManualEncryptionKeys(ctx, *m.manualKeyConfig); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -339,42 +414,174 @@ func (m *Manager) UnwrapKey(ctx context.Context, in UnwrapInput) ([]byte, error)
|
||||
m.logger.WarnContext(ctx, "failed to unwrap file key using cached decrypter, refetching from keystore")
|
||||
}
|
||||
|
||||
encryption, err := m.cache.GetRecordingEncryption(ctx)
|
||||
decrypter, err := m.findDecrypter(ctx, in.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
}
|
||||
|
||||
fileKey, err = decrypter.Decrypt(in.Rand, in.WrappedKey, in.Opts)
|
||||
return fileKey, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// RotateKey marks the currently active key pair as 'rotating' and adds a new pair to the active set.
|
||||
func (m *Manager) RotateKey(ctx context.Context) error {
|
||||
sessionRecordingConfig, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := shouldManageKeys(sessionRecordingConfig); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
encryption, err := m.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
var activePair *recordingencryptionv1.KeyPair
|
||||
for _, pair := range activePairs {
|
||||
switch pair.GetState() {
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING:
|
||||
return trace.AlreadyExists("Recording encryption key rotation already in progress, complete or rollback to start a new one")
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE:
|
||||
return trace.BadParameter("Failed recording encryption key rotation in progress, at least one Teleport Auth Service does not have access to the new key pair. Please rollback the rotation, ensure that all Auth Services have access to provision and use keys within the configured keystore, and then try again.")
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE:
|
||||
activePair = pair
|
||||
}
|
||||
}
|
||||
|
||||
if activePair == nil {
|
||||
return trace.NotFound("no active key present to rotate")
|
||||
}
|
||||
|
||||
activePair.State = recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING
|
||||
if _, err := m.modifyRecordingEncryption(ctx, encryption); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteRotation creates a new [RotatedKey] resource for rotating key pair and removes them from the active set.
|
||||
func (m *Manager) CompleteRotation(ctx context.Context) error {
|
||||
sessionRecordingConfig, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := shouldManageKeys(sessionRecordingConfig); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
encryption, err := m.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
var remainingPairs []*recordingencryptionv1.KeyPair
|
||||
var rotatedPairs []*recordingencryptionv1.KeyPair
|
||||
for _, pair := range activePairs {
|
||||
switch pair.GetState() {
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING:
|
||||
rotatedPairs = append(rotatedPairs, pair)
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE:
|
||||
remainingPairs = append(remainingPairs, pair)
|
||||
case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE:
|
||||
return trace.BadParameter("Failed recording encryption key rotation in progress, at least one Teleport Auth Service does not have access to the new key pair. Please rollback the rotation, ensure that all Auth Services have access to provision and use keys within the configured keystore, and then try again.")
|
||||
}
|
||||
}
|
||||
|
||||
for _, pair := range rotatedPairs {
|
||||
if _, err := m.CreateRotatedKey(ctx, pair.GetKeyPair()); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
encryption.Spec.ActiveKeyPairs = remainingPairs
|
||||
if _, err := m.modifyRecordingEncryption(ctx, encryption); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackRotation reverts an in progress rotation by setting 'rotating' keys back to 'active' and removing
|
||||
// all other keys from the active set.
|
||||
func (m *Manager) RollbackRotation(ctx context.Context) error {
|
||||
sessionRecordingConfig, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := shouldManageKeys(sessionRecordingConfig); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
encryption, err := m.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
var rollbackPairs []*recordingencryptionv1.KeyPair
|
||||
for _, pair := range activePairs {
|
||||
if pair.GetState() == recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING {
|
||||
pair.State = recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE
|
||||
rollbackPairs = append(rollbackPairs, pair)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rollbackPairs) == 0 {
|
||||
return trace.BadParameter("skipping rollback that would remove all encryption keys")
|
||||
}
|
||||
|
||||
encryption.Spec.ActiveKeyPairs = rollbackPairs
|
||||
if _, err := m.modifyRecordingEncryption(ctx, encryption); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRotationState returns the state of each active key identified by a fingerprint.
|
||||
func (m *Manager) GetRotationState(ctx context.Context) ([]*recordingencryptionv1.FingerprintWithState, error) {
|
||||
sessionRecordingConfig, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// TODO (eriktate): search rotated keys as well once rotation is implemented
|
||||
activePairs := encryption.GetSpec().ActiveKeyPairs
|
||||
for _, key := range activePairs {
|
||||
if key.GetKeyPair() == nil {
|
||||
continue
|
||||
}
|
||||
if err := shouldManageKeys(sessionRecordingConfig); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
activeFP, err := fingerprintPEM(key.KeyPair.PublicKey)
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to fingerprint active public key", "error", err)
|
||||
continue
|
||||
}
|
||||
encryption, err := m.GetRecordingEncryption(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if activeFP != in.Fingerprint {
|
||||
continue
|
||||
}
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
if len(activePairs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
decrypter, err := m.keyStore.GetDecrypter(ctx, key.KeyPair)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileKey, err := decrypter.Decrypt(in.Rand, in.WrappedKey, in.Opts)
|
||||
states := make([]*recordingencryptionv1.FingerprintWithState, 0, len(activePairs))
|
||||
for _, pair := range activePairs {
|
||||
fingerprint, err := fingerprintPEM(pair.GetKeyPair().PublicKey)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return fileKey, nil
|
||||
states = append(states, &recordingencryptionv1.FingerprintWithState{
|
||||
Fingerprint: fingerprint,
|
||||
State: pair.GetState(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil, trace.NotFound("no accessible decrypter found")
|
||||
return states, nil
|
||||
}
|
||||
|
||||
// Watch for changes in the recording_encryption resource and respond by ensuring access to keys.
|
||||
@@ -401,7 +608,7 @@ func (m *Manager) Watch(ctx context.Context, events types.Events) (err error) {
|
||||
}()
|
||||
|
||||
// on initial startup we should try to immediately resolve recording encryption
|
||||
if err := m.resolveRecordingEncryption(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
if err := m.resolveRecordingEncryptionState(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
m.logger.ErrorContext(ctx, "initial attempt to resolve recording encryption failed", "error", err)
|
||||
}
|
||||
|
||||
@@ -431,7 +638,7 @@ func (m *Manager) Watch(ctx context.Context, events types.Events) (err error) {
|
||||
if ev.Type != types.OpPut || ev.Resource.GetKind() != types.KindRecordingEncryption {
|
||||
continue
|
||||
}
|
||||
if err := m.resolveRecordingEncryption(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
if err := m.resolveRecordingEncryptionState(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
m.logger.ErrorContext(ctx, "failure handling recording encryption event", "kind", ev.Resource.GetKind(), "error", err)
|
||||
continue
|
||||
}
|
||||
@@ -444,7 +651,7 @@ func (m *Manager) Watch(ctx context.Context, events types.Events) (err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.resolveRecordingEncryption(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
if err := m.resolveRecordingEncryptionState(ctx, shouldRetryAfterJitterFn); err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed interval sync of recording encryption keys", "error", err)
|
||||
}
|
||||
case <-watch.Done():
|
||||
@@ -465,37 +672,67 @@ func (m *Manager) Watch(ctx context.Context, events types.Events) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) resolveRecordingEncryption(ctx context.Context, shouldRetryFn func() bool) error {
|
||||
// modifySessionRecordingConfig modifies the [SessionRecordingConfig] while maintaining the correct
|
||||
// [RecordingEncryption] state. Any changes to active encryption keys are also applied to the [SessionRecordingConfig]
|
||||
// before applying the change.
|
||||
func (m *Manager) modifySessionRecordingConfig(ctx context.Context, cfg types.SessionRecordingConfig, persistFn func(context.Context, types.SessionRecordingConfig) (types.SessionRecordingConfig, error)) (types.SessionRecordingConfig, error) {
|
||||
var result types.SessionRecordingConfig
|
||||
err := backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
encryption, err := m.resolveRecordingEncryption(ctx, cfg, nil)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
_ = cfg.SetEncryptionKeys(getAgeEncryptionKeys(encryption.GetSpec().GetActiveKeyPairs()))
|
||||
result, err = persistFn(ctx, cfg)
|
||||
return trace.Wrap(err)
|
||||
})
|
||||
|
||||
return result, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// modifyRecordingEncryption modifies the [RecordingEncryption] if the fetched [SessionRecordingConfig] allows it.
|
||||
// Any changes to active encryption keys are also applied to the [SessionRecordingConfig] before releasing the lock.
|
||||
func (m *Manager) modifyRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
|
||||
var result *recordingencryptionv1.RecordingEncryption
|
||||
err := backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
sessionRecordingCfg, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
result, err = m.resolveRecordingEncryption(ctx, sessionRecordingCfg, encryption)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if sessionRecordingCfg.SetEncryptionKeys(getAgeEncryptionKeys(result.GetSpec().GetActiveKeyPairs())) {
|
||||
if _, err = m.ClusterConfigurationInternal.UpdateSessionRecordingConfig(ctx, sessionRecordingCfg); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// resolveRecordingEncryptionState fetches the latest [SessionRecordingConfig] and [RecordingEncryption] resources
|
||||
// from the backend and applies any updates the calling auth instance might require. This is meant to be run on an
|
||||
// interval or in response to watch events. The primary updates expected are key state changes for inaccessible
|
||||
// encryption keys and refreshing the manual key management cache. If the current state cannot be resolved after a
|
||||
// number of retries, a [trace.LimitExceededError] is returned.
|
||||
func (m *Manager) resolveRecordingEncryptionState(ctx context.Context, shouldRetryFn func() bool) error {
|
||||
const retries = 3
|
||||
for retry := range retries {
|
||||
err := backend.RunWhileLocked(ctx, m.lockConfig, func(ctx context.Context) error {
|
||||
sessionRecordingConfig, err := m.GetSessionRecordingConfig(ctx)
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to retrieve session_recording_config, retrying", "error", err)
|
||||
return trace.Wrap(err)
|
||||
if _, err := m.modifyRecordingEncryption(ctx, nil); err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to resolve recording encryption keys, retrying", "retry", retry, "retries_left", retries-retry, "error", err)
|
||||
if shouldRetryFn() {
|
||||
continue
|
||||
}
|
||||
|
||||
encryptionCfg := sessionRecordingConfig.GetEncryptionConfig()
|
||||
if encryptionCfg == nil || !encryptionCfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
encryption, err := m.ensureRecordingEncryptionKey(ctx, *encryptionCfg)
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to resolve recording encryption keys, retrying", "retry", retry, "retries_left", retries-retry, "error", err)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if sessionRecordingConfig.SetEncryptionKeys(getAgeEncryptionKeys(encryption.GetSpec().ActiveKeyPairs)) {
|
||||
if _, err := m.ClusterConfigurationInternal.UpdateSessionRecordingConfig(ctx, sessionRecordingConfig); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil && shouldRetryFn() {
|
||||
continue
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -522,6 +759,31 @@ func getAgeEncryptionKeys(keys []*recordingencryptionv1.KeyPair) iter.Seq[*types
|
||||
}
|
||||
}
|
||||
|
||||
func getManualKeyManagement(encryptionCfg *types.SessionRecordingEncryptionConfig) *types.ManualKeyManagementConfig {
|
||||
if encryptionCfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if encryptionCfg.ManualKeyManagement == nil || !encryptionCfg.ManualKeyManagement.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return encryptionCfg.ManualKeyManagement
|
||||
}
|
||||
|
||||
func shouldManageKeys(sessionRecordingConfig types.SessionRecordingConfig) error {
|
||||
encryptionCfg := sessionRecordingConfig.GetEncryptionConfig()
|
||||
if encryptionCfg == nil || !encryptionCfg.Enabled {
|
||||
return trace.NotFound("session recording encryption is not enabled")
|
||||
}
|
||||
|
||||
if manual := getManualKeyManagement(encryptionCfg); manual != nil && manual.Enabled {
|
||||
return trace.NotFound("session recording encryption is using manual key management")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fingerprint a public key for use in logging and as a cache key.
|
||||
func Fingerprint(pubKey crypto.PublicKey) (string, error) {
|
||||
derPub, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
@@ -73,6 +75,10 @@ func newFakeKeyStore(keyType types.PrivateKeyType) *fakeKeyStore {
|
||||
return &fakeKeyStore{
|
||||
keys: make(map[string][]crypto.Decrypter),
|
||||
keyType: keyType,
|
||||
currLabel: types.KeyLabel{
|
||||
Type: "pkcs11",
|
||||
Label: "test",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,3 +439,217 @@ func TestUnwrapKey(t *testing.T) {
|
||||
|
||||
require.Equal(t, fileKey, unwrappedKey)
|
||||
}
|
||||
|
||||
func TestRotateKey(t *testing.T) {
|
||||
ctx, bk := newLocalBackend(t)
|
||||
|
||||
config := newManagerConfig(t, bk, types.PrivateKeyType_RAW)
|
||||
manager, err := recordingencryption.NewManager(ctx, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &types.SessionRecordingConfigV2{}
|
||||
require.NoError(t, req.CheckAndSetDefaults())
|
||||
req.Spec.Encryption = &types.SessionRecordingEncryptionConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// setup initial state
|
||||
src, err := manager.CreateSessionRecordingConfig(ctx, req)
|
||||
require.NoError(t, err)
|
||||
encryptionKeys := src.GetEncryptionKeys()
|
||||
require.Len(t, encryptionKeys, 1)
|
||||
|
||||
encryption, err := config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 1)
|
||||
initialKey := activePairs[0]
|
||||
require.NotNil(t, initialKey.KeyPair)
|
||||
require.NotEmpty(t, initialKey.KeyPair.PrivateKey)
|
||||
require.NotEmpty(t, initialKey.KeyPair.PublicKey)
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, initialKey.State)
|
||||
|
||||
// rotate key
|
||||
err = manager.RotateKey(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
encryption, err = config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs = encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 2)
|
||||
|
||||
var foundInitialPair bool
|
||||
var newPair *recordingencryptionv1.KeyPair
|
||||
for _, pair := range activePairs {
|
||||
if slices.Equal(initialKey.KeyPair.PublicKey, pair.KeyPair.PublicKey) {
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING, pair.State)
|
||||
foundInitialPair = true
|
||||
} else {
|
||||
newPair = pair
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, pair.State)
|
||||
}
|
||||
}
|
||||
|
||||
require.True(t, foundInitialPair && newPair != nil)
|
||||
|
||||
// complete rotation
|
||||
err = manager.CompleteRotation(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
encryption, err = config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs = encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 1)
|
||||
|
||||
require.Equal(t, newPair.KeyPair.PublicKey, activePairs[0].KeyPair.PublicKey)
|
||||
require.Equal(t, newPair.KeyPair.PrivateKey, activePairs[0].KeyPair.PrivateKey)
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, activePairs[0].State)
|
||||
|
||||
pubKey, err := x509.ParsePKIXPublicKey(initialKey.KeyPair.PublicKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
fingerprint, err := recordingencryption.Fingerprint(pubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
rotatedKey, err := config.Backend.GetRotatedKey(ctx, fingerprint)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, initialKey.KeyPair.PublicKey, rotatedKey.Spec.EncryptionKeyPair.PublicKey)
|
||||
require.Equal(t, initialKey.KeyPair.PrivateKey, rotatedKey.Spec.EncryptionKeyPair.PrivateKey)
|
||||
}
|
||||
|
||||
func TestRotateRollback(t *testing.T) {
|
||||
ctx, bk := newLocalBackend(t)
|
||||
|
||||
config := newManagerConfig(t, bk, types.PrivateKeyType_RAW)
|
||||
manager, err := recordingencryption.NewManager(ctx, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &types.SessionRecordingConfigV2{}
|
||||
require.NoError(t, req.CheckAndSetDefaults())
|
||||
req.Spec.Encryption = &types.SessionRecordingEncryptionConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// setup initial state
|
||||
src, err := manager.CreateSessionRecordingConfig(ctx, req)
|
||||
require.NoError(t, err)
|
||||
encryptionKeys := src.GetEncryptionKeys()
|
||||
require.Len(t, encryptionKeys, 1)
|
||||
|
||||
encryption, err := config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs := encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 1)
|
||||
initialKey := proto.CloneOf(activePairs[0])
|
||||
require.NotNil(t, initialKey.KeyPair)
|
||||
require.NotEmpty(t, initialKey.KeyPair.PrivateKey)
|
||||
require.NotEmpty(t, initialKey.KeyPair.PublicKey)
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, initialKey.State)
|
||||
|
||||
// rotate key
|
||||
err = manager.RotateKey(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
encryption, err = config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs = encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 2)
|
||||
|
||||
var foundInitialPair bool
|
||||
var newPair *recordingencryptionv1.KeyPair
|
||||
for _, pair := range activePairs {
|
||||
if slices.Equal(initialKey.KeyPair.PublicKey, pair.KeyPair.PublicKey) {
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING, pair.State)
|
||||
foundInitialPair = true
|
||||
} else {
|
||||
newPair = pair
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, pair.State)
|
||||
}
|
||||
}
|
||||
|
||||
require.True(t, foundInitialPair && newPair != nil)
|
||||
|
||||
// rollback rotation
|
||||
err = manager.RollbackRotation(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
encryption, err = config.Backend.GetRecordingEncryption(ctx)
|
||||
require.NoError(t, err)
|
||||
activePairs = encryption.GetSpec().GetActiveKeyPairs()
|
||||
require.Len(t, activePairs, 1)
|
||||
|
||||
require.Equal(t, initialKey.KeyPair.PublicKey, activePairs[0].KeyPair.PublicKey)
|
||||
require.Equal(t, initialKey.KeyPair.PrivateKey, activePairs[0].KeyPair.PrivateKey)
|
||||
require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, activePairs[0].State)
|
||||
|
||||
pubKey, err := x509.ParsePKIXPublicKey(initialKey.KeyPair.PublicKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
fingerprint, err := recordingencryption.Fingerprint(pubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// no rotated key should be found after a rollback
|
||||
_, err = config.Backend.GetRotatedKey(ctx, fingerprint)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRotateKeyDisabled(t *testing.T) {
|
||||
ctx, bk := newLocalBackend(t)
|
||||
|
||||
config := newManagerConfig(t, bk, types.PrivateKeyType_RAW)
|
||||
manager, err := recordingencryption.NewManager(ctx, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &types.SessionRecordingConfigV2{}
|
||||
require.NoError(t, req.CheckAndSetDefaults())
|
||||
req.Spec.Encryption = &types.SessionRecordingEncryptionConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
// setup initial state
|
||||
src, err := manager.CreateSessionRecordingConfig(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.RotateKey(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
_, err = manager.GetRotationState(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
err = manager.CompleteRotation(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
err = manager.RollbackRotation(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
_, err = config.KeyStore.NewEncryptionKeyPair(ctx, cryptosuites.RecordingKeyWrapping)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, ok := src.(*types.SessionRecordingConfigV2)
|
||||
require.True(t, ok, "expected SessionRecordingConfigV2")
|
||||
|
||||
cfg.Spec.Encryption.Enabled = true
|
||||
cfg.Spec.Encryption.ManualKeyManagement = &types.ManualKeyManagementConfig{
|
||||
Enabled: true,
|
||||
ActiveKeys: []*types.KeyLabel{{
|
||||
Type: "pkcs11",
|
||||
Label: "test",
|
||||
}},
|
||||
}
|
||||
|
||||
_, err = manager.UpdateSessionRecordingConfig(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.RotateKey(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
_, err = manager.GetRotationState(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
err = manager.CompleteRotation(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
err = manager.RollbackRotation(ctx)
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
}
|
||||
|
||||
@@ -32,11 +32,20 @@ import (
|
||||
"github.com/gravitational/teleport/lib/session"
|
||||
)
|
||||
|
||||
// A KeyRotater facilitates rotation of encryption keys.
|
||||
type KeyRotater interface {
|
||||
RotateKey(context.Context) error
|
||||
CompleteRotation(context.Context) error
|
||||
RollbackRotation(context.Context) error
|
||||
GetRotationState(context.Context) ([]*recordingencryptionv1.FingerprintWithState, error)
|
||||
}
|
||||
|
||||
// ServiceConfig captures everything a [Service] requires to fulfill requests.
|
||||
type ServiceConfig struct {
|
||||
Authorizer authz.Authorizer
|
||||
Logger *slog.Logger
|
||||
Uploader events.MultipartUploader
|
||||
KeyRotater KeyRotater
|
||||
}
|
||||
|
||||
// NewService returns a new [Service] based on the given [ServiceConfig].
|
||||
@@ -46,6 +55,8 @@ func NewService(cfg ServiceConfig) (*Service, error) {
|
||||
return nil, trace.BadParameter("authorizer is required")
|
||||
case cfg.Uploader == nil:
|
||||
return nil, trace.BadParameter("uploader is required")
|
||||
case cfg.KeyRotater == nil:
|
||||
return nil, trace.BadParameter("key rotater is required")
|
||||
}
|
||||
|
||||
if cfg.Logger == nil {
|
||||
@@ -56,6 +67,7 @@ func NewService(cfg ServiceConfig) (*Service, error) {
|
||||
logger: cfg.Logger,
|
||||
uploader: cfg.Uploader,
|
||||
auth: cfg.Authorizer,
|
||||
rotater: cfg.KeyRotater,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -66,6 +78,7 @@ type Service struct {
|
||||
auth authz.Authorizer
|
||||
logger *slog.Logger
|
||||
uploader events.MultipartUploader
|
||||
rotater KeyRotater
|
||||
}
|
||||
|
||||
func streamUploadAsProto(upload events.StreamUpload) *recordingencryptionv1.Upload {
|
||||
@@ -184,3 +197,71 @@ func (s *Service) CompleteUpload(ctx context.Context, req *recordingencryptionv1
|
||||
|
||||
return &recordingencryptionv1.CompleteUploadResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) authorizeKeyRotation(ctx context.Context) error {
|
||||
authCtx, err := s.auth.Authorize(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := authCtx.AuthorizeAdminAction(); err != nil {
|
||||
return trace.AccessDenied("Key rotation can only be performed by admins")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RotateKey starts the rotation process for the active key pair used while encrypting session recording data.
|
||||
func (s *Service) RotateKey(ctx context.Context, req *recordingencryptionv1.RotateKeyRequest) (*recordingencryptionv1.RotateKeyResponse, error) {
|
||||
if err := s.authorizeKeyRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.rotater.RotateKey(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &recordingencryptionv1.RotateKeyResponse{}, nil
|
||||
}
|
||||
|
||||
// CompleteRotation moves rotated keys out of the active set into new RotatedKey resources.
|
||||
func (s *Service) CompleteRotation(ctx context.Context, req *recordingencryptionv1.CompleteRotationRequest) (*recordingencryptionv1.CompleteRotationResponse, error) {
|
||||
if err := s.authorizeKeyRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.rotater.CompleteRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &recordingencryptionv1.CompleteRotationResponse{}, nil
|
||||
}
|
||||
|
||||
// RollbackRotation removes active keys and reverts rotating keys back to being active.
|
||||
func (s *Service) RollbackRotation(ctx context.Context, req *recordingencryptionv1.RollbackRotationRequest) (*recordingencryptionv1.RollbackRotationResponse, error) {
|
||||
if err := s.authorizeKeyRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.rotater.RollbackRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &recordingencryptionv1.RollbackRotationResponse{}, nil
|
||||
}
|
||||
|
||||
// GetRotationState the state and fingerprint of all currently active keys.
|
||||
func (s *Service) GetRotationState(ctx context.Context, req *recordingencryptionv1.GetRotationStateRequest) (*recordingencryptionv1.GetRotationStateResponse, error) {
|
||||
if err := s.authorizeKeyRotation(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
states, err := s.rotater.GetRotationState(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &recordingencryptionv1.GetRotationStateResponse{
|
||||
KeyPairStates: states,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// 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 recordingencryptionv1_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
recordingencryptionv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
|
||||
"github.com/gravitational/teleport/lib/auth/recordingencryption/recordingencryptionv1"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
||||
)
|
||||
|
||||
type authKey struct{}
|
||||
|
||||
func withAuthCtx(ctx context.Context, authCtx authz.Context) context.Context {
|
||||
return context.WithValue(ctx, authKey{}, authCtx)
|
||||
}
|
||||
|
||||
func TestRotateKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ctx authz.Context
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "authorized RotateKey",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthMFAVerified),
|
||||
}, {
|
||||
name: "unauthorized RotateKey",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthUnauthorized),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
ctx := withAuthCtx(t.Context(), c.ctx)
|
||||
rotater := newFakeKeyRotater()
|
||||
cfg := recordingencryptionv1.ServiceConfig{
|
||||
Authorizer: &fakeAuthorizer{},
|
||||
Logger: logtest.NewLogger(),
|
||||
Uploader: fakeUploader{},
|
||||
KeyRotater: rotater,
|
||||
}
|
||||
|
||||
service, err := recordingencryptionv1.NewService(cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 1)
|
||||
|
||||
_, err = service.RotateKey(ctx, nil)
|
||||
if c.expectErr {
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
require.Len(t, rotater.keys, 1)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 2)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteRotation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ctx authz.Context
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "authorized CompleteRotation",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthMFAVerified),
|
||||
}, {
|
||||
name: "unauthorized CompleteRotation",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthUnauthorized),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
authCtx := withAuthCtx(t.Context(), newAuthCtx(authz.AdminActionAuthMFAVerified))
|
||||
ctx := withAuthCtx(t.Context(), c.ctx)
|
||||
rotater := newFakeKeyRotater()
|
||||
cfg := recordingencryptionv1.ServiceConfig{
|
||||
Authorizer: &fakeAuthorizer{},
|
||||
Logger: logtest.NewLogger(),
|
||||
Uploader: fakeUploader{},
|
||||
KeyRotater: rotater,
|
||||
}
|
||||
|
||||
service, err := recordingencryptionv1.NewService(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.RotateKey(authCtx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 2)
|
||||
|
||||
_, err = service.CompleteRotation(ctx, nil)
|
||||
if c.expectErr {
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
require.Len(t, rotater.keys, 2)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollbackRotation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ctx authz.Context
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "authorized Rollback",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthMFAVerified),
|
||||
}, {
|
||||
name: "unauthorized Rollback",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthUnauthorized),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
authCtx := withAuthCtx(t.Context(), newAuthCtx(authz.AdminActionAuthMFAVerified))
|
||||
ctx := withAuthCtx(t.Context(), c.ctx)
|
||||
rotater := newFakeKeyRotater()
|
||||
cfg := recordingencryptionv1.ServiceConfig{
|
||||
Authorizer: &fakeAuthorizer{},
|
||||
Logger: logtest.NewLogger(),
|
||||
Uploader: fakeUploader{},
|
||||
KeyRotater: rotater,
|
||||
}
|
||||
|
||||
service, err := recordingencryptionv1.NewService(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.RotateKey(authCtx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 2)
|
||||
|
||||
_, err = service.RollbackRotation(ctx, nil)
|
||||
if c.expectErr {
|
||||
require.True(t, trace.IsAccessDenied(err))
|
||||
require.Len(t, rotater.keys, 2)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rotater.keys, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRotationState(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ctx authz.Context
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "authorized",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthMFAVerified),
|
||||
}, {
|
||||
name: "unauthorized",
|
||||
ctx: newAuthCtx(authz.AdminActionAuthUnauthorized),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
ctx := withAuthCtx(t.Context(), c.ctx)
|
||||
rotater := newFakeKeyRotater()
|
||||
cfg := recordingencryptionv1.ServiceConfig{
|
||||
Authorizer: &fakeAuthorizer{},
|
||||
Logger: logtest.NewLogger(),
|
||||
Uploader: fakeUploader{},
|
||||
KeyRotater: rotater,
|
||||
}
|
||||
|
||||
service, err := recordingencryptionv1.NewService(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := service.GetRotationState(ctx, nil)
|
||||
if c.expectErr {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, res)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.KeyPairStates, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthCtx(action authz.AdminActionAuthState) authz.Context {
|
||||
return authz.Context{
|
||||
AdminActionAuthState: action,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeUploader struct {
|
||||
events.MultipartUploader
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct{}
|
||||
|
||||
func (f *fakeAuthorizer) Authorize(ctx context.Context) (*authz.Context, error) {
|
||||
authCtx, ok := ctx.Value(authKey{}).(authz.Context)
|
||||
if !ok {
|
||||
return nil, errors.New("no auth")
|
||||
}
|
||||
|
||||
return &authCtx, nil
|
||||
}
|
||||
|
||||
type fakeKeyRotater struct {
|
||||
keys []*recordingencryptionv1pb.FingerprintWithState
|
||||
}
|
||||
|
||||
func newFakeKeyRotater() *fakeKeyRotater {
|
||||
return &fakeKeyRotater{
|
||||
keys: []*recordingencryptionv1pb.FingerprintWithState{
|
||||
{
|
||||
Fingerprint: uuid.New().String(),
|
||||
State: recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ACTIVE,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeKeyRotater) RotateKey(ctx context.Context) error {
|
||||
if len(f.keys) != 1 {
|
||||
return errors.New("rotation in progress")
|
||||
}
|
||||
|
||||
if f.keys[0].State != recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ACTIVE {
|
||||
return fmt.Errorf("keys in unexpected state: %v", f.keys[0].State)
|
||||
}
|
||||
|
||||
f.keys[0].State = recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ROTATING
|
||||
f.keys = append(f.keys, &recordingencryptionv1pb.FingerprintWithState{
|
||||
Fingerprint: uuid.New().String(),
|
||||
State: recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ACTIVE,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeKeyRotater) CompleteRotation(ctx context.Context) error {
|
||||
var keys []*recordingencryptionv1pb.FingerprintWithState
|
||||
for _, key := range f.keys {
|
||||
if key.State == recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ACTIVE {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
f.keys = keys
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeKeyRotater) RollbackRotation(ctx context.Context) error {
|
||||
var keys []*recordingencryptionv1pb.FingerprintWithState
|
||||
for _, key := range f.keys {
|
||||
if key.State == recordingencryptionv1pb.KeyPairState_KEY_PAIR_STATE_ROTATING {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
f.keys = keys
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeKeyRotater) GetRotationState(ctx context.Context) ([]*recordingencryptionv1pb.FingerprintWithState, error) {
|
||||
return f.keys, nil
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func precomputeTestKeys(ctx context.Context, bitSize int) {
|
||||
const testKeysNumber = 25
|
||||
|
||||
// testKeysNumber is the number of RSA 4096 keys generated in tests.
|
||||
const testKeysNumber4096 = 3
|
||||
const testKeysNumber4096 = 10
|
||||
|
||||
func generateTestKeys(ctx context.Context, bitSize int) <-chan *rsa.PrivateKey {
|
||||
count := testKeysNumber
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
PrecomputedKeys = make(chan *rsa.PrivateKey, 25)
|
||||
|
||||
// PrecomputedKeys4096 is a queue of cached 4096-bit keys ready for usage.
|
||||
PrecomputedKeys4096 = make(chan *rsa.PrivateKey, 3)
|
||||
PrecomputedKeys4096 = make(chan *rsa.PrivateKey, 10)
|
||||
|
||||
// StartPrecomputeOnce is used to start the background task that precomputes key pairs.
|
||||
StartPrecomputeOnce sync.Once
|
||||
|
||||
@@ -2200,9 +2200,10 @@ func (process *TeleportProcess) initAuthService() error {
|
||||
LockNameComponents: []string{"recording_encryption"},
|
||||
},
|
||||
},
|
||||
InitialSessionRecordingConfig: cfg.Auth.SessionRecordingConfig,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
return trace.Wrap(err, "initializing session recording encryption")
|
||||
}
|
||||
|
||||
clusterConfig = recordingEncryptionManager
|
||||
|
||||
Reference in New Issue
Block a user