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
7 changes: 7 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions lib/services/local/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 128 additions & 0 deletions lib/services/local/recording_encryption.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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:
Comment thread
eriktate marked this conversation as resolved.
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)
}
}
88 changes: 88 additions & 0 deletions lib/services/local/recording_encryption_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
}
36 changes: 36 additions & 0 deletions lib/services/recording_encryption.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
}
Loading