diff --git a/api/types/constants.go b/api/types/constants.go
index 507b7af600897..1915173c7c59a 100644
--- a/api/types/constants.go
+++ b/api/types/constants.go
@@ -311,10 +311,17 @@ const (
// KindSessionRecordingConfig is the resource for session recording configuration.
KindSessionRecordingConfig = "session_recording_config"
+ // KindRecordingEncryption is the collection of active session recording encryption keys.
+ KindRecordingEncryption = "recording_encryption"
+
// MetaNameSessionRecordingConfig is the exact name of the singleton resource for
// session recording configuration.
MetaNameSessionRecordingConfig = "session-recording-config"
+ // MetaNameRecordingEncryption is the exact name of the singleton resource for
+ // session recording configuration.
+ MetaNameRecordingEncryption = "recording-encryption"
+
// KindExternalAuditStorage the resource kind for External Audit Storage
// configuration.
KindExternalAuditStorage = "external_audit_storage"
diff --git a/api/types/resource.go b/api/types/resource.go
index 544f7b1f92b8c..571572ef7235a 100644
--- a/api/types/resource.go
+++ b/api/types/resource.go
@@ -746,7 +746,7 @@ func GetRevision(v any) (string, error) {
case Resource:
return r.GetRevision(), nil
case ResourceMetadata:
- return r.GetMetadata().Revision, nil
+ return r.GetMetadata().GetRevision(), nil
}
return "", trace.BadParameter("unable to determine revision from resource of type %T", v)
}
diff --git a/lib/services/local/events.go b/lib/services/local/events.go
index c667f3e50a3d5..8a5555f521807 100644
--- a/lib/services/local/events.go
+++ b/lib/services/local/events.go
@@ -258,6 +258,8 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch types.Watch) (type
parser = newWorkloadIdentityX509RevocationParser()
case types.KindHealthCheckConfig:
parser = newHealthCheckConfigParser()
+ case types.KindRecordingEncryption:
+ parser = newRecordingEncryptionParser()
default:
if watch.AllowPartialSuccess {
continue
diff --git a/lib/services/local/recording_encryption.go b/lib/services/local/recording_encryption.go
new file mode 100644
index 0000000000000..a98ffbca37a63
--- /dev/null
+++ b/lib/services/local/recording_encryption.go
@@ -0,0 +1,128 @@
+// 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 .
+
+package local
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+ recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/backend"
+ "github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/lib/services/local/generic"
+)
+
+const (
+ recordingEncryptionPrefix = "recording_encryption"
+)
+
+// RecordingEncryptionService exposes backend functionality for working with the
+// cluster's RecordingEncryption resource.
+type RecordingEncryptionService struct {
+ encryption *generic.ServiceWrapper[*recordingencryptionv1.RecordingEncryption]
+}
+
+var _ services.RecordingEncryption = (*RecordingEncryptionService)(nil)
+
+// NewRecordingEncryptionService creates a new RecordingEncryptionService.
+func NewRecordingEncryptionService(b backend.Backend) (*RecordingEncryptionService, error) {
+ const pageLimit = 100
+ encryption, err := generic.NewServiceWrapper(generic.ServiceConfig[*recordingencryptionv1.RecordingEncryption]{
+ Backend: b,
+ PageLimit: pageLimit,
+ ResourceKind: types.KindRecordingEncryption,
+ BackendPrefix: backend.NewKey(recordingEncryptionPrefix),
+ MarshalFunc: services.MarshalProtoResource[*recordingencryptionv1.RecordingEncryption],
+ UnmarshalFunc: services.UnmarshalProtoResource[*recordingencryptionv1.RecordingEncryption],
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return &RecordingEncryptionService{
+ encryption: encryption,
+ }, nil
+}
+
+// CreateRecordingEncryption creates a new RecordingEncryption in the backend.
+func (s *RecordingEncryptionService) CreateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
+ if encryption.Metadata == nil {
+ encryption.Metadata = &headerv1.Metadata{}
+ }
+ encryption.Metadata.Name = types.MetaNameRecordingEncryption
+ created, err := s.encryption.CreateResource(ctx, encryption)
+ return created, trace.Wrap(err)
+}
+
+// UpdateRecordingEncryption replaces the RecordingEncryption resource with the given one.
+func (s *RecordingEncryptionService) UpdateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
+ if encryption.Metadata == nil {
+ encryption.Metadata = &headerv1.Metadata{}
+ }
+ encryption.Metadata.Name = types.MetaNameRecordingEncryption
+ updated, err := s.encryption.ConditionalUpdateResource(ctx, encryption)
+ return updated, trace.Wrap(err)
+}
+
+// DeleteRecordingEncryption removes the RecordingEncryption from the cluster.
+func (s *RecordingEncryptionService) DeleteRecordingEncryption(ctx context.Context) error {
+ return trace.Wrap(s.encryption.DeleteResource(ctx, types.MetaNameRecordingEncryption))
+}
+
+// GetRecordingEncryption retrieves the RecordingEncryption for the cluster.
+func (s *RecordingEncryptionService) GetRecordingEncryption(ctx context.Context) (*recordingencryptionv1.RecordingEncryption, error) {
+ encryption, err := s.encryption.GetResource(ctx, types.MetaNameRecordingEncryption)
+ return encryption, trace.Wrap(err)
+}
+
+type recordingEncryptionParser struct {
+ baseParser
+}
+
+func newRecordingEncryptionParser() *recordingEncryptionParser {
+ return &recordingEncryptionParser{
+ baseParser: newBaseParser(backend.NewKey(recordingEncryptionPrefix)),
+ }
+}
+
+func (p *recordingEncryptionParser) parse(event backend.Event) (types.Resource, error) {
+ switch event.Type {
+ case types.OpPut:
+ resource, err := services.UnmarshalProtoResource[*recordingencryptionv1.RecordingEncryption](
+ event.Item.Value,
+ services.WithExpires(event.Item.Expires),
+ services.WithRevision(event.Item.Revision),
+ )
+ if err != nil {
+ return nil, trace.Wrap(err, "unmarshalling resource from event")
+ }
+ return types.Resource153ToLegacy(resource), nil
+ case types.OpDelete:
+ return &types.ResourceHeader{
+ Kind: types.KindRecordingEncryption,
+ Version: types.V1,
+ Metadata: types.Metadata{
+ Name: types.MetaNameRecordingEncryption,
+ },
+ }, nil
+ default:
+ return nil, trace.BadParameter("event %v is not supported", event.Type)
+ }
+}
diff --git a/lib/services/local/recording_encryption_test.go b/lib/services/local/recording_encryption_test.go
new file mode 100644
index 0000000000000..0f5420f02d1e1
--- /dev/null
+++ b/lib/services/local/recording_encryption_test.go
@@ -0,0 +1,88 @@
+// 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 .
+
+package local
+
+import (
+ "context"
+ "crypto"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/backend"
+ "github.com/gravitational/teleport/lib/backend/memory"
+)
+
+func TestRecordingEncryption(t *testing.T) {
+ bk, err := memory.New(memory.Config{})
+ require.NoError(t, err)
+ service, err := NewRecordingEncryptionService(backend.NewSanitizer(bk))
+ require.NoError(t, err)
+
+ ctx := context.Background()
+
+ initialEncryption := pb.RecordingEncryption{
+ Spec: &pb.RecordingEncryptionSpec{
+ ActiveKeys: nil,
+ },
+ }
+
+ // get should fail when there's no recording encryption
+ _, err = service.GetRecordingEncryption(ctx)
+ require.Error(t, err)
+
+ created, err := service.CreateRecordingEncryption(ctx, &initialEncryption)
+ require.NoError(t, err)
+
+ encryption, err := service.GetRecordingEncryption(ctx)
+ require.NoError(t, err)
+
+ require.Empty(t, created.Spec.ActiveKeys)
+ require.Empty(t, encryption.Spec.ActiveKeys)
+
+ encryption.Spec.ActiveKeys = []*pb.WrappedKey{
+ {
+ RecordingEncryptionPair: &types.EncryptionKeyPair{
+ PrivateKey: []byte("recording encryption private"),
+ PublicKey: []byte("recording encryption public"),
+ Hash: 0,
+ },
+ KeyEncryptionPair: &types.EncryptionKeyPair{
+ PrivateKey: []byte("key encryption private"),
+ PublicKey: []byte("key encryption public"),
+ Hash: uint32(crypto.SHA256),
+ },
+ },
+ }
+
+ updated, err := service.UpdateRecordingEncryption(ctx, encryption)
+ require.NoError(t, err)
+ require.Len(t, updated.Spec.ActiveKeys, 1)
+ require.EqualExportedValues(t, encryption.Spec.ActiveKeys[0], updated.Spec.ActiveKeys[0])
+
+ encryption, err = service.GetRecordingEncryption(ctx)
+ require.NoError(t, err)
+ require.Len(t, encryption.Spec.ActiveKeys, 1)
+ require.EqualExportedValues(t, updated.Spec.ActiveKeys[0], encryption.Spec.ActiveKeys[0])
+
+ err = service.DeleteRecordingEncryption(ctx)
+ require.NoError(t, err)
+ _, err = service.GetRecordingEncryption(ctx)
+ require.Error(t, err)
+}
diff --git a/lib/services/recording_encryption.go b/lib/services/recording_encryption.go
new file mode 100644
index 0000000000000..49216df69749a
--- /dev/null
+++ b/lib/services/recording_encryption.go
@@ -0,0 +1,36 @@
+// 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 .
+
+package services
+
+import (
+ "context"
+
+ recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
+)
+
+// RecordingEncryption handles CRUD operations for the RecordingEncryption resource.
+type RecordingEncryption interface {
+ // CreateRecordingEncryption creates a new RecordingEncryption in the backend if one
+ // does not already exist.
+ CreateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error)
+ // UpdateRecordingEncryption replaces the RecordingEncryption resource with the given one.
+ UpdateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error)
+ // DeleteRecordingEncryption removes the RecordingEncryption from the cluster.
+ DeleteRecordingEncryption(ctx context.Context) error
+ // GetRecordingEncryption retrieves the RecordingEncryption for the cluster.
+ GetRecordingEncryption(ctx context.Context) (*recordingencryptionv1.RecordingEncryption, error)
+}