Add disconnect expired cert and lock mode for scoped role (#66376)

* Add disconnect expired cert and lock mode for scoped role

Update IAC docs

* respond to code review

Add a scoped authorizer in the forwarder_test

fix up proto comments

Update forwarder.go

* fix forwarder setting nil disconnectExpiredCert

* Update validate lock to accept empty string - will be treated as best_effort down the line

add comment
This commit is contained in:
williamong-tel
2026-05-14 21:51:48 +00:00
committed by GitHub
parent 30b403795e
commit 6500619d93
20 changed files with 1983 additions and 118 deletions
@@ -234,8 +234,14 @@ type ScopedRoleDefaults struct {
// SessionRecording configures the session recording strategy for all protocols that don't
// explicitly set their session recording mode.
SessionRecording *SessionRecording `protobuf:"bytes,2,opt,name=session_recording,json=sessionRecording,proto3" json:"session_recording,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session.
// If unset, cluster wide defaults are used.
DisconnectExpiredCert *bool `protobuf:"varint,3,opt,name=disconnect_expired_cert,json=disconnectExpiredCert,proto3,oneof" json:"disconnect_expired_cert,omitempty"`
// Lock specifies the default locking mode for access sessions across all protocols that
// do not specify their own value. If unset, cluster wide defaults are used.
Lock *Lock `protobuf:"bytes,4,opt,name=lock,proto3" json:"lock,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ScopedRoleDefaults) Reset() {
@@ -282,6 +288,20 @@ func (x *ScopedRoleDefaults) GetSessionRecording() *SessionRecording {
return nil
}
func (x *ScopedRoleDefaults) GetDisconnectExpiredCert() bool {
if x != nil && x.DisconnectExpiredCert != nil {
return *x.DisconnectExpiredCert
}
return false
}
func (x *ScopedRoleDefaults) GetLock() *Lock {
if x != nil {
return x.Lock
}
return nil
}
// ScopedRoleSSH groups all scoped role fields relevant to SSH access. Fields within the SSH block
// encompass selection criteria and preconditions for access, as well as the controls to be applied in
// cases where access is permitted. An SSH block is the primary source of truth for controls to be applied
@@ -318,8 +338,14 @@ type ScopedRoleSSH struct {
EnhancedRecording *EnhancedRecording `protobuf:"bytes,12,opt,name=enhanced_recording,json=enhancedRecording,proto3" json:"enhanced_recording,omitempty"`
// SessionRecording configures the session recording strategy for SSH sessions.
SessionRecording *SessionRecording `protobuf:"bytes,13,opt,name=session_recording,json=sessionRecording,proto3" json:"session_recording,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// DisconnectExpiredCert controls whether SSH sessions are disconnected when the
// user certificate expires.
// Defaults to value cluster wide auth preference if not set.
DisconnectExpiredCert *bool `protobuf:"varint,14,opt,name=disconnect_expired_cert,json=disconnectExpiredCert,proto3,oneof" json:"disconnect_expired_cert,omitempty"`
// Lock configures the role's locking behavior for SSH sessions.
Lock *Lock `protobuf:"bytes,15,opt,name=lock,proto3" json:"lock,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ScopedRoleSSH) Reset() {
@@ -436,6 +462,20 @@ func (x *ScopedRoleSSH) GetSessionRecording() *SessionRecording {
return nil
}
func (x *ScopedRoleSSH) GetDisconnectExpiredCert() bool {
if x != nil && x.DisconnectExpiredCert != nil {
return *x.DisconnectExpiredCert
}
return false
}
func (x *ScopedRoleSSH) GetLock() *Lock {
if x != nil {
return x.Lock
}
return nil
}
// The group of all scoped role fields relevant to kube access. Fields within the kube block
// encompass selection criteria and preconditions for access, as well as the controls to be applied in
// cases where access is permitted. A kube block is the primary source of truth for controls to be applied
@@ -453,8 +493,12 @@ type ScopedRoleKube struct {
// Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value
// (or global default) applies.
ClientIdleTimeout string `protobuf:"bytes,5,opt,name=client_idle_timeout,json=clientIdleTimeout,proto3" json:"client_idle_timeout,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.
DisconnectExpiredCert *bool `protobuf:"varint,6,opt,name=disconnect_expired_cert,json=disconnectExpiredCert,proto3,oneof" json:"disconnect_expired_cert,omitempty"`
// Lock configures the role's locking behavior for kubernetes sessions.
Lock *Lock `protobuf:"bytes,7,opt,name=lock,proto3" json:"lock,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ScopedRoleKube) Reset() {
@@ -515,6 +559,20 @@ func (x *ScopedRoleKube) GetClientIdleTimeout() string {
return ""
}
func (x *ScopedRoleKube) GetDisconnectExpiredCert() bool {
if x != nil && x.DisconnectExpiredCert != nil {
return *x.DisconnectExpiredCert
}
return false
}
func (x *ScopedRoleKube) GetLock() *Lock {
if x != nil {
return x.Lock
}
return nil
}
// ScopedRule maps resources to verbs. This is the underlying type used to describe
// permissions like 'scoped_role:read' or 'scoped_role_assignment:create'.
type ScopedRule struct {
@@ -847,7 +905,7 @@ func (x *EnhancedRecording) GetDisk() bool {
// SessionRecording sets the session recording behavior.
type SessionRecording struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Mode sets the session recording mode. Allowed values: strict | best_effort.
// Mode sets the session recording mode. Allowed values: strict or best_effort.
Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -890,6 +948,54 @@ func (x *SessionRecording) GetMode() string {
return ""
}
// Lock controls locking mode for sessions authorized via this
// scoped role.
type Lock struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Allowed values: strict or best_effort.
// Defaults to value cluster wide auth preference if not set.
Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Lock) Reset() {
*x = Lock{}
mi := &file_teleport_scopes_access_v1_role_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Lock) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Lock) ProtoMessage() {}
func (x *Lock) ProtoReflect() protoreflect.Message {
mi := &file_teleport_scopes_access_v1_role_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Lock.ProtoReflect.Descriptor instead.
func (*Lock) Descriptor() ([]byte, []int) {
return file_teleport_scopes_access_v1_role_proto_rawDescGZIP(), []int{12}
}
func (x *Lock) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
var File_teleport_scopes_access_v1_role_proto protoreflect.FileDescriptor
const file_teleport_scopes_access_v1_role_proto_rawDesc = "" +
@@ -908,10 +1014,13 @@ const file_teleport_scopes_access_v1_role_proto_rawDesc = "" +
"\bdefaults\x18\x05 \x01(\v2-.teleport.scopes.access.v1.ScopedRoleDefaultsR\bdefaults\x12;\n" +
"\x05rules\x18\x06 \x03(\v2%.teleport.scopes.access.v1.ScopedRuleR\x05rules\x12:\n" +
"\x03ssh\x18\a \x01(\v2(.teleport.scopes.access.v1.ScopedRoleSSHR\x03ssh\x12=\n" +
"\x04kube\x18\b \x01(\v2).teleport.scopes.access.v1.ScopedRoleKubeR\x04kubeJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04R\x05allowR\aoptions\"\x9e\x01\n" +
"\x04kube\x18\b \x01(\v2).teleport.scopes.access.v1.ScopedRoleKubeR\x04kubeJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04R\x05allowR\aoptions\"\xac\x02\n" +
"\x12ScopedRoleDefaults\x12.\n" +
"\x13client_idle_timeout\x18\x01 \x01(\tR\x11clientIdleTimeout\x12X\n" +
"\x11session_recording\x18\x02 \x01(\v2+.teleport.scopes.access.v1.SessionRecordingR\x10sessionRecording\"\x8b\x06\n" +
"\x11session_recording\x18\x02 \x01(\v2+.teleport.scopes.access.v1.SessionRecordingR\x10sessionRecording\x12;\n" +
"\x17disconnect_expired_cert\x18\x03 \x01(\bH\x00R\x15disconnectExpiredCert\x88\x01\x01\x123\n" +
"\x04lock\x18\x04 \x01(\v2\x1f.teleport.scopes.access.v1.LockR\x04lockB\x1a\n" +
"\x18_disconnect_expired_cert\"\x99\a\n" +
"\rScopedRoleSSH\x12\x16\n" +
"\x06logins\x18\x01 \x03(\tR\x06logins\x120\n" +
"\x06labels\x18\x02 \x03(\v2\x18.teleport.label.v1.LabelR\x06labels\x12.\n" +
@@ -925,17 +1034,23 @@ const file_teleport_scopes_access_v1_role_proto_rawDesc = "" +
" \x03(\tR\vhostSudoers\x12 \n" +
"\tfile_copy\x18\v \x01(\bH\x03R\bfileCopy\x88\x01\x01\x12[\n" +
"\x12enhanced_recording\x18\f \x01(\v2,.teleport.scopes.access.v1.EnhancedRecordingR\x11enhancedRecording\x12X\n" +
"\x11session_recording\x18\r \x01(\v2+.teleport.scopes.access.v1.SessionRecordingR\x10sessionRecordingB\x18\n" +
"\x11session_recording\x18\r \x01(\v2+.teleport.scopes.access.v1.SessionRecordingR\x10sessionRecording\x12;\n" +
"\x17disconnect_expired_cert\x18\x0e \x01(\bH\x04R\x15disconnectExpiredCert\x88\x01\x01\x123\n" +
"\x04lock\x18\x0f \x01(\v2\x1f.teleport.scopes.access.v1.LockR\x04lockB\x18\n" +
"\x16_permit_x11_forwardingB\x10\n" +
"\x0e_forward_agentB\x0f\n" +
"\r_max_sessionsB\f\n" +
"\n" +
"_file_copy\"\xb1\x01\n" +
"_file_copyB\x1a\n" +
"\x18_disconnect_expired_cert\"\xbf\x02\n" +
"\x0eScopedRoleKube\x120\n" +
"\x06labels\x18\x01 \x03(\v2\x18.teleport.label.v1.LabelR\x06labels\x12\x16\n" +
"\x06groups\x18\x02 \x03(\tR\x06groups\x12\x14\n" +
"\x05users\x18\x03 \x03(\tR\x05users\x12.\n" +
"\x13client_idle_timeout\x18\x05 \x01(\tR\x11clientIdleTimeoutJ\x04\b\x04\x10\x05R\tresources\"@\n" +
"\x13client_idle_timeout\x18\x05 \x01(\tR\x11clientIdleTimeout\x12;\n" +
"\x17disconnect_expired_cert\x18\x06 \x01(\bH\x00R\x15disconnectExpiredCert\x88\x01\x01\x123\n" +
"\x04lock\x18\a \x01(\v2\x1f.teleport.scopes.access.v1.LockR\x04lockB\x1a\n" +
"\x18_disconnect_expired_certJ\x04\b\x04\x10\x05R\tresources\"@\n" +
"\n" +
"ScopedRule\x12\x1c\n" +
"\tresources\x18\x01 \x03(\tR\tresources\x12\x14\n" +
@@ -965,6 +1080,8 @@ const file_teleport_scopes_access_v1_role_proto_rawDesc = "" +
"\b_networkB\a\n" +
"\x05_disk\"&\n" +
"\x10SessionRecording\x12\x12\n" +
"\x04mode\x18\x01 \x01(\tR\x04mode\"\x1a\n" +
"\x04Lock\x12\x12\n" +
"\x04mode\x18\x01 \x01(\tR\x04modeBWZUgithub.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1;accessv1b\x06proto3"
var (
@@ -979,7 +1096,7 @@ func file_teleport_scopes_access_v1_role_proto_rawDescGZIP() []byte {
return file_teleport_scopes_access_v1_role_proto_rawDescData
}
var file_teleport_scopes_access_v1_role_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_teleport_scopes_access_v1_role_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_teleport_scopes_access_v1_role_proto_goTypes = []any{
(*ScopedRole)(nil), // 0: teleport.scopes.access.v1.ScopedRole
(*ScopedRoleSpec)(nil), // 1: teleport.scopes.access.v1.ScopedRoleSpec
@@ -993,30 +1110,34 @@ var file_teleport_scopes_access_v1_role_proto_goTypes = []any{
(*CreateHostUser)(nil), // 9: teleport.scopes.access.v1.CreateHostUser
(*EnhancedRecording)(nil), // 10: teleport.scopes.access.v1.EnhancedRecording
(*SessionRecording)(nil), // 11: teleport.scopes.access.v1.SessionRecording
(*v1.Metadata)(nil), // 12: teleport.header.v1.Metadata
(*v11.Label)(nil), // 13: teleport.label.v1.Label
(*Lock)(nil), // 12: teleport.scopes.access.v1.Lock
(*v1.Metadata)(nil), // 13: teleport.header.v1.Metadata
(*v11.Label)(nil), // 14: teleport.label.v1.Label
}
var file_teleport_scopes_access_v1_role_proto_depIdxs = []int32{
12, // 0: teleport.scopes.access.v1.ScopedRole.metadata:type_name -> teleport.header.v1.Metadata
13, // 0: teleport.scopes.access.v1.ScopedRole.metadata:type_name -> teleport.header.v1.Metadata
1, // 1: teleport.scopes.access.v1.ScopedRole.spec:type_name -> teleport.scopes.access.v1.ScopedRoleSpec
2, // 2: teleport.scopes.access.v1.ScopedRoleSpec.defaults:type_name -> teleport.scopes.access.v1.ScopedRoleDefaults
5, // 3: teleport.scopes.access.v1.ScopedRoleSpec.rules:type_name -> teleport.scopes.access.v1.ScopedRule
3, // 4: teleport.scopes.access.v1.ScopedRoleSpec.ssh:type_name -> teleport.scopes.access.v1.ScopedRoleSSH
4, // 5: teleport.scopes.access.v1.ScopedRoleSpec.kube:type_name -> teleport.scopes.access.v1.ScopedRoleKube
11, // 6: teleport.scopes.access.v1.ScopedRoleDefaults.session_recording:type_name -> teleport.scopes.access.v1.SessionRecording
13, // 7: teleport.scopes.access.v1.ScopedRoleSSH.labels:type_name -> teleport.label.v1.Label
6, // 8: teleport.scopes.access.v1.ScopedRoleSSH.port_forwarding:type_name -> teleport.scopes.access.v1.SSHPortForwarding
9, // 9: teleport.scopes.access.v1.ScopedRoleSSH.host_user_creation:type_name -> teleport.scopes.access.v1.CreateHostUser
10, // 10: teleport.scopes.access.v1.ScopedRoleSSH.enhanced_recording:type_name -> teleport.scopes.access.v1.EnhancedRecording
11, // 11: teleport.scopes.access.v1.ScopedRoleSSH.session_recording:type_name -> teleport.scopes.access.v1.SessionRecording
13, // 12: teleport.scopes.access.v1.ScopedRoleKube.labels:type_name -> teleport.label.v1.Label
7, // 13: teleport.scopes.access.v1.SSHPortForwarding.local:type_name -> teleport.scopes.access.v1.SSHLocalPortForwarding
8, // 14: teleport.scopes.access.v1.SSHPortForwarding.remote:type_name -> teleport.scopes.access.v1.SSHRemotePortForwarding
15, // [15:15] is the sub-list for method output_type
15, // [15:15] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
12, // 7: teleport.scopes.access.v1.ScopedRoleDefaults.lock:type_name -> teleport.scopes.access.v1.Lock
14, // 8: teleport.scopes.access.v1.ScopedRoleSSH.labels:type_name -> teleport.label.v1.Label
6, // 9: teleport.scopes.access.v1.ScopedRoleSSH.port_forwarding:type_name -> teleport.scopes.access.v1.SSHPortForwarding
9, // 10: teleport.scopes.access.v1.ScopedRoleSSH.host_user_creation:type_name -> teleport.scopes.access.v1.CreateHostUser
10, // 11: teleport.scopes.access.v1.ScopedRoleSSH.enhanced_recording:type_name -> teleport.scopes.access.v1.EnhancedRecording
11, // 12: teleport.scopes.access.v1.ScopedRoleSSH.session_recording:type_name -> teleport.scopes.access.v1.SessionRecording
12, // 13: teleport.scopes.access.v1.ScopedRoleSSH.lock:type_name -> teleport.scopes.access.v1.Lock
14, // 14: teleport.scopes.access.v1.ScopedRoleKube.labels:type_name -> teleport.label.v1.Label
12, // 15: teleport.scopes.access.v1.ScopedRoleKube.lock:type_name -> teleport.scopes.access.v1.Lock
7, // 16: teleport.scopes.access.v1.SSHPortForwarding.local:type_name -> teleport.scopes.access.v1.SSHLocalPortForwarding
8, // 17: teleport.scopes.access.v1.SSHPortForwarding.remote:type_name -> teleport.scopes.access.v1.SSHRemotePortForwarding
18, // [18:18] is the sub-list for method output_type
18, // [18:18] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
}
func init() { file_teleport_scopes_access_v1_role_proto_init() }
@@ -1024,7 +1145,9 @@ func file_teleport_scopes_access_v1_role_proto_init() {
if File_teleport_scopes_access_v1_role_proto != nil {
return
}
file_teleport_scopes_access_v1_role_proto_msgTypes[2].OneofWrappers = []any{}
file_teleport_scopes_access_v1_role_proto_msgTypes[3].OneofWrappers = []any{}
file_teleport_scopes_access_v1_role_proto_msgTypes[4].OneofWrappers = []any{}
file_teleport_scopes_access_v1_role_proto_msgTypes[7].OneofWrappers = []any{}
file_teleport_scopes_access_v1_role_proto_msgTypes[8].OneofWrappers = []any{}
file_teleport_scopes_access_v1_role_proto_msgTypes[10].OneofWrappers = []any{}
@@ -1034,7 +1157,7 @@ func file_teleport_scopes_access_v1_role_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_scopes_access_v1_role_proto_rawDesc), len(file_teleport_scopes_access_v1_role_proto_rawDesc)),
NumEnums: 0,
NumMessages: 12,
NumMessages: 13,
NumExtensions: 0,
NumServices: 0,
},
+31 -1
View File
@@ -89,6 +89,14 @@ message ScopedRoleDefaults {
// SessionRecording configures the session recording strategy for all protocols that don't
// explicitly set their session recording mode.
SessionRecording session_recording = 2;
// DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session.
// If unset, cluster wide defaults are used.
optional bool disconnect_expired_cert = 3;
// Lock specifies the default locking mode for access sessions across all protocols that
// do not specify their own value. If unset, cluster wide defaults are used.
Lock lock = 4;
}
// ScopedRoleSSH groups all scoped role fields relevant to SSH access. Fields within the SSH block
@@ -136,6 +144,14 @@ message ScopedRoleSSH {
// SessionRecording configures the session recording strategy for SSH sessions.
SessionRecording session_recording = 13;
// DisconnectExpiredCert controls whether SSH sessions are disconnected when the
// user certificate expires.
// Defaults to value cluster wide auth preference if not set.
optional bool disconnect_expired_cert = 14;
// Lock configures the role's locking behavior for SSH sessions.
Lock lock = 15;
}
// The group of all scoped role fields relevant to kube access. Fields within the kube block
@@ -160,6 +176,12 @@ message ScopedRoleKube {
// Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value
// (or global default) applies.
string client_idle_timeout = 5;
// DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.
optional bool disconnect_expired_cert = 6;
// Lock configures the role's locking behavior for kubernetes sessions.
Lock lock = 7;
}
// ScopedRule maps resources to verbs. This is the underlying type used to describe
@@ -213,6 +235,14 @@ message EnhancedRecording {
// SessionRecording sets the session recording behavior.
message SessionRecording {
// Mode sets the session recording mode. Allowed values: strict | best_effort.
// Mode sets the session recording mode. Allowed values: strict or best_effort.
string mode = 1;
}
// Lock controls locking mode for sessions authorized via this
// scoped role.
message Lock {
// Allowed values: strict or best_effort.
// Defaults to value cluster wide auth preference if not set.
string mode = 1;
}
@@ -41,21 +41,31 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|Field|Type|Description|
|---|---|---|
|client_idle_timeout|string|ClientIdleTimeout sets the default idle timeout for access sessions across all protocols that do not specify their own value. Must be a valid Go duration string (e.g. "30m", "1h").|
|disconnect_expired_cert|boolean|DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session. If unset, cluster wide defaults are used.|
|lock|[object](#specdefaultslock)|Lock specifies the default locking mode for access sessions across all protocols that do not specify their own value. If unset, cluster wide defaults are used.|
|session_recording|[object](#specdefaultssession_recording)|SessionRecording configures the session recording strategy for all protocols that don't explicitly set their session recording mode.|
### spec.defaults.lock
|Field|Type|Description|
|---|---|---|
|mode|string|Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.|
### spec.defaults.session_recording
|Field|Type|Description|
|---|---|---|
|mode|string|Mode sets the session recording mode. Allowed values: strict | best_effort.|
|mode|string|Mode sets the session recording mode. Allowed values: strict or best_effort.|
### spec.kube
|Field|Type|Description|
|---|---|---|
|client_idle_timeout|string|Overrides the defaults block idle timeout specifically for kube sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.|
|disconnect_expired_cert|boolean|DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.|
|groups|[]string|The list of kubernetes groups this role allows.|
|labels|[][object](#speckubelabels-items)|The map of kubernetes cluster labels used for RBAC.|
|lock|[object](#speckubelock)|Lock configures the role's locking behavior for kubernetes sessions.|
|users|[]string|An optional list of impersonatable kubernetes users this role allows.|
### spec.kube.labels items
@@ -65,6 +75,12 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|name|string|The name of the label.|
|values|[]string|The values associated with the label.|
### spec.kube.lock
|Field|Type|Description|
|---|---|---|
|mode|string|Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.|
### spec.rules items
|Field|Type|Description|
@@ -77,12 +93,14 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|Field|Type|Description|
|---|---|---|
|client_idle_timeout|string|ClientIdleTimeout overrides the defaults block idle timeout specifically for SSH sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.|
|disconnect_expired_cert|boolean|DisconnectExpiredCert controls whether SSH sessions are disconnected when the user certificate expires. Defaults to value cluster wide auth preference if not set.|
|enhanced_recording|[object](#specsshenhanced_recording)|EnhancedRecording is the set of BPF events to record for enhanced session recording.|
|file_copy|boolean|FileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults to allowing the user to download and upload files by default.|
|forward_agent|boolean|ForwardAgent enables SSH agent forwarding.|
|host_sudoers|[]string|Sudoers is a list of entries to include in a users sudoer file|
|host_user_creation|[object](#specsshhost_user_creation)|HostUserCreation configures the creation of host users.|
|labels|[][object](#specsshlabels-items)|Labels is the set of node labels used to dynamically select which nodes this role applies to.|
|lock|[object](#specsshlock)|Lock configures the role's locking behavior for SSH sessions.|
|logins|[]string|Logins is the list of OS logins this role permits on matching nodes.|
|max_sessions|integer|MaxSessions defines the maximum number of concurrent sessions per connection.|
|permit_x11_forwarding|boolean|PermitX11Forwarding, when true, authorizes use of X11 forwarding over SSH sessions. If not set, X11 forwarding is not permitted.|
@@ -112,6 +130,12 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|name|string|The name of the label.|
|values|[]string|The values associated with the label.|
### spec.ssh.lock
|Field|Type|Description|
|---|---|---|
|mode|string|Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.|
### spec.ssh.port_forwarding
|Field|Type|Description|
@@ -135,5 +159,5 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|Field|Type|Description|
|---|---|---|
|mode|string|Mode sets the session recording mode. Allowed values: strict | best_effort.|
|mode|string|Mode sets the session recording mode. Allowed values: strict or best_effort.|
@@ -63,13 +63,22 @@ Optional:
Optional:
- `client_idle_timeout` (String) ClientIdleTimeout sets the default idle timeout for access sessions across all protocols that do not specify their own value. Must be a valid Go duration string (e.g. "30m", "1h").
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session. If unset, cluster wide defaults are used.
- `lock` (Attributes) Lock specifies the default locking mode for access sessions across all protocols that do not specify their own value. If unset, cluster wide defaults are used. (see [below for nested schema](#nested-schema-for-specdefaultslock))
- `session_recording` (Attributes) SessionRecording configures the session recording strategy for all protocols that don't explicitly set their session recording mode. (see [below for nested schema](#nested-schema-for-specdefaultssession_recording))
### Nested Schema for `spec.defaults.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.defaults.session_recording`
Optional:
- `mode` (String) Mode sets the session recording mode. Allowed values: strict | best_effort.
- `mode` (String) Mode sets the session recording mode. Allowed values: strict or best_effort.
@@ -78,8 +87,10 @@ Optional:
Optional:
- `client_idle_timeout` (String) Overrides the defaults block idle timeout specifically for kube sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.
- `groups` (List of String) The list of kubernetes groups this role allows.
- `labels` (Attributes List) The map of kubernetes cluster labels used for RBAC. (see [below for nested schema](#nested-schema-for-speckubelabels))
- `lock` (Attributes) Lock configures the role's locking behavior for kubernetes sessions. (see [below for nested schema](#nested-schema-for-speckubelock))
- `users` (List of String) An optional list of impersonatable kubernetes users this role allows.
### Nested Schema for `spec.kube.labels`
@@ -90,6 +101,13 @@ Optional:
- `values` (List of String) The values associated with the label.
### Nested Schema for `spec.kube.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.rules`
@@ -104,12 +122,14 @@ Optional:
Optional:
- `client_idle_timeout` (String) ClientIdleTimeout overrides the defaults block idle timeout specifically for SSH sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert controls whether SSH sessions are disconnected when the user certificate expires. Defaults to value cluster wide auth preference if not set.
- `enhanced_recording` (Attributes) EnhancedRecording is the set of BPF events to record for enhanced session recording. (see [below for nested schema](#nested-schema-for-specsshenhanced_recording))
- `file_copy` (Boolean) FileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults to allowing the user to download and upload files by default.
- `forward_agent` (Boolean) ForwardAgent enables SSH agent forwarding.
- `host_sudoers` (List of String) Sudoers is a list of entries to include in a users sudoer file
- `host_user_creation` (Attributes) HostUserCreation configures the creation of host users. (see [below for nested schema](#nested-schema-for-specsshhost_user_creation))
- `labels` (Attributes List) Labels is the set of node labels used to dynamically select which nodes this role applies to. (see [below for nested schema](#nested-schema-for-specsshlabels))
- `lock` (Attributes) Lock configures the role's locking behavior for SSH sessions. (see [below for nested schema](#nested-schema-for-specsshlock))
- `logins` (List of String) Logins is the list of OS logins this role permits on matching nodes.
- `max_sessions` (Number) MaxSessions defines the maximum number of concurrent sessions per connection.
- `permit_x11_forwarding` (Boolean) PermitX11Forwarding, when true, authorizes use of X11 forwarding over SSH sessions. If not set, X11 forwarding is not permitted.
@@ -142,6 +162,13 @@ Optional:
- `values` (List of String) The values associated with the label.
### Nested Schema for `spec.ssh.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.ssh.port_forwarding`
Optional:
@@ -168,5 +195,5 @@ Optional:
Optional:
- `mode` (String) Mode sets the session recording mode. Allowed values: strict | best_effort.
- `mode` (String) Mode sets the session recording mode. Allowed values: strict or best_effort.
@@ -92,13 +92,22 @@ Optional:
Optional:
- `client_idle_timeout` (String) ClientIdleTimeout sets the default idle timeout for access sessions across all protocols that do not specify their own value. Must be a valid Go duration string (e.g. "30m", "1h").
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session. If unset, cluster wide defaults are used.
- `lock` (Attributes) Lock specifies the default locking mode for access sessions across all protocols that do not specify their own value. If unset, cluster wide defaults are used. (see [below for nested schema](#nested-schema-for-specdefaultslock))
- `session_recording` (Attributes) SessionRecording configures the session recording strategy for all protocols that don't explicitly set their session recording mode. (see [below for nested schema](#nested-schema-for-specdefaultssession_recording))
### Nested Schema for `spec.defaults.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.defaults.session_recording`
Optional:
- `mode` (String) Mode sets the session recording mode. Allowed values: strict | best_effort.
- `mode` (String) Mode sets the session recording mode. Allowed values: strict or best_effort.
@@ -107,8 +116,10 @@ Optional:
Optional:
- `client_idle_timeout` (String) Overrides the defaults block idle timeout specifically for kube sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.
- `groups` (List of String) The list of kubernetes groups this role allows.
- `labels` (Attributes List) The map of kubernetes cluster labels used for RBAC. (see [below for nested schema](#nested-schema-for-speckubelabels))
- `lock` (Attributes) Lock configures the role's locking behavior for kubernetes sessions. (see [below for nested schema](#nested-schema-for-speckubelock))
- `users` (List of String) An optional list of impersonatable kubernetes users this role allows.
### Nested Schema for `spec.kube.labels`
@@ -119,6 +130,13 @@ Optional:
- `values` (List of String) The values associated with the label.
### Nested Schema for `spec.kube.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.rules`
@@ -133,12 +151,14 @@ Optional:
Optional:
- `client_idle_timeout` (String) ClientIdleTimeout overrides the defaults block idle timeout specifically for SSH sessions. Must be a valid Go duration string (e.g. "30m", "1h"). If empty, the defaults block value (or global default) applies.
- `disconnect_expired_cert` (Boolean) DisconnectExpiredCert controls whether SSH sessions are disconnected when the user certificate expires. Defaults to value cluster wide auth preference if not set.
- `enhanced_recording` (Attributes) EnhancedRecording is the set of BPF events to record for enhanced session recording. (see [below for nested schema](#nested-schema-for-specsshenhanced_recording))
- `file_copy` (Boolean) FileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults to allowing the user to download and upload files by default.
- `forward_agent` (Boolean) ForwardAgent enables SSH agent forwarding.
- `host_sudoers` (List of String) Sudoers is a list of entries to include in a users sudoer file
- `host_user_creation` (Attributes) HostUserCreation configures the creation of host users. (see [below for nested schema](#nested-schema-for-specsshhost_user_creation))
- `labels` (Attributes List) Labels is the set of node labels used to dynamically select which nodes this role applies to. (see [below for nested schema](#nested-schema-for-specsshlabels))
- `lock` (Attributes) Lock configures the role's locking behavior for SSH sessions. (see [below for nested schema](#nested-schema-for-specsshlock))
- `logins` (List of String) Logins is the list of OS logins this role permits on matching nodes.
- `max_sessions` (Number) MaxSessions defines the maximum number of concurrent sessions per connection.
- `permit_x11_forwarding` (Boolean) PermitX11Forwarding, when true, authorizes use of X11 forwarding over SSH sessions. If not set, X11 forwarding is not permitted.
@@ -171,6 +191,13 @@ Optional:
- `values` (List of String) The values associated with the label.
### Nested Schema for `spec.ssh.lock`
Optional:
- `mode` (String) Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.
### Nested Schema for `spec.ssh.port_forwarding`
Optional:
@@ -197,4 +224,4 @@ Optional:
Optional:
- `mode` (String) Mode sets the session recording mode. Allowed values: strict | best_effort.
- `mode` (String) Mode sets the session recording mode. Allowed values: strict or best_effort.
@@ -56,6 +56,22 @@ spec:
access sessions across all protocols that do not specify their
own value. Must be a valid Go duration string (e.g. "30m", "1h").
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert defines the default behavior
of all protocols when certs expire for a session. If unset,
cluster wide defaults are used.
type: boolean
lock:
description: Lock specifies the default locking mode for access
sessions across all protocols that do not specify their own
value. If unset, cluster wide defaults are used.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
session_recording:
description: SessionRecording configures the session recording
strategy for all protocols that don't explicitly set their session
@@ -64,7 +80,7 @@ spec:
properties:
mode:
description: 'Mode sets the session recording mode. Allowed
values: strict | best_effort.'
values: strict or best_effort.'
type: string
type: object
type: object
@@ -78,6 +94,10 @@ spec:
"30m", "1h"). If empty, the defaults block value (or global
default) applies.
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert controls whether Kube sessions
are disconnected when the user certificate expires.
type: boolean
groups:
description: The list of kubernetes groups this role allows.
items:
@@ -100,6 +120,16 @@ spec:
type: object
nullable: true
type: array
lock:
description: Lock configures the role's locking behavior for kubernetes
sessions.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
users:
description: An optional list of impersonatable kubernetes users
this role allows.
@@ -140,6 +170,11 @@ spec:
string (e.g. "30m", "1h"). If empty, the defaults block value
(or global default) applies.
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert controls whether SSH sessions
are disconnected when the user certificate expires. Defaults
to value cluster wide auth preference if not set.
type: boolean
enhanced_recording:
description: EnhancedRecording is the set of BPF events to record
for enhanced session recording.
@@ -206,6 +241,16 @@ spec:
type: object
nullable: true
type: array
lock:
description: Lock configures the role's locking behavior for SSH
sessions.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
logins:
description: Logins is the list of OS logins this role permits
on matching nodes.
@@ -250,7 +295,7 @@ spec:
properties:
mode:
description: 'Mode sets the session recording mode. Allowed
values: strict | best_effort.'
values: strict or best_effort.'
type: string
type: object
type: object
@@ -56,6 +56,22 @@ spec:
access sessions across all protocols that do not specify their
own value. Must be a valid Go duration string (e.g. "30m", "1h").
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert defines the default behavior
of all protocols when certs expire for a session. If unset,
cluster wide defaults are used.
type: boolean
lock:
description: Lock specifies the default locking mode for access
sessions across all protocols that do not specify their own
value. If unset, cluster wide defaults are used.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
session_recording:
description: SessionRecording configures the session recording
strategy for all protocols that don't explicitly set their session
@@ -64,7 +80,7 @@ spec:
properties:
mode:
description: 'Mode sets the session recording mode. Allowed
values: strict | best_effort.'
values: strict or best_effort.'
type: string
type: object
type: object
@@ -78,6 +94,10 @@ spec:
"30m", "1h"). If empty, the defaults block value (or global
default) applies.
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert controls whether Kube sessions
are disconnected when the user certificate expires.
type: boolean
groups:
description: The list of kubernetes groups this role allows.
items:
@@ -100,6 +120,16 @@ spec:
type: object
nullable: true
type: array
lock:
description: Lock configures the role's locking behavior for kubernetes
sessions.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
users:
description: An optional list of impersonatable kubernetes users
this role allows.
@@ -140,6 +170,11 @@ spec:
string (e.g. "30m", "1h"). If empty, the defaults block value
(or global default) applies.
type: string
disconnect_expired_cert:
description: DisconnectExpiredCert controls whether SSH sessions
are disconnected when the user certificate expires. Defaults
to value cluster wide auth preference if not set.
type: boolean
enhanced_recording:
description: EnhancedRecording is the set of BPF events to record
for enhanced session recording.
@@ -206,6 +241,16 @@ spec:
type: object
nullable: true
type: array
lock:
description: Lock configures the role's locking behavior for SSH
sessions.
nullable: true
properties:
mode:
description: 'Allowed values: strict or best_effort. Defaults
to value cluster wide auth preference if not set.'
type: string
type: object
logins:
description: Logins is the list of OS logins this role permits
on matching nodes.
@@ -250,7 +295,7 @@ spec:
properties:
mode:
description: 'Mode sets the session recording mode. Allowed
values: strict | best_effort.'
values: strict or best_effort.'
type: string
type: object
type: object
@@ -121,9 +121,23 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"disconnect_expired_cert": {
Description: "DisconnectExpiredCert defines the default behavior of all protocols when certs expire for a session. If unset, cluster wide defaults are used.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.BoolType,
},
"lock": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"mode": {
Description: "Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
}}),
Description: "Lock specifies the default locking mode for access sessions across all protocols that do not specify their own value. If unset, cluster wide defaults are used.",
Optional: true,
},
"session_recording": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"mode": {
Description: "Mode sets the session recording mode. Allowed values: strict | best_effort.",
Description: "Mode sets the session recording mode. Allowed values: strict or best_effort.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
}}),
@@ -141,6 +155,11 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"disconnect_expired_cert": {
Description: "DisconnectExpiredCert controls whether Kube sessions are disconnected when the user certificate expires.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.BoolType,
},
"groups": {
Description: "The list of kubernetes groups this role allows.",
Optional: true,
@@ -162,6 +181,15 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
Description: "The map of kubernetes cluster labels used for RBAC.",
Optional: true,
},
"lock": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"mode": {
Description: "Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
}}),
Description: "Lock configures the role's locking behavior for kubernetes sessions.",
Optional: true,
},
"users": {
Description: "An optional list of impersonatable kubernetes users this role allows.",
Optional: true,
@@ -194,6 +222,11 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"disconnect_expired_cert": {
Description: "DisconnectExpiredCert controls whether SSH sessions are disconnected when the user certificate expires. Defaults to value cluster wide auth preference if not set.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.BoolType,
},
"enhanced_recording": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
"command": {
@@ -267,6 +300,15 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
Description: "Labels is the set of node labels used to dynamically select which nodes this role applies to.",
Optional: true,
},
"lock": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"mode": {
Description: "Allowed values: strict or best_effort. Defaults to value cluster wide auth preference if not set.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
}}),
Description: "Lock configures the role's locking behavior for SSH sessions.",
Optional: true,
},
"logins": {
Description: "Logins is the list of OS logins this role permits on matching nodes.",
Optional: true,
@@ -308,7 +350,7 @@ func GenSchemaScopedRole(ctx context.Context) (github_com_hashicorp_terraform_pl
},
"session_recording": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"mode": {
Description: "Mode sets the session recording mode. Allowed values: strict | best_effort.",
Description: "Mode sets the session recording mode. Allowed values: strict or best_effort.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
}}),
@@ -634,6 +676,59 @@ func CopyScopedRoleFromTerraform(_ context.Context, tf github_com_hashicorp_terr
}
}
}
{
a, ok := tf.Attrs["disconnect_expired_cert"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.defaults.disconnect_expired_cert"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.defaults.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
} else {
var t *bool
if !v.Null && !v.Unknown {
c := bool(v.Value)
t = &c
}
obj.DisconnectExpiredCert = t
}
}
}
{
a, ok := tf.Attrs["lock"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.defaults.lock"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.defaults.lock", "github.com/hashicorp/terraform-plugin-framework/types.Object"})
} else {
obj.Lock = nil
if !v.Null && !v.Unknown {
tf := v
obj.Lock = &github_com_gravitational_teleport_api_gen_proto_go_teleport_scopes_access_v1.Lock{}
obj := obj.Lock
{
a, ok := tf.Attrs["mode"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.defaults.lock.mode"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.defaults.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
} else {
var t string
if !v.Null && !v.Unknown {
t = string(v.Value)
}
obj.Mode = t
}
}
}
}
}
}
}
}
}
}
@@ -1227,6 +1322,59 @@ func CopyScopedRoleFromTerraform(_ context.Context, tf github_com_hashicorp_terr
}
}
}
{
a, ok := tf.Attrs["disconnect_expired_cert"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.ssh.disconnect_expired_cert"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.ssh.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
} else {
var t *bool
if !v.Null && !v.Unknown {
c := bool(v.Value)
t = &c
}
obj.DisconnectExpiredCert = t
}
}
}
{
a, ok := tf.Attrs["lock"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.ssh.lock"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.ssh.lock", "github.com/hashicorp/terraform-plugin-framework/types.Object"})
} else {
obj.Lock = nil
if !v.Null && !v.Unknown {
tf := v
obj.Lock = &github_com_gravitational_teleport_api_gen_proto_go_teleport_scopes_access_v1.Lock{}
obj := obj.Lock
{
a, ok := tf.Attrs["mode"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.ssh.lock.mode"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.ssh.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
} else {
var t string
if !v.Null && !v.Unknown {
t = string(v.Value)
}
obj.Mode = t
}
}
}
}
}
}
}
}
}
}
@@ -1389,6 +1537,59 @@ func CopyScopedRoleFromTerraform(_ context.Context, tf github_com_hashicorp_terr
}
}
}
{
a, ok := tf.Attrs["disconnect_expired_cert"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.kube.disconnect_expired_cert"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.kube.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
} else {
var t *bool
if !v.Null && !v.Unknown {
c := bool(v.Value)
t = &c
}
obj.DisconnectExpiredCert = t
}
}
}
{
a, ok := tf.Attrs["lock"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.kube.lock"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.kube.lock", "github.com/hashicorp/terraform-plugin-framework/types.Object"})
} else {
obj.Lock = nil
if !v.Null && !v.Unknown {
tf := v
obj.Lock = &github_com_gravitational_teleport_api_gen_proto_go_teleport_scopes_access_v1.Lock{}
obj := obj.Lock
{
a, ok := tf.Attrs["mode"]
if !ok {
diags.Append(attrReadMissingDiag{"ScopedRole.spec.kube.lock.mode"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrReadConversionFailureDiag{"ScopedRole.spec.kube.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
} else {
var t string
if !v.Null && !v.Unknown {
t = string(v.Value)
}
obj.Mode = t
}
}
}
}
}
}
}
}
}
}
@@ -1856,6 +2057,86 @@ func CopyScopedRoleToTerraform(ctx context.Context, obj *github_com_gravitationa
}
}
}
{
t, ok := tf.AttrTypes["disconnect_expired_cert"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.defaults.disconnect_expired_cert"})
} else {
v, ok := tf.Attrs["disconnect_expired_cert"].(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.defaults.disconnect_expired_cert", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.defaults.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
}
}
if obj.DisconnectExpiredCert == nil {
v.Null = true
} else {
v.Null = false
v.Value = bool(*obj.DisconnectExpiredCert)
}
v.Unknown = false
tf.Attrs["disconnect_expired_cert"] = v
}
}
{
a, ok := tf.AttrTypes["lock"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.defaults.lock"})
} else {
o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.defaults.lock", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"})
} else {
v, ok := tf.Attrs["lock"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
v = github_com_hashicorp_terraform_plugin_framework_types.Object{
AttrTypes: o.AttrTypes,
Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)),
}
} else {
if v.Attrs == nil {
v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes))
}
}
if obj.Lock == nil {
v.Null = true
} else {
obj := obj.Lock
tf := &v
{
t, ok := tf.AttrTypes["mode"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.defaults.lock.mode"})
} else {
v, ok := tf.Attrs["mode"].(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.defaults.lock.mode", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.defaults.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
}
v.Null = string(obj.Mode) == ""
}
v.Value = string(obj.Mode)
v.Unknown = false
tf.Attrs["mode"] = v
}
}
}
v.Unknown = false
tf.Attrs["lock"] = v
}
}
}
}
v.Unknown = false
tf.Attrs["defaults"] = v
@@ -2858,6 +3139,86 @@ func CopyScopedRoleToTerraform(ctx context.Context, obj *github_com_gravitationa
}
}
}
{
t, ok := tf.AttrTypes["disconnect_expired_cert"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.ssh.disconnect_expired_cert"})
} else {
v, ok := tf.Attrs["disconnect_expired_cert"].(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.ssh.disconnect_expired_cert", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.ssh.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
}
}
if obj.DisconnectExpiredCert == nil {
v.Null = true
} else {
v.Null = false
v.Value = bool(*obj.DisconnectExpiredCert)
}
v.Unknown = false
tf.Attrs["disconnect_expired_cert"] = v
}
}
{
a, ok := tf.AttrTypes["lock"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.ssh.lock"})
} else {
o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.ssh.lock", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"})
} else {
v, ok := tf.Attrs["lock"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
v = github_com_hashicorp_terraform_plugin_framework_types.Object{
AttrTypes: o.AttrTypes,
Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)),
}
} else {
if v.Attrs == nil {
v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes))
}
}
if obj.Lock == nil {
v.Null = true
} else {
obj := obj.Lock
tf := &v
{
t, ok := tf.AttrTypes["mode"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.ssh.lock.mode"})
} else {
v, ok := tf.Attrs["mode"].(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.ssh.lock.mode", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.ssh.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
}
v.Null = string(obj.Mode) == ""
}
v.Value = string(obj.Mode)
v.Unknown = false
tf.Attrs["mode"] = v
}
}
}
v.Unknown = false
tf.Attrs["lock"] = v
}
}
}
}
v.Unknown = false
tf.Attrs["ssh"] = v
@@ -3151,6 +3512,86 @@ func CopyScopedRoleToTerraform(ctx context.Context, obj *github_com_gravitationa
tf.Attrs["client_idle_timeout"] = v
}
}
{
t, ok := tf.AttrTypes["disconnect_expired_cert"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.kube.disconnect_expired_cert"})
} else {
v, ok := tf.Attrs["disconnect_expired_cert"].(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.kube.disconnect_expired_cert", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.Bool)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.kube.disconnect_expired_cert", "github.com/hashicorp/terraform-plugin-framework/types.Bool"})
}
}
if obj.DisconnectExpiredCert == nil {
v.Null = true
} else {
v.Null = false
v.Value = bool(*obj.DisconnectExpiredCert)
}
v.Unknown = false
tf.Attrs["disconnect_expired_cert"] = v
}
}
{
a, ok := tf.AttrTypes["lock"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.kube.lock"})
} else {
o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.kube.lock", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"})
} else {
v, ok := tf.Attrs["lock"].(github_com_hashicorp_terraform_plugin_framework_types.Object)
if !ok {
v = github_com_hashicorp_terraform_plugin_framework_types.Object{
AttrTypes: o.AttrTypes,
Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)),
}
} else {
if v.Attrs == nil {
v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes))
}
}
if obj.Lock == nil {
v.Null = true
} else {
obj := obj.Lock
tf := &v
{
t, ok := tf.AttrTypes["mode"]
if !ok {
diags.Append(attrWriteMissingDiag{"ScopedRole.spec.kube.lock.mode"})
} else {
v, ok := tf.Attrs["mode"].(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"ScopedRole.spec.kube.lock.mode", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"ScopedRole.spec.kube.lock.mode", "github.com/hashicorp/terraform-plugin-framework/types.String"})
}
v.Null = string(obj.Mode) == ""
}
v.Value = string(obj.Mode)
v.Unknown = false
tf.Attrs["mode"] = v
}
}
}
v.Unknown = false
tf.Attrs["lock"] = v
}
}
}
}
v.Unknown = false
tf.Attrs["kube"] = v
+5 -3
View File
@@ -184,19 +184,21 @@ func (s *ScopedContext) GetDisconnectCertExpiry(authPref readonly.AuthPreference
if s.unscopedContext != nil {
return s.unscopedContext.GetDisconnectCertExpiry(authPref)
}
if !authPref.GetDisconnectExpiredCert() {
return time.Time{}
}
return s.GetDisconnectCertExpiryTime()
}
// GetDisconnectCertExpiryTime returns the timestamp for when the identity expires.
// It takes in a bool parameter to specify whether to return the expiry time or not.
func (s *ScopedContext) GetDisconnectCertExpiryTime() time.Time {
identity := s.Identity.GetIdentity()
if !identity.PreviousIdentityExpires.IsZero() {
// If this is a short-lived mfa verified cert, return the certificate extension
// that holds its issuing certificates expiry value.
return identity.PreviousIdentityExpires
}
// Otherwise, return the current certificates expiration
return identity.Expires
}
+28
View File
@@ -211,6 +211,9 @@ func (f *ForwarderConfig) CheckAndSetDefaults() error {
if f.ScopedAuthz == nil {
return trace.BadParameter("missing parameter ScopedAuthz")
}
if f.LockWatcher == nil {
return trace.BadParameter("missing parameter LockWatcher")
}
if f.Emitter == nil {
return trace.BadParameter("missing parameter Emitter")
}
@@ -470,6 +473,9 @@ type authContext struct {
// It is false if the target cluster is served by another teleport service or a different
// Teleport cluster.
isLocalKubernetesCluster bool
// LockingMode determines the kubernetes' behavior when locks are stale
LockingMode constants.LockingMode
}
func (c authContext) String() string {
@@ -1274,6 +1280,27 @@ func (f *Forwarder) authorize(ctx context.Context, actx *authContext) error {
return trace.Wrap(err)
}
authPref, err := f.cfg.CachingAuthClient.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
if actx.checker.Kube().AdjustDisconnectExpiredCert(authPref.GetDisconnectExpiredCert()) {
actx.disconnectExpiredCert = actx.ScopedContext.GetDisconnectCertExpiryTime()
} else {
actx.disconnectExpiredCert = time.Time{}
}
// For scoped roles, check for the locking mode here so that we can verify whether users can connect or not when not
// when the lock is stale.
if isScoped {
actx.LockingMode = actx.checker.Kube().LockingMode(authPref.GetLockingMode())
// TODO(williamo/scopes): Potentially delete this in favor of checking locks in lib/authz/scoped.go
if err := f.cfg.LockWatcher.CheckLockInForce(actx.LockingMode, actx.LockTargets()...); err != nil {
return trace.Wrap(err)
}
}
// If the user has active Access requests we need to validate that they allow
// the kubeResource.
// This is required because CheckAccess does not validate the subresource type.
@@ -2615,6 +2642,7 @@ func (s *clusterSession) monitorConn(conn net.Conn, err error, hostID string) (n
Emitter: s.parent.cfg.AuthClient,
EmitterContext: s.parent.ctx,
MessageWriter: formatForwardResponseError(s.sendErrStatus),
LockingMode: s.LockingMode,
})
if err != nil {
tc.CloseWithCause(err)
+414 -38
View File
@@ -57,6 +57,11 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
labelv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/label/v1"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/tlsutils"
@@ -69,6 +74,7 @@ import (
testingkubemock "github.com/gravitational/teleport/lib/kube/proxy/testing/kube_server"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/scopes/pinning"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/tlsca"
@@ -121,6 +127,27 @@ func TestAuthenticate(t *testing.T) {
})
require.NoError(t, err)
newTestLockWatcher := func(t *testing.T) *services.LockWatcher {
lockWatcher, err := services.NewLockWatcher(t.Context(), services.LockWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: teleport.ComponentProxy,
Client: &mockEventClient{},
},
})
require.NoError(t, err)
t.Cleanup(lockWatcher.Close)
return lockWatcher
}
lockWatcher := newTestLockWatcher(t)
// marks the lock watcher as stale
configureStaleLockWatcher := func(t *testing.T, f *Forwarder) {
staleLockWatcher := newTestLockWatcher(t)
close(staleLockWatcher.StaleC)
require.Eventually(t, staleLockWatcher.IsStale, time.Second, 10*time.Millisecond)
f.cfg.LockWatcher = staleLockWatcher
}
ap := &mockAccessPoint{
netConfig: nc,
recordingConfig: types.DefaultSessionRecordingConfig(),
@@ -141,49 +168,58 @@ func TestAuthenticate(t *testing.T) {
"local": mockCluster{name: "local"},
},
}
f := &Forwarder{
log: logtest.NewLogger(),
cfg: ForwarderConfig{
ClusterName: "local",
CachingAuthClient: ap,
TracerProvider: otel.GetTracerProvider(),
tracer: otel.Tracer(teleport.ComponentKube),
ClusterFeatures: fakeClusterFeatures,
KubeServiceType: ProxyService,
},
getKubernetesServersForKubeCluster: func(ctx context.Context, name string) ([]types.KubeServer, error) {
servers, err := ap.GetKubernetesServers(ctx)
if err != nil {
return nil, err
}
var filtered []types.KubeServer
for _, server := range servers {
if server.GetCluster().GetName() == name {
filtered = append(filtered, server)
newForwarder := func() *Forwarder {
return &Forwarder{
log: logtest.NewLogger(),
cfg: ForwarderConfig{
ClusterName: "local",
CachingAuthClient: ap,
TracerProvider: otel.GetTracerProvider(),
tracer: otel.Tracer(teleport.ComponentKube),
ClusterFeatures: fakeClusterFeatures,
KubeServiceType: ProxyService,
LockWatcher: lockWatcher,
},
getKubernetesServersForKubeCluster: func(ctx context.Context, name string) ([]types.KubeServer, error) {
servers, err := ap.GetKubernetesServers(ctx)
if err != nil {
return nil, err
}
}
return filtered, nil
},
var filtered []types.KubeServer
for _, server := range servers {
if server.GetCluster().GetName() == name {
filtered = append(filtered, server)
}
}
return filtered, nil
},
}
}
const remoteAddr = "user.example.com"
activeAccessRequests := []string{uuid.NewString(), uuid.NewString()}
tests := []struct {
desc string
user authz.IdentityGetter
authzErr bool
roleKubeUsers []string
roleKubeGroups []string
routeToCluster string
kubernetesCluster string
haveKubeCreds bool
tunnel reversetunnelclient.Server
kubeServers []types.KubeServer
activeRequests []string
desc string
user authz.IdentityGetter
authzErr bool
roleKubeUsers []string
roleKubeGroups []string
routeToCluster string
kubernetesCluster string
haveKubeCreds bool
tunnel reversetunnelclient.Server
kubeServers []types.KubeServer
activeRequests []string
configureForwarder func(t *testing.T, f *Forwarder)
scoped bool
scopedKubeLockMode constants.LockingMode
scopedKubeDisconnectExpiredCert *bool
wantCtx *authContext
wantErr bool
wantAuthErr bool
wantCtx *authContext
wantErr bool
wantAuthErr bool
wantAuthorizeErrContains string
wantDisconnectExpiredCert *time.Time
}{
{
desc: "local user and cluster with active access request",
@@ -316,6 +352,227 @@ func TestAuthenticate(t *testing.T) {
),
},
},
{
desc: "scoped user with stale locks and best effort locking produces no error",
user: authz.LocalUser{},
scoped: true,
roleKubeGroups: []string{"kube-group-a", "kube-group-b"},
scopedKubeLockMode: constants.LockingModeBestEffort,
routeToCluster: "local",
kubernetesCluster: "local",
haveKubeCreds: true,
tunnel: tun,
configureForwarder: configureStaleLockWatcher,
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
wantCtx: &authContext{
kubeUsers: set.New("user-a"),
kubeGroups: set.New("kube-group-a", "kube-group-b", teleport.KubeSystemAuthenticated),
kubeClusterName: "local",
kubeClusterLabels: make(map[string]string),
certExpires: certExpiration,
teleportCluster: teleportClusterClient{
name: "local",
remoteAddr: *utils.MustParseAddr(remoteAddr),
},
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
},
},
{
desc: "scoped user with stale locks and strict locking produces error",
user: authz.LocalUser{},
scoped: true,
roleKubeGroups: []string{"kube-group-a", "kube-group-b"},
scopedKubeLockMode: constants.LockingModeStrict,
routeToCluster: "local",
kubernetesCluster: "local",
haveKubeCreds: true,
tunnel: tun,
configureForwarder: configureStaleLockWatcher,
wantAuthorizeErrContains: services.StrictLockingModeAccessDenied.Error(),
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
},
{
desc: "scoped user inherits disconnect on cert expiry from auth pref",
user: authz.LocalUser{},
scoped: true,
roleKubeGroups: []string{"kube-group-a", "kube-group-b"},
routeToCluster: "local",
kubernetesCluster: "local",
haveKubeCreds: true,
tunnel: tun,
wantDisconnectExpiredCert: &certExpiration,
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
wantCtx: &authContext{
kubeUsers: set.New("user-a"),
kubeGroups: set.New("kube-group-a", "kube-group-b", teleport.KubeSystemAuthenticated),
kubeClusterName: "local",
kubeClusterLabels: make(map[string]string),
certExpires: certExpiration,
teleportCluster: teleportClusterClient{
name: "local",
remoteAddr: *utils.MustParseAddr(remoteAddr),
},
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
},
},
{
desc: "scoped user disables on cert expiry",
user: authz.LocalUser{},
scoped: true,
roleKubeGroups: []string{"kube-group-a", "kube-group-b"},
routeToCluster: "local",
kubernetesCluster: "local",
scopedKubeDisconnectExpiredCert: ptr(false),
wantDisconnectExpiredCert: &time.Time{},
haveKubeCreds: true,
tunnel: tun,
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
wantCtx: &authContext{
kubeUsers: set.New("user-a"),
kubeGroups: set.New("kube-group-a", "kube-group-b", teleport.KubeSystemAuthenticated),
kubeClusterName: "local",
kubeClusterLabels: make(map[string]string),
certExpires: certExpiration,
teleportCluster: teleportClusterClient{
name: "local",
remoteAddr: *utils.MustParseAddr(remoteAddr),
},
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
},
},
{
desc: "scoped user sets disconnectExpiredCert to true",
user: authz.LocalUser{},
scoped: true,
roleKubeGroups: []string{"kube-group-a", "kube-group-b"},
scopedKubeDisconnectExpiredCert: ptr(true),
wantDisconnectExpiredCert: &certExpiration,
routeToCluster: "local",
kubernetesCluster: "local",
haveKubeCreds: true,
tunnel: tun,
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
wantCtx: &authContext{
kubeUsers: set.New("user-a"),
kubeGroups: set.New("kube-group-a", "kube-group-b", teleport.KubeSystemAuthenticated),
kubeClusterName: "local",
kubeClusterLabels: make(map[string]string),
certExpires: certExpiration,
teleportCluster: teleportClusterClient{
name: "local",
remoteAddr: *utils.MustParseAddr(remoteAddr),
},
kubeServers: newKubeServersFromKubeClusters(
t,
&types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: "local",
Labels: map[string]string{},
},
Scope: scopedTestScope,
Spec: types.KubernetesClusterSpecV3{
DynamicLabels: map[string]types.CommandLabelV2{},
},
},
),
},
},
{
desc: "local user and cluster, no kubeconfig",
user: authz.LocalUser{},
@@ -707,6 +964,10 @@ func TestAuthenticate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
f := newForwarder()
if tt.configureForwarder != nil {
tt.configureForwarder(t, f)
}
f.cfg.ReverseTunnelSrv = tt.tunnel
ap.kubeServers = tt.kubeServers
roles, err := services.RoleSetFromSpec("ops", types.RoleSpecV6{
@@ -723,13 +984,36 @@ func TestAuthenticate(t *testing.T) {
Roles: roles.RoleNames(),
}, "local", roles),
Identity: authz.WrapIdentity(tlsca.Identity{
Username: username,
Groups: roles.RoleNames(),
RouteToCluster: tt.routeToCluster,
KubernetesCluster: tt.kubernetesCluster,
ActiveRequests: tt.activeRequests,
Expires: certExpiration,
}),
UnmappedIdentity: authz.WrapIdentity(tlsca.Identity{
Username: username,
Groups: roles.RoleNames(),
RouteToCluster: tt.routeToCluster,
KubernetesCluster: tt.kubernetesCluster,
ActiveRequests: tt.activeRequests,
Expires: certExpiration,
}),
}
authorizer := mockAuthorizer{scopedCtx: authz.ScopedContextFromUnscopedContext(&authCtx)}
var authorizer mockAuthorizer
if tt.scoped {
authorizer = newScopedKubeAuthorizer(t, scopedKubeAuthorizerConfig{
user: user,
identity: authCtx.Identity,
kubeUsers: tt.roleKubeUsers,
kubeGroups: tt.roleKubeGroups,
kubeLockMode: tt.scopedKubeLockMode,
kubeDisconnectExpired: tt.scopedKubeDisconnectExpiredCert,
clusterName: "local",
})
} else {
authorizer = mockAuthorizer{scopedCtx: authz.ScopedContextFromUnscopedContext(&authCtx)}
}
if tt.authzErr {
authorizer.err = trace.AccessDenied("denied!")
}
@@ -767,13 +1051,22 @@ func TestAuthenticate(t *testing.T) {
}
require.NoError(t, err)
err = f.authorize(context.Background(), gotCtx)
if tt.wantAuthorizeErrContains != "" {
require.ErrorContains(t, err, tt.wantAuthorizeErrContains)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(gotCtx, tt.wantCtx,
cmp.AllowUnexported(authContext{}, teleportClusterClient{}, metaResource{}, apiResource{}),
cmpopts.IgnoreFields(authContext{}, "clientIdleTimeout", "sessionTTL", "ScopedContext", "recordingConfig", "disconnectExpiredCert", "kubeCluster", "checker", "accessState"),
cmpopts.IgnoreFields(authContext{}, "clientIdleTimeout", "sessionTTL", "ScopedContext", "recordingConfig", "disconnectExpiredCert", "kubeCluster", "checker", "accessState", "LockingMode"),
))
if tt.wantDisconnectExpiredCert != nil {
require.Equal(t, *tt.wantDisconnectExpiredCert, gotCtx.disconnectExpiredCert)
}
// validate authCtx.key() to make sure it includes certExpires timestamp.
// this is important to make sure user's credentials are correctly cached
// and once user's re-login, Teleport won't reuse their previous cache entry.
@@ -1313,6 +1606,89 @@ func (a mockAuthorizer) AuthorizeScoped(context.Context) (*authz.ScopedContext,
return a.scopedCtx, a.err
}
const scopedTestRole = "scoped-kube-admin"
const scopedTestScope = "/staging"
type scopedKubeAuthorizerConfig struct {
user types.User
identity authz.IdentityGetter
kubeUsers []string
kubeGroups []string
kubeLockMode constants.LockingMode
kubeDisconnectExpired *bool
clusterName string
}
func newScopedKubeAuthorizer(t *testing.T, cfg scopedKubeAuthorizerConfig) mockAuthorizer {
t.Helper()
var lock *scopedaccessv1.Lock
if cfg.kubeLockMode != "" {
lock = &scopedaccessv1.Lock{Mode: string(cfg.kubeLockMode)}
}
role := &scopedaccessv1.ScopedRole{
Kind: "scoped_role",
Metadata: &headerv1.Metadata{
Name: scopedTestRole,
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{scopedTestScope},
Kube: &scopedaccessv1.ScopedRoleKube{
Labels: []*labelv1.Label{
{Name: types.Wildcard, Values: []string{types.Wildcard}},
},
Groups: cfg.kubeGroups,
Users: cfg.kubeUsers,
Lock: lock,
DisconnectExpiredCert: cfg.kubeDisconnectExpired,
},
},
Version: types.V1,
}
reader := &fakeScopedRoleReader{
role: role,
}
pin := &scopesv1.Pin{Scope: scopedTestScope}
pin.AssignmentTree = pinning.AssignmentTreeFromMap(map[string]map[string][]string{
"/": {
scopedTestScope: {scopedTestRole},
},
})
checkerCtx, err := services.NewScopedAccessCheckerContext(t.Context(), &services.AccessInfo{
Username: cfg.user.GetName(),
ScopePin: pin,
}, cfg.clusterName, reader)
require.NoError(t, err)
return mockAuthorizer{
scopedCtx: &authz.ScopedContext{
User: cfg.user,
Identity: cfg.identity,
CheckerContext: checkerCtx,
},
}
}
func ptr[T any](v T) *T { return &v }
type fakeScopedRoleReader struct {
role *scopedaccessv1.ScopedRole
}
func (r *fakeScopedRoleReader) GetScopedRole(_ context.Context, req *scopedaccessv1.GetScopedRoleRequest) (*scopedaccessv1.GetScopedRoleResponse, error) {
return &scopedaccessv1.GetScopedRoleResponse{Role: r.role}, nil
}
func (r *fakeScopedRoleReader) ListScopedRoles(context.Context, *scopedaccessv1.ListScopedRolesRequest) (*scopedaccessv1.ListScopedRolesResponse, error) {
return &scopedaccessv1.ListScopedRolesResponse{Roles: []*scopedaccessv1.ScopedRole{r.role}}, nil
}
type mockEventClient struct {
services.Presence
types.Events
+34
View File
@@ -274,6 +274,27 @@ func StrongValidateRole(role *scopedaccessv1.ScopedRole) error {
}
}
// verify that the lock.Mode is a recognized value for Defaults
if lock := role.GetSpec().GetDefaults().GetLock(); lock != nil {
if err := validateLock(lock); err != nil {
return trace.BadParameter("scoped role %q has invalid defaults.lock.mode %q", role.GetMetadata().GetName(), lock.GetMode())
}
}
// verify that lock.Mode is a recognized value for SSH
if lock := role.GetSpec().GetSsh().GetLock(); lock != nil {
if err := validateLock(lock); err != nil {
return trace.BadParameter("scoped role %q has invalid ssh.lock.mode %q", role.GetMetadata().GetName(), lock.GetMode())
}
}
// verify that lock.Mode is a recognized value for Kube
if lock := role.GetSpec().GetKube().GetLock(); lock != nil {
if err := validateLock(lock); err != nil {
return trace.BadParameter("scoped role %q has invalid kube.lock.mode %q", role.GetMetadata().GetName(), lock.GetMode())
}
}
// verify that kube labels are well-formed
for _, label := range role.GetSpec().GetKube().GetLabels() {
// we currently don't support any form of wildcard/regex/substitution in scoped role
@@ -324,6 +345,19 @@ func validateDoesNotContain(values []string, invalidSet string) string {
return ""
}
func validateLock(lock *scopedaccessv1.Lock) error {
mode := lock.GetMode()
switch constants.LockingMode(mode) {
// Allow for empty string - we will fall back to the cluster defaults - or best_effort if not set.
// This matches current behavior for unscoped lock mode checks.
case "":
case constants.LockingModeBestEffort, constants.LockingModeStrict:
default:
return trace.Errorf("invalid lock mode")
}
return nil
}
func validateRoleName(name string) error {
// note: having the scope name be validated as a segment name is a bit of an arbitrary choice, but its basically
// equivalent to what we would want from a standalone name requirement, and there may even be some future benefit
+215 -1
View File
@@ -409,6 +409,24 @@ func TestValidateRole(t *testing.T) {
},
},
},
},
},
{
name: "invalid defaults.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Defaults: &scopedaccessv1.ScopedRoleDefaults{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
Version: types.V1,
},
strongOk: false,
@@ -447,6 +465,24 @@ func TestValidateRole(t *testing.T) {
},
},
},
},
},
{
name: "invalid ssh.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
Version: types.V1,
},
strongOk: false,
@@ -466,6 +502,172 @@ func TestValidateRole(t *testing.T) {
},
},
},
},
},
{
name: "valid ssh.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
},
Version: types.V1,
},
strongOk: true,
weakOk: true,
},
{
name: "empty ssh.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: "",
},
},
},
Version: types.V1,
},
strongOk: true,
weakOk: true,
},
{
name: "invalid kube.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
Version: types.V1,
},
strongOk: false,
weakOk: true,
},
{
name: "valid Kube.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
},
Version: types.V1,
},
strongOk: true,
weakOk: true,
},
{
name: "empty Kube.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: "",
},
},
},
Version: types.V1,
},
strongOk: true,
weakOk: true,
},
{
name: "invalid defaults.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Defaults: &scopedaccessv1.ScopedRoleDefaults{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
Version: types.V1,
},
strongOk: false,
weakOk: true,
},
{
name: "valid defaults.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Defaults: &scopedaccessv1.ScopedRoleDefaults{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
},
Version: types.V1,
},
strongOk: true,
weakOk: true,
},
{
name: "empty defaults.lock.mode",
role: &scopedaccessv1.ScopedRole{
Kind: KindScopedRole,
Metadata: &headerv1.Metadata{
Name: "test",
},
Scope: "/",
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo"},
Defaults: &scopedaccessv1.ScopedRoleDefaults{
Lock: &scopedaccessv1.Lock{
Mode: "",
},
},
},
Version: types.V1,
},
strongOk: true,
@@ -1296,6 +1498,10 @@ func TestStrongValidateRoleSpecAllFieldsValidated(t *testing.T) {
SessionRecording: &scopedaccessv1.SessionRecording{
Mode: string(constants.SessionRecordingModeStrict),
},
Lock: &scopedaccessv1.Lock{
Mode: "strict",
},
DisconnectExpiredCert: ptr(true),
},
Rules: []*scopedaccessv1.ScopedRule{
{
@@ -1331,6 +1537,10 @@ func TestStrongValidateRoleSpecAllFieldsValidated(t *testing.T) {
SessionRecording: &scopedaccessv1.SessionRecording{
Mode: string(constants.SessionRecordingModeStrict),
},
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeBestEffort),
},
DisconnectExpiredCert: proto.Bool(true),
},
Kube: &scopedaccessv1.ScopedRoleKube{
Groups: []string{"viewer"},
@@ -1338,7 +1548,11 @@ func TestStrongValidateRoleSpecAllFieldsValidated(t *testing.T) {
Labels: []*labelv1.Label{
{Name: "env", Values: []string{"prod"}},
},
ClientIdleTimeout: "1h",
ClientIdleTimeout: "1h",
DisconnectExpiredCert: ptr(true),
Lock: &scopedaccessv1.Lock{
Mode: "strict",
},
},
}
+53 -1
View File
@@ -231,6 +231,7 @@ func baseScopedRole() *scopedaccessv1.ScopedRole {
Spec: &scopedaccessv1.ScopedRoleSpec{
AssignableScopes: []string{"/foo/bar"},
Ssh: &scopedaccessv1.ScopedRoleSSH{},
Kube: &scopedaccessv1.ScopedRoleKube{},
},
Version: types.V1,
}
@@ -335,13 +336,36 @@ func TestEnhancedRecordingNotInClassicRole(t *testing.T) {
Network: ptr(true),
Disk: ptr(true),
}
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
// BPF is the classic role equivalent of enhanced_recording.
require.Equal(t, apidefaults.EnhancedEvents(), role.GetOptions().BPF)
}
func TestDisconnectExpiredCertNotInClassicRole(t *testing.T) {
t.Parallel()
sr := baseScopedRole()
sr.Spec.Ssh.DisconnectExpiredCert = ptr(true)
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
require.Equal(t, types.NewBool(false), role.GetOptions().DisconnectExpiredCert)
}
func TestKubeDisconnectExpiredCertNotInClassicRole(t *testing.T) {
t.Parallel()
sr := baseScopedRole()
sr.Spec.Kube = &scopedaccessv1.ScopedRoleKube{
DisconnectExpiredCert: ptr(true),
}
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
require.Equal(t, types.NewBool(false), role.GetOptions().DisconnectExpiredCert)
}
func TestSessionRecordingNotInClassicRole(t *testing.T) {
t.Parallel()
@@ -352,6 +376,7 @@ func TestSessionRecordingNotInClassicRole(t *testing.T) {
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
// RecordSession is the classic role equivalent of session_recording_mode.
require.Equal(t, &types.RecordSession{
Desktop: types.NewBoolOption(true),
@@ -359,6 +384,33 @@ func TestSessionRecordingNotInClassicRole(t *testing.T) {
}, role.GetOptions().RecordSession)
}
func TestSSHLockingModeNotInClassicRole(t *testing.T) {
t.Parallel()
sr := baseScopedRole()
sr.Spec.Ssh.Lock = &scopedaccessv1.Lock{
Mode: "strict",
}
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
require.Empty(t, string(role.GetOptions().Lock))
}
func TestKubeLockingModeNotInClassicRole(t *testing.T) {
t.Parallel()
sr := baseScopedRole()
sr.Spec.Kube.Lock = &scopedaccessv1.Lock{
Mode: "strict",
}
role, err := ScopedRoleToRole(sr, "/foo/bar")
require.NoError(t, err)
require.Empty(t, string(role.GetOptions().Lock))
}
// TestKubeConversion verifies the various kube-related scoped role conversion scenarios.
func TestKubeConversion(t *testing.T) {
t.Parallel()
+75
View File
@@ -0,0 +1,75 @@
/*
* Teleport
* Copyright (C) 2026 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 (
"time"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/constants"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
)
// adjust the client idle timeout value - preferring the default if not set
func (c *ScopedAccessChecker) adjustScopedClientIdleTimeout(idleStr string, timeout time.Duration) (time.Duration, error) {
if idleStr == "" {
idleStr = c.role.GetSpec().GetDefaults().GetClientIdleTimeout()
}
if idleStr != "" {
d, err := time.ParseDuration(idleStr)
if err != nil {
return 0, trace.Errorf("invalid client_idle_timeout %q in scoped role %q: %w", idleStr, c.role.GetMetadata().GetName(), err)
}
if d > 0 && (timeout == 0 || d < timeout) {
return max(d, 0), nil
}
}
return max(timeout, 0), nil
}
// adjustScopedDisconnectExpiredCert returns the disconnect on Expired Cert condition - - applying the default if applicable.
func (c *ScopedAccessChecker) adjustScopedDisconnectExpiredCert(roleSpecifiedDisconnect *bool, defaultdisconnect bool) bool {
if roleSpecifiedDisconnect == nil && c.role.GetSpec().GetDefaults() != nil {
roleSpecifiedDisconnect = c.role.GetSpec().GetDefaults().DisconnectExpiredCert
}
if roleSpecifiedDisconnect != nil {
return *roleSpecifiedDisconnect
}
return defaultdisconnect
}
// LockingMode returns the lock enforcement mode to apply - applying the default if applicable.
func (c *ScopedAccessChecker) scopedLockingMode(lock *accessv1.Lock, defaultMode constants.LockingMode) constants.LockingMode {
if lock == nil {
lock = c.role.GetSpec().GetDefaults().GetLock()
}
// both protocol specific lock and the default locks are nil, so return the defaultMode.
if lock == nil {
return defaultMode
}
mode := constants.LockingMode(lock.GetMode())
switch mode {
case constants.LockingModeStrict, constants.LockingModeBestEffort:
return mode
default:
return defaultMode
}
}
+22 -15
View File
@@ -21,8 +21,7 @@ package services
import (
"time"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
)
@@ -78,19 +77,27 @@ func (c *KubeAccessChecker) AdjustClientIdleTimeout(timeout time.Duration) (time
if !c.checker.isScoped() {
return c.checker.unscopedChecker.AdjustClientIdleTimeout(timeout), nil
}
// Kube block takes precedence over defaults block.
idleStr := c.checker.role.GetSpec().GetKube().GetClientIdleTimeout()
if idleStr == "" {
idleStr = c.checker.role.GetSpec().GetDefaults().GetClientIdleTimeout()
return c.checker.adjustScopedClientIdleTimeout(c.checker.role.GetSpec().GetKube().GetClientIdleTimeout(), timeout)
}
// AdjustDisconnectExpiredCert adjusts whether to disconnect on certificate expiry.
func (c *KubeAccessChecker) AdjustDisconnectExpiredCert(disconnect bool) bool {
if !c.checker.isScoped() {
return c.checker.unscopedChecker.AdjustDisconnectExpiredCert(disconnect)
}
if idleStr != "" {
d, err := time.ParseDuration(idleStr)
if err != nil {
return 0, trace.Errorf("invalid client_idle_timeout %q in scoped role %q: %w", idleStr, c.checker.role.GetMetadata().GetName(), err)
}
if d > 0 && (timeout == 0 || d < timeout) {
return max(d, 0), nil
}
kube := c.checker.role.GetSpec().GetKube()
var disconnectExpiredCert *bool
if kube != nil {
disconnectExpiredCert = kube.DisconnectExpiredCert
}
return max(timeout, 0), nil
return c.checker.adjustScopedDisconnectExpiredCert(disconnectExpiredCert, disconnect)
}
// LockingMode returns the SSH lock enforcement mode to apply.
func (c *KubeAccessChecker) LockingMode(defaultMode constants.LockingMode) constants.LockingMode {
if !c.checker.isScoped() {
return c.checker.unscopedChecker.LockingMode(defaultMode)
}
return c.checker.scopedLockingMode(c.checker.role.GetSpec().GetKube().GetLock(), defaultMode)
}
+171
View File
@@ -0,0 +1,171 @@
/*
* Teleport
* Copyright (C) 2026 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 (
"testing"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/constants"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
)
func TestKubeAccessCheckerAdjustDisconnectExpiredCert(t *testing.T) {
tts := []struct {
name string
spec *scopedaccessv1.ScopedRoleSpec
defaultVal bool
expect bool
}{
{
name: "unset defers to default false",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{},
},
defaultVal: false,
expect: false,
},
{
name: "unset kube and default defers to default true",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{},
},
defaultVal: true,
expect: true,
},
{
name: "explicit true overrides default false",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{
DisconnectExpiredCert: ptr(true),
},
},
defaultVal: false,
expect: true,
},
{
name: "explicit false overrides default true",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{
DisconnectExpiredCert: ptr(false),
},
},
defaultVal: true,
expect: false,
},
{
name: "default block is applied when kube block is empty",
spec: &scopedaccessv1.ScopedRoleSpec{
Defaults: &scopedaccessv1.ScopedRoleDefaults{
DisconnectExpiredCert: ptr(false),
},
Kube: &scopedaccessv1.ScopedRoleKube{},
},
defaultVal: true,
expect: false,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
checker := newScopedCheckerWithRole(tt.spec).Kube()
require.Equal(t, tt.expect, checker.AdjustDisconnectExpiredCert(tt.defaultVal))
})
}
}
func TestKubeAccessCheckerLockingMode(t *testing.T) {
tts := []struct {
name string
spec *scopedaccessv1.ScopedRoleSpec
defaultMode constants.LockingMode
expect constants.LockingMode
}{
{
name: "unset kube and default blocks defers to default mode",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{},
},
defaultMode: constants.LockingModeBestEffort,
expect: constants.LockingModeBestEffort,
},
{
name: "strict from role",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
},
defaultMode: constants.LockingModeBestEffort,
expect: constants.LockingModeStrict,
},
{
name: "best effort from role",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeBestEffort),
},
},
},
defaultMode: constants.LockingModeStrict,
expect: constants.LockingModeBestEffort,
},
{
name: "invalid value falls back to default",
spec: &scopedaccessv1.ScopedRoleSpec{
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
defaultMode: constants.LockingModeStrict,
expect: constants.LockingModeStrict,
},
{
name: "empty mode falls back to default block",
spec: &scopedaccessv1.ScopedRoleSpec{
Defaults: &scopedaccessv1.ScopedRoleDefaults{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
Kube: &scopedaccessv1.ScopedRoleKube{
Lock: nil,
},
},
defaultMode: constants.LockingModeBestEffort,
expect: constants.LockingModeStrict,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
checker := newScopedCheckerWithRole(tt.spec).Kube()
require.Equal(t, tt.expect, checker.LockingMode(tt.defaultMode))
})
}
}
+17 -16
View File
@@ -62,21 +62,8 @@ func (c *SSHAccessChecker) AdjustClientIdleTimeout(timeout time.Duration) (time.
if !c.checker.isScoped() {
return c.checker.unscopedChecker.AdjustClientIdleTimeout(timeout), nil
}
// SSH block takes precedence over defaults block.
idleStr := c.checker.role.GetSpec().GetSsh().GetClientIdleTimeout()
if idleStr == "" {
idleStr = c.checker.role.GetSpec().GetDefaults().GetClientIdleTimeout()
}
if idleStr != "" {
d, err := time.ParseDuration(idleStr)
if err != nil {
return 0, trace.Errorf("invalid client_idle_timeout %q in scoped role %q: %w", idleStr, c.checker.role.GetMetadata().GetName(), err)
}
if d > 0 && (timeout == 0 || d < timeout) {
return max(d, 0), nil
}
}
return max(timeout, 0), nil
return c.checker.adjustScopedClientIdleTimeout(c.checker.role.GetSpec().GetSsh().GetClientIdleTimeout(), timeout)
}
// AdjustDisconnectExpiredCert adjusts whether to disconnect on certificate expiry.
@@ -84,7 +71,21 @@ func (c *SSHAccessChecker) AdjustDisconnectExpiredCert(disconnect bool) bool {
if !c.checker.isScoped() {
return c.checker.unscopedChecker.AdjustDisconnectExpiredCert(disconnect)
}
return c.checker.scopedCompatChecker.AdjustDisconnectExpiredCert(disconnect)
ssh := c.checker.role.GetSpec().GetSsh()
var disconnectExpiredCert *bool
if ssh != nil {
disconnectExpiredCert = ssh.DisconnectExpiredCert
}
return c.checker.adjustScopedDisconnectExpiredCert(disconnectExpiredCert, disconnect)
}
// LockingMode returns the SSH lock enforcement mode to apply.
func (c *SSHAccessChecker) LockingMode(defaultMode constants.LockingMode) constants.LockingMode {
if !c.checker.isScoped() {
return c.checker.unscopedChecker.LockingMode(defaultMode)
}
return c.checker.scopedLockingMode(c.checker.role.GetSpec().GetSsh().GetLock(), defaultMode)
}
// SessionRecordingMode returns the session recording mode for SSH sessions.
+143
View File
@@ -194,6 +194,149 @@ func TestSSHAccessCheckerAdjustClientIdleTimeout(t *testing.T) {
func ptr[T any](v T) *T { return &v }
func TestSSHAccessCheckerAdjustDisconnectExpiredCert(t *testing.T) {
t.Parallel()
tts := []struct {
name string
spec *scopedaccessv1.ScopedRoleSpec
defaultVal bool
expect bool
}{
{
name: "unset defers to default false",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{},
},
defaultVal: false,
expect: false,
},
{
name: "unset defers to default true",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{},
},
defaultVal: true,
expect: true,
},
{
name: "explicit true overrides default false",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
DisconnectExpiredCert: ptr(true),
},
},
defaultVal: false,
expect: true,
},
{
name: "explicit false overrides default true",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
DisconnectExpiredCert: ptr(false),
},
},
defaultVal: true,
expect: false,
},
{
name: "unset ssh block defaults to default block",
spec: &scopedaccessv1.ScopedRoleSpec{
Defaults: &scopedaccessv1.ScopedRoleDefaults{
DisconnectExpiredCert: ptr(false),
},
Ssh: &scopedaccessv1.ScopedRoleSSH{},
},
defaultVal: true,
expect: false,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
checker := newScopedCheckerWithRole(tt.spec).SSH()
require.Equal(t, tt.expect, checker.AdjustDisconnectExpiredCert(tt.defaultVal))
})
}
}
func TestSSHAccessCheckerLockingMode(t *testing.T) {
t.Parallel()
tts := []struct {
name string
spec *scopedaccessv1.ScopedRoleSpec
defaultMode constants.LockingMode
expect constants.LockingMode
}{
{
name: "unset defers to default",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{},
},
defaultMode: constants.LockingModeBestEffort,
expect: constants.LockingModeBestEffort,
},
{
name: "strict from role",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeStrict),
},
},
},
defaultMode: constants.LockingModeBestEffort,
expect: constants.LockingModeStrict,
},
{
name: "best effort from role",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: string(constants.LockingModeBestEffort),
},
},
},
defaultMode: constants.LockingModeStrict,
expect: constants.LockingModeBestEffort,
},
{
name: "invalid value falls back to default",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{
Mode: "invalid",
},
},
},
defaultMode: constants.LockingModeStrict,
expect: constants.LockingModeStrict,
},
{
name: "empty mode falls back to default",
spec: &scopedaccessv1.ScopedRoleSpec{
Ssh: &scopedaccessv1.ScopedRoleSSH{
Lock: &scopedaccessv1.Lock{},
},
},
defaultMode: constants.LockingModeStrict,
expect: constants.LockingModeStrict,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
checker := newScopedCheckerWithRole(tt.spec).SSH()
require.Equal(t, tt.expect, checker.LockingMode(tt.defaultMode))
})
}
}
func TestSSHAccessCheckerPermitX11Forwarding(t *testing.T) {
t.Parallel()
+1 -1
View File
@@ -1182,7 +1182,7 @@ func (a *ahLoginChecker) evaluateScopedSSHAccess(ident *sshca.Identity, ca types
ClientIdleTimeout: durationpb.New(clientIdleTimeout),
DisconnectExpiredCert: timestampFromGoTime(getDisconnectExpiredCertFromSSHIdentityScoped(checker.SSH(), authPref, ident)),
SessionRecordingMode: string(checker.SSH().SessionRecordingMode()),
LockingMode: string(checker.LockingMode(authPref.GetLockingMode())),
LockingMode: string(checker.SSH().LockingMode(authPref.GetLockingMode())),
PrivateKeyPolicy: string(privateKeyPolicy),
LockTargets: decision.LockTargetsToProto(lockTargets),
MappedRoles: accessInfo.Roles,