diff --git a/api/client/client.go b/api/client/client.go
index 378cc84e20e5d..beeb17e40e0e0 100644
--- a/api/client/client.go
+++ b/api/client/client.go
@@ -935,6 +935,12 @@ func (c *Client) RecordingMetadataServiceClient() recordingmetadatav1.RecordingM
return recordingmetadatav1.NewRecordingMetadataServiceClient(c.conn)
}
+// RecordingEncryptionServiceClient returns an unadorned client for the session
+// recording encryption service.
+func (c *Client) RecordingEncryptionServiceClient() recordingencryptionv1pb.RecordingEncryptionServiceClient {
+ return recordingencryptionv1pb.NewRecordingEncryptionServiceClient(c.conn)
+}
+
// GetVnetConfig returns the singleton VnetConfig resource.
func (c *Client) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) {
return c.VnetConfigServiceClient().GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{})
diff --git a/lib/auth/init.go b/lib/auth/init.go
index abda4a0a58bdb..72e5fa3519403 100644
--- a/lib/auth/init.go
+++ b/lib/auth/init.go
@@ -551,7 +551,14 @@ func initCluster(ctx context.Context, cfg InitConfig, asrv *Server) error {
})
g.Go(func() error {
- ctx, span := cfg.Tracer.Start(gctx, "auth/InitializeSessionRecordingConfig")
+ ctx, span := cfg.Tracer.Start(gctx, "auth/initializeAuthPreference")
+ if err := initializeAuthPreference(ctx, asrv, cfg.AuthPreference); err != nil {
+ span.End()
+ return trace.Wrap(err)
+ }
+ span.End()
+
+ ctx, span = cfg.Tracer.Start(gctx, "auth/InitializeSessionRecordingConfig")
defer span.End()
return trace.Wrap(initializeSessionRecordingConfig(ctx, asrv, cfg.SessionRecordingConfig))
})
@@ -568,12 +575,6 @@ func initCluster(ctx context.Context, cfg InitConfig, asrv *Server) error {
return trace.Wrap(initializeVnetConfig(ctx, asrv))
})
- g.Go(func() error {
- ctx, span := cfg.Tracer.Start(gctx, "auth/initializeAuthPreference")
- defer span.End()
- return trace.Wrap(initializeAuthPreference(ctx, asrv, cfg.AuthPreference))
- })
-
g.Go(func() error {
_, span := cfg.Tracer.Start(gctx, "auth/SetStaticTokens")
defer span.End()
diff --git a/lib/config/configuration.go b/lib/config/configuration.go
index 941a88a24f944..39460319108bc 100644
--- a/lib/config/configuration.go
+++ b/lib/config/configuration.go
@@ -973,7 +973,7 @@ func applyAuthConfig(fc *FileConfig, cfg *servicecfg.Config) error {
return trace.BadParameter("cannot set both proxy_checks_host_keys and session_recording_config at the same time, prefer session_recording_config.proxy_checks_host_keys")
}
- src = *fc.Auth.SessionRecordingConfig
+ src = fc.Auth.SessionRecordingConfig.toSpec()
}
cfg.Auth.SessionRecordingConfig, err = types.NewSessionRecordingConfigFromConfigFile(src)
diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go
index 0799b824fd228..6d1373019b336 100644
--- a/lib/config/fileconf.go
+++ b/lib/config/fileconf.go
@@ -747,7 +747,7 @@ type Auth struct {
// SessionRecordingConfig configures how session recording should be handled including things like
// encryption and key management.
- SessionRecordingConfig *types.SessionRecordingConfigSpecV2 `yaml:"session_recording_config,omitempty"`
+ SessionRecordingConfig *SessionRecordingConfig `yaml:"session_recording_config,omitempty"`
// LicenseFile is a path to the license file. The path can be either absolute or
// relative to the global data dir
@@ -2929,3 +2929,50 @@ type Relay struct {
// Relay service credentials should be authoritative for.
APIPublicHostnames []string `yaml:"api_public_hostnames"`
}
+
+// SessionRecordingEncryptionConfig is the session_recording_config.encryption
+// section of the Teleport config file. It maps directly to [types.SessionRecordingEncryptionConfig]
+type SessionRecordingEncryptionConfig struct {
+ Enabled bool `yaml:"enabled,omitempty"`
+ ManualKeyManagement *struct {
+ Enabled bool `yaml:"enabled,omitempty"`
+ ActiveKeys []*types.KeyLabel `yaml:"active_keys,omitempty"`
+ RotatedKeys []*types.KeyLabel `yaml:"rotated_keys,omitempty"`
+ } `yaml:"manual_key_management,omitempty"`
+}
+
+// SessionRecordingConfig is the session_recording_config section of the Teleport config file.
+// It maps directly to [types.SessionRecordingConfigSpecV2]
+type SessionRecordingConfig struct {
+ Mode string `yaml:"mode"`
+ ProxyChecksHostKeys *types.BoolOption `yaml:"proxy_checks_host_keys,omitempty"`
+ Encryption *SessionRecordingEncryptionConfig `yaml:"encryption,omitempty"`
+}
+
+// toSpec converts SessionRecordingConfig into a types.SessionRecordingConfigSpecV2 so it can
+// be used to initialize session recording.
+func (src *SessionRecordingConfig) toSpec() types.SessionRecordingConfigSpecV2 {
+ if src == nil {
+ return types.SessionRecordingConfigSpecV2{}
+ }
+
+ var encryption *types.SessionRecordingEncryptionConfig
+ if src.Encryption != nil {
+ encryption = &types.SessionRecordingEncryptionConfig{
+ Enabled: src.Encryption.Enabled,
+ }
+ if src.Encryption.ManualKeyManagement != nil {
+ encryption.ManualKeyManagement = &types.ManualKeyManagementConfig{
+ Enabled: src.Encryption.ManualKeyManagement.Enabled,
+ ActiveKeys: src.Encryption.ManualKeyManagement.ActiveKeys,
+ RotatedKeys: src.Encryption.ManualKeyManagement.RotatedKeys,
+ }
+ }
+ }
+
+ return types.SessionRecordingConfigSpecV2{
+ Mode: src.Mode,
+ ProxyChecksHostKeys: src.ProxyChecksHostKeys,
+ Encryption: encryption,
+ }
+}
diff --git a/tool/tctl/common/recordings_command.go b/tool/tctl/common/recordings_command.go
index f2a2fdae8dfed..f354451bf6262 100644
--- a/tool/tctl/common/recordings_command.go
+++ b/tool/tctl/common/recordings_command.go
@@ -21,6 +21,7 @@ package common
import (
"context"
"fmt"
+ "io"
"os"
"github.com/alecthomas/kingpin/v2"
@@ -47,6 +48,8 @@ type RecordingsCommand struct {
format string
// recordingsList implements the "tctl recordings ls" subcommand.
recordingsList *kingpin.CmdClause
+ // recordingsEncryption implements the "tctl recordings encryption" subcommand.
+ recordingsEncryption recordingsEncryptionCommand
// fromUTC is the start time to use for the range of recordings listed by the recorded session listing command
fromUTC string
// toUTC is the start time to use for the range of recordings listed by the recorded session listing command
@@ -55,10 +58,17 @@ type RecordingsCommand struct {
maxRecordingsToShow int
// recordingsSince is a duration which sets the time into the past in which to list session recordings
recordingsSince string
+
+ // stdout allows to switch standard output source for resource command. Used in tests.
+ stdout io.Writer
}
// Initialize allows RecordingsCommand to plug itself into the CLI parser
func (c *RecordingsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) {
+ if c.stdout == nil {
+ c.stdout = os.Stdout
+ }
+
c.config = config
recordings := app.Command("recordings", "View and control session recordings.")
c.recordingsList = recordings.Command("ls", "List recorded sessions.")
@@ -67,6 +77,11 @@ func (c *RecordingsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Glob
c.recordingsList.Flag("to-utc", fmt.Sprintf("End of time range in which recordings are listed. Format %s. Defaults to current time.", defaults.TshTctlSessionListTimeFormat)).StringVar(&c.toUTC)
c.recordingsList.Flag("limit", fmt.Sprintf("Maximum number of recordings to show. Default %s.", defaults.TshTctlSessionListLimit)).Default(defaults.TshTctlSessionListLimit).IntVar(&c.maxRecordingsToShow)
c.recordingsList.Flag("last", "Duration into the past from which session recordings should be listed. Format 5h30m40s").StringVar(&c.recordingsSince)
+ c.recordingsEncryption.Initialize(recordings, c.stdout)
+
+ if c.recordingsEncryption.stdout == nil {
+ c.recordingsEncryption.stdout = c.stdout
+ }
}
// TryRun attempts to run subcommands like "recordings ls".
@@ -76,7 +91,7 @@ func (c *RecordingsCommand) TryRun(ctx context.Context, cmd string, clientFunc c
case c.recordingsList.FullCommand():
commandFunc = c.ListRecordings
default:
- return false, nil
+ return c.recordingsEncryption.TryRun(ctx, cmd, clientFunc)
}
client, closeFn, err := clientFunc(ctx)
if err != nil {
@@ -103,5 +118,5 @@ func (c *RecordingsCommand) ListRecordings(ctx context.Context, tc *authclient.C
if err != nil {
return trace.Errorf("getting session events: %v", err)
}
- return trace.Wrap(common.ShowSessions(recordings, c.format, os.Stdout))
+ return trace.Wrap(common.ShowSessions(recordings, c.format, c.stdout))
}
diff --git a/tool/tctl/common/recordings_encryption_command.go b/tool/tctl/common/recordings_encryption_command.go
new file mode 100644
index 0000000000000..6f9290d745ad9
--- /dev/null
+++ b/tool/tctl/common/recordings_encryption_command.go
@@ -0,0 +1,207 @@
+// 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 common
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport"
+ recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
+ "github.com/gravitational/teleport/lib/asciitable"
+ "github.com/gravitational/teleport/lib/auth/authclient"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/utils"
+ commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
+)
+
+type recordingsEncryptionCommand struct {
+ // cmd implements the "tctl recordings encryptino" parent command
+ cmd *kingpin.CmdClause
+
+ // rotateCmd implements the "tctl recordings encryption rotate" subcommand.
+ rotateCmd *kingpin.CmdClause
+
+ // statusCmd implements the "tctl recordings encryption status" subcommand.
+ statusCmd *kingpin.CmdClause
+
+ // completeCmd implements the "tctl recordings encryption complete" subcommand.
+ completeCmd *kingpin.CmdClause
+
+ // rollbackCmd implements the "tctl recordings encryption rollback" subcommand.
+ rollbackCmd *kingpin.CmdClause
+
+ // format is the output format of statusCmd (text, json, or yaml)
+ format string
+
+ // stdout allows for redirecting command output. Useful for tests.
+ stdout io.Writer
+}
+
+// Initialize allows recordingsEncryptionCommand to plug itself into the CLI parser.
+func (c *recordingsEncryptionCommand) Initialize(recordingsCmd *kingpin.CmdClause, stdout io.Writer) {
+ c.cmd = recordingsCmd.Command("encryption", "Manage encryption properties of session recordings.")
+
+ c.rotateCmd = c.cmd.Command("rotate", "Rotate encryption keys used for encrypting session recordings.")
+ c.statusCmd = c.cmd.Command("status", "Show current rotation status.")
+ c.statusCmd.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)+". Defaults to 'text'.").Default(teleport.Text).StringVar(&c.format)
+ c.completeCmd = c.cmd.Command("complete-rotation", "Completes an in-progress encryption key rotation.")
+ c.rollbackCmd = c.cmd.Command("rollback-rotation", "Rolls back an in-progress encryption key rotation.")
+ if stdout == nil {
+ c.stdout = os.Stdout
+ }
+}
+
+// TryRun attempts to run subcommands like "recordings encryption rotate".
+func (c *recordingsEncryptionCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
+ var commandFunc func(ctx context.Context, client *authclient.Client) error
+ switch cmd {
+ case c.rotateCmd.FullCommand():
+ commandFunc = c.Rotate
+ case c.statusCmd.FullCommand():
+ commandFunc = c.Status
+ case c.completeCmd.FullCommand():
+ commandFunc = c.Complete
+ case c.rollbackCmd.FullCommand():
+ commandFunc = c.Rollback
+ default:
+ return false, nil
+ }
+ client, closeFn, err := clientFunc(ctx)
+ if err != nil {
+ return false, trace.Wrap(err)
+ }
+ err = commandFunc(ctx, client)
+ closeFn(ctx)
+
+ return true, trace.Wrap(err)
+}
+
+// Rotate initiates a key rotation. It should fail if a key rotation is already
+// in progress.
+func (c *recordingsEncryptionCommand) Rotate(ctx context.Context, tc *authclient.Client) error {
+ client := tc.RecordingEncryptionServiceClient()
+ if _, err := client.RotateKey(ctx, &recordingencryptionv1.RotateKeyRequest{}); err != nil {
+ return trace.Errorf("rotating key encryption keys: %v", err)
+ }
+ fmt.Fprintln(c.stdout, "Rotation started")
+ return nil
+}
+
+// Complete an in progress key rotation. It should fail if any key is marked
+// 'inaccessible'.
+func (c *recordingsEncryptionCommand) Complete(ctx context.Context, tc *authclient.Client) error {
+ client := tc.RecordingEncryptionServiceClient()
+ if _, err := client.CompleteRotation(ctx, &recordingencryptionv1.CompleteRotationRequest{}); err != nil {
+ return trace.Errorf("completing encryption key rotation: %v", err)
+ }
+
+ fmt.Fprintln(c.stdout, "Rotation completed")
+ return nil
+}
+
+// Rollback an in progress key rotation.
+func (c *recordingsEncryptionCommand) Rollback(ctx context.Context, tc *authclient.Client) error {
+ client := tc.RecordingEncryptionServiceClient()
+ if _, err := client.RollbackRotation(ctx, &recordingencryptionv1.RollbackRotationRequest{}); err != nil {
+ return trace.Errorf("rolling back encryption key rotation: %v", err)
+ }
+
+ fmt.Fprintln(c.stdout, "Rotation rollback successful")
+ return nil
+}
+
+// Status displays the current rotation status of the active encryption keys.
+func (c *recordingsEncryptionCommand) Status(ctx context.Context, tc *authclient.Client) error {
+ client := tc.RecordingEncryptionServiceClient()
+ res, err := client.GetRotationState(ctx, &recordingencryptionv1.GetRotationStateRequest{})
+ if err != nil {
+ return trace.Errorf("fetching encryption key status: %v", err)
+ }
+
+ switch c.format {
+ case teleport.Text, "":
+ return trace.Wrap(c.writeStatusText(c.stdout, res.GetKeyPairStates()))
+ case teleport.YAML:
+ return trace.Wrap(c.writeStatusYAML(c.stdout, res.GetKeyPairStates()))
+ case teleport.JSON:
+ return trace.Wrap(c.writeStatusJSON(c.stdout, res.GetKeyPairStates()))
+ }
+
+ return trace.Wrap(err, "writing encryption key status")
+}
+
+func (c *recordingsEncryptionCommand) writeStatusJSON(w io.Writer, keyStates []*recordingencryptionv1.FingerprintWithState) error {
+ data, err := json.MarshalIndent(keyStates, "", " ")
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ _, err = w.Write(data)
+ return trace.Wrap(err)
+}
+
+func (c *recordingsEncryptionCommand) writeStatusYAML(w io.Writer, keyStates []*recordingencryptionv1.FingerprintWithState) error {
+ return trace.Wrap(utils.WriteYAML(w, keyStates))
+}
+
+func (c *recordingsEncryptionCommand) writeStatusText(w io.Writer, keyStates []*recordingencryptionv1.FingerprintWithState) error {
+ rotationState := recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_UNSPECIFIED
+ t := asciitable.MakeTable([]string{"Key Pair Fingerprint", "State"})
+ for _, pair := range keyStates {
+ if pair.State == recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE {
+ rotationState = pair.State
+ }
+
+ if pair.State == recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING {
+ if rotationState != recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE {
+ rotationState = pair.State
+ }
+ }
+
+ t.AddRow([]string{pair.Fingerprint, c.getFriendlyStatusString(pair.State)})
+ }
+
+ switch rotationState {
+ case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE:
+ fmt.Fprintln(w, "Rotation failed due to inaccessible key")
+ case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING:
+ fmt.Fprintln(w, "Rotation in progress")
+ }
+
+ _, err := t.AsBuffer().WriteTo(w)
+ return trace.Wrap(err)
+}
+
+func (c *recordingsEncryptionCommand) getFriendlyStatusString(state recordingencryptionv1.KeyPairState) string {
+ switch state {
+ case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING:
+ return "rotating"
+ case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE:
+ return "active"
+ case recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_INACCESSIBLE:
+ return "inaccessible"
+ default:
+ return "unknown"
+ }
+}
diff --git a/tool/tctl/common/recordings_encryption_command_test.go b/tool/tctl/common/recordings_encryption_command_test.go
new file mode 100644
index 0000000000000..2e7faa64a5abf
--- /dev/null
+++ b/tool/tctl/common/recordings_encryption_command_test.go
@@ -0,0 +1,139 @@
+// 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 common
+
+import (
+ "bytes"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
+ "github.com/gravitational/teleport/integration/helpers"
+ "github.com/gravitational/teleport/lib/auth/authclient"
+ "github.com/gravitational/teleport/lib/config"
+ "github.com/gravitational/teleport/tool/teleport/testenv"
+)
+
+func TestRecordingEncryptionKeyRotation(t *testing.T) {
+ dynAddr := helpers.NewDynamicServiceAddr(t)
+ fileConfig := &config.FileConfig{
+ Global: config.Global{
+ DataDir: t.TempDir(),
+ },
+ Auth: config.Auth{
+ Service: config.Service{
+ EnabledFlag: "true",
+ ListenAddress: dynAddr.AuthAddr,
+ },
+ SessionRecordingConfig: &config.SessionRecordingConfig{
+ Mode: "node",
+ Encryption: &config.SessionRecordingEncryptionConfig{
+ Enabled: true,
+ },
+ },
+ },
+ }
+
+ process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
+ clt, err := testenv.NewDefaultAuthClient(process)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, clt.Close())
+ require.NoError(t, process.Close())
+ require.NoError(t, process.Wait())
+ })
+
+ // get initial status to confirm one active key exists
+ keyStates := getEncryptionKeyStates(t, clt)
+ require.Len(t, keyStates, 1)
+ initialKeyState := keyStates[0]
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, initialKeyState.State)
+
+ // start key rotation
+ _, err = runRecordingsCommand(t, clt, []string{"recordings", "encryption", "rotate"})
+ require.NoError(t, err)
+
+ // refetch status to confirm original key is now 'rotating' and new key is 'active'
+ keyStates = getEncryptionKeyStates(t, clt)
+ require.Len(t, keyStates, 2)
+ rotatedKeyState := keyStates[0]
+ newKeyState := keyStates[1]
+
+ require.Equal(t, initialKeyState.Fingerprint, rotatedKeyState.Fingerprint)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING, rotatedKeyState.State)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, newKeyState.State)
+
+ // confirm a second rotation fails when one is already in progress
+ _, err = runRecordingsCommand(t, clt, []string{"recordings", "encryption", "rotate"})
+ require.Error(t, err)
+
+ // rollback rotation
+ _, err = runRecordingsCommand(t, clt, []string{"recordings", "encryption", "rollback-rotation"})
+ require.NoError(t, err)
+
+ // ensure initial key is the only active key remaining
+ keyStates = getEncryptionKeyStates(t, clt)
+ require.Len(t, keyStates, 1)
+ newKeyState = keyStates[0]
+ require.Equal(t, initialKeyState.Fingerprint, newKeyState.Fingerprint)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, newKeyState.State)
+
+ // start a new rotation
+ _, err = runRecordingsCommand(t, clt, []string{"recordings", "encryption", "rotate"})
+ require.NoError(t, err)
+
+ // confirm in progress rotation state
+ keyStates = getEncryptionKeyStates(t, clt)
+ require.Len(t, keyStates, 2)
+ rotatedKeyState = keyStates[0]
+ newKeyState = keyStates[1]
+ require.Equal(t, initialKeyState.Fingerprint, rotatedKeyState.Fingerprint)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ROTATING, rotatedKeyState.State)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, newKeyState.State)
+
+ // complete rotation
+ _, err = runRecordingsCommand(t, clt, []string{"recordings", "encryption", "complete-rotation"})
+ require.NoError(t, err)
+
+ // ensure remaining active key is new
+ keyStates = getEncryptionKeyStates(t, clt)
+ require.Len(t, keyStates, 1)
+ finalKeyState := keyStates[0]
+ require.Equal(t, newKeyState.Fingerprint, finalKeyState.Fingerprint)
+ require.Equal(t, recordingencryptionv1.KeyPairState_KEY_PAIR_STATE_ACTIVE, finalKeyState.State)
+}
+
+func getEncryptionKeyStates(t *testing.T, client *authclient.Client) []*recordingencryptionv1.FingerprintWithState {
+ var keyStates []*recordingencryptionv1.FingerprintWithState
+ out, err := runRecordingsCommand(t, client, []string{"recordings", "encryption", "status", "--format", "json"})
+ require.NoError(t, err)
+ err = json.Unmarshal(out.Bytes(), &keyStates)
+ require.NoError(t, err)
+
+ return keyStates
+}
+
+func runRecordingsCommand(t *testing.T, client *authclient.Client, args []string) (*bytes.Buffer, error) {
+ var stdoutBuf bytes.Buffer
+ command := &RecordingsCommand{
+ stdout: &stdoutBuf,
+ }
+
+ return &stdoutBuf, runCommand(t, client, command, args)
+}
diff --git a/tool/tctl/common/tctl_test.go b/tool/tctl/common/tctl_test.go
index d6da5c5f98b64..00c450f9338b9 100644
--- a/tool/tctl/common/tctl_test.go
+++ b/tool/tctl/common/tctl_test.go
@@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/config"
+ "github.com/gravitational/teleport/lib/cryptosuites/cryptosuitestest"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
@@ -48,7 +49,11 @@ func TestMain(m *testing.M) {
}
modules.SetInsecureTestMode(true)
- os.Exit(m.Run())
+ ctx, cancel := context.WithCancel(context.Background())
+ cryptosuitestest.PrecomputeRSAKeys(ctx)
+ exitCode := m.Run()
+ cancel()
+ os.Exit(exitCode)
}
func BenchmarkInit(b *testing.B) {