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
6 changes: 6 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
15 changes: 8 additions & 7 deletions lib/auth/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 48 additions & 1 deletion lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
Comment on lines +2933 to +2950
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like it could cause problems if we forget to update this in response to any changes made to the types.SessionRecordingEncryptionConfig resource. Why is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fields with underscores were broken before I added this, so manual_key_management and proxy_checks_host_keys. If there's a way to get the yaml parser to use the json tags during unmarshaling, that would definitely be better

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@atburke any thoughts here regarding the yaml parser and json tags with _?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The yaml parser isn't seeing the json tags at all and is assuming camel case instead of snake case. I don't think any of our current yaml packages can handle mixed yaml and json tags within the same object; we'll have to either do what you're doing here or get goccy just for ReadConfig.


// 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,
}
}
19 changes: 17 additions & 2 deletions tool/tctl/common/recordings_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package common
import (
"context"
"fmt"
"io"
"os"

"github.com/alecthomas/kingpin/v2"
Expand All @@ -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
Expand All @@ -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.")
Expand All @@ -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".
Expand All @@ -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 {
Expand All @@ -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))
}
207 changes: 207 additions & 0 deletions tool/tctl/common/recordings_encryption_command.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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