Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2380,6 +2380,13 @@ message RoleOptions {
// IDP is a set of options related to accessing IdPs within Teleport.
// Requires Teleport Enterprise.
IdPOptions IDP = 25 [(gogoproto.jsontag) = "idp,omitempty"];

// CreateDesktopUser allows users to be automatically created on a Windows desktop
BoolValue CreateDesktopUser = 26 [
(gogoproto.nullable) = true,
(gogoproto.jsontag) = "create_desktop_user",
(gogoproto.customtype) = "BoolOption"
];
}

message RecordSession {
Expand Down Expand Up @@ -2555,6 +2562,9 @@ message RoleConditions {
(gogoproto.jsontag) = "group_labels,omitempty",
(gogoproto.customtype) = "Labels"
];

// DesktopGroups is a list of groups for created desktop users to be added to
repeated string DesktopGroups = 28 [(gogoproto.jsontag) = "desktop_groups,omitempty"];
}

// KubernetesResource is the Kubernetes resource identifier.
Expand Down
30 changes: 28 additions & 2 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ type Role interface {
// SetHostGroups sets the list of groups this role is put in when users are provisioned
SetHostGroups(RoleConditionType, []string)

// GetDesktopGroups gets the list of groups this role is put in when desktop users are provisioned
GetDesktopGroups(RoleConditionType) []string
// SetDesktopGroups sets the list of groups this role is put in when desktop users are provisioned
SetDesktopGroups(RoleConditionType, []string)

// GetHostSudoers gets the list of sudoers entries for the role
GetHostSudoers(RoleConditionType) []string
// SetHostSudoers sets the list of sudoers entries for the role
Expand All @@ -215,7 +220,7 @@ type Role interface {

// GetDatabaseServiceLabels gets the map of db service labels this role is allowed or denied access to.
GetDatabaseServiceLabels(RoleConditionType) Labels
// SetDatabaseLabels sets the map of db service labels this role is allowed or denied access to.
// SetDatabaseServiceLabels sets the map of db service labels this role is allowed or denied access to.
SetDatabaseServiceLabels(RoleConditionType, Labels)

// GetGroupLabels gets the map of group labels this role is allowed or denied access to.
Expand Down Expand Up @@ -719,7 +724,7 @@ func (r *RoleV6) SetRules(rct RoleConditionType, in []Rule) {
}
}

// GetGroups gets all groups for provisioned user
// GetHostGroups gets all groups for provisioned user
func (r *RoleV6) GetHostGroups(rct RoleConditionType) []string {
if rct == Allow {
return r.Spec.Allow.HostGroups
Expand All @@ -737,6 +742,24 @@ func (r *RoleV6) SetHostGroups(rct RoleConditionType, groups []string) {
}
}

// GetDesktopGroups gets all groups for provisioned user
func (r *RoleV6) GetDesktopGroups(rct RoleConditionType) []string {
if rct == Allow {
return r.Spec.Allow.DesktopGroups
}
return r.Spec.Deny.DesktopGroups
}

// SetDesktopGroups sets all groups for provisioned user
func (r *RoleV6) SetDesktopGroups(rct RoleConditionType, groups []string) {
ncopy := utils.CopyStrings(groups)
if rct == Allow {
r.Spec.Allow.DesktopGroups = ncopy
} else {
r.Spec.Deny.DesktopGroups = ncopy
}
}

// GetHostSudoers gets the list of sudoers entries for the role
func (r *RoleV6) GetHostSudoers(rct RoleConditionType) []string {
if rct == Allow {
Expand Down Expand Up @@ -830,6 +853,9 @@ func (r *RoleV6) CheckAndSetDefaults() error {
if r.Spec.Options.CreateHostUser == nil {
r.Spec.Options.CreateHostUser = NewBoolOption(false)
}
if r.Spec.Options.CreateDesktopUser == nil {
r.Spec.Options.CreateDesktopUser = NewBoolOption(false)
}
if r.Spec.Options.SSHFileCopy == nil {
r.Spec.Options.SSHFileCopy = NewBoolOption(true)
}
Expand Down
2,906 changes: 1,508 additions & 1,398 deletions api/types/types.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e
Submodule e updated from a4f12c to 30a1da
28 changes: 28 additions & 0 deletions lib/auth/windows/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"encoding/pem"
"fmt"
"time"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
)

const (
Expand All @@ -46,6 +48,20 @@ type certRequest struct {
keyDER []byte
}

func createUsersExtension(groups []string) (pkix.Extension, error) {
value, err := json.Marshal(struct {
CreateUser bool `json:"createUser"`
Groups []string `json:"groups"`
}{true, groups})
if err != nil {
return pkix.Extension{}, trace.Wrap(err)
}
return pkix.Extension{
Id: tlsca.CreateWindowsUserOID,
Value: value,
}, nil
}

func getCertRequest(req *GenerateCredentialsRequest) (*certRequest, error) {
// Important: rdpclient currently only supports 2048-bit RSA keys.
// If you switch the key type here, update handle_general_authentication in
Expand Down Expand Up @@ -78,6 +94,14 @@ func getCertRequest(req *GenerateCredentialsRequest) (*certRequest, error) {
},
}

if req.CreateUser {
createUser, err := createUsersExtension(req.Groups)
if err != nil {
return nil, trace.Wrap(err)
}
csr.ExtraExtensions = append(csr.ExtraExtensions, createUser)
}

if req.ActiveDirectorySID != "" {
adUserMapping, err := asn1.Marshal(SubjectAltName[adSid]{
otherName[adSid]{
Expand Down Expand Up @@ -145,6 +169,10 @@ type GenerateCredentialsRequest struct {
// CAType is the certificate authority type used to generate the certificate.
// This is used to proper generate the CRL LDAP path.
CAType types.CertAuthType
// CreateUser specifies if Windows user should be created if missing
CreateUser bool
// Groups are groups that user should be member of
Groups []string
}

// GenerateWindowsDesktopCredentials generates a private key / certificate pair for the given
Expand Down
3 changes: 3 additions & 0 deletions lib/services/access_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ type AccessChecker interface {
// a role disallows host user creation
HostUsers(types.Server) (*HostUsersInfo, error)

// DesktopGroups returns the desktop groups a user is allowed to create or an access denied error if a role disallows desktop user creation
DesktopGroups(types.WindowsDesktop) ([]string, error)

// PinSourceIP forces the same client IP for certificate generation and SSH usage
PinSourceIP() bool

Expand Down
42 changes: 42 additions & 0 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ func ApplyTraits(r types.Role, traits map[string][]string) types.Role {
r.SetHostSudoers(condition,
applyValueTraitsSlice(r.GetHostSudoers(condition), traits, "host_sudoers"))

r.SetDesktopGroups(condition,
applyValueTraitsSlice(r.GetDesktopGroups(condition), traits, "desktop_groups"))

options := r.GetOptions()
for i, ext := range options.CertExtensions {
vals, err := ApplyValueTraits(ext.Value, traits)
Expand Down Expand Up @@ -2713,6 +2716,45 @@ func (set RoleSet) EnhancedRecordingSet() map[string]bool {
return m
}

// DesktopGroups returns the desktop groups a user is allowed to create or an access denied error if a role disallows desktop user creation
func (set RoleSet) DesktopGroups(s types.WindowsDesktop) ([]string, error) {
groups := make(map[string]struct{})
labels := s.GetAllLabels()
for _, role := range set {
result, _, err := MatchLabels(role.GetWindowsDesktopLabels(types.Allow), labels)
if err != nil {
return nil, trace.Wrap(err)
}
// skip nodes that dont have matching labels
if !result {
continue
}
createDesktopUser := role.GetOptions().CreateDesktopUser
// if any of the matching roles do not enable create host
// user, the user should not be allowed on
if createDesktopUser == nil || !createDesktopUser.Value {
return nil, trace.AccessDenied("user is not allowed to create host users")
}
for _, group := range role.GetDesktopGroups(types.Allow) {
groups[group] = struct{}{}
}
}
for _, role := range set {
result, _, err := MatchLabels(role.GetWindowsDesktopLabels(types.Deny), labels)
if err != nil {
return nil, trace.Wrap(err)
}
if !result {
continue
}
for _, group := range role.GetDesktopGroups(types.Deny) {
delete(groups, group)
}
}

return utils.StringsSliceFromSet(groups), nil
}

// HostUsers returns host user information matching a server or nil if
// a role disallows host user creation
func (set RoleSet) HostUsers(s types.Server) (*HostUsersInfo, error) {
Expand Down
5 changes: 5 additions & 0 deletions lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ func TestRoleParse(t *testing.T) {
BPF: apidefaults.EnhancedEvents(),
DesktopClipboard: types.NewBoolOption(true),
DesktopDirectorySharing: types.NewBoolOption(true),
CreateDesktopUser: types.NewBoolOption(false),
CreateHostUser: types.NewBoolOption(false),
SSHFileCopy: types.NewBoolOption(true),
IDP: &types.IdPOptions{
Expand Down Expand Up @@ -285,6 +286,7 @@ func TestRoleParse(t *testing.T) {
BPF: apidefaults.EnhancedEvents(),
DesktopClipboard: types.NewBoolOption(true),
DesktopDirectorySharing: types.NewBoolOption(true),
CreateDesktopUser: types.NewBoolOption(false),
CreateHostUser: types.NewBoolOption(false),
SSHFileCopy: types.NewBoolOption(true),
IDP: &types.IdPOptions{
Expand Down Expand Up @@ -376,6 +378,7 @@ func TestRoleParse(t *testing.T) {
BPF: apidefaults.EnhancedEvents(),
DesktopClipboard: types.NewBoolOption(true),
DesktopDirectorySharing: types.NewBoolOption(true),
CreateDesktopUser: types.NewBoolOption(false),
CreateHostUser: types.NewBoolOption(false),
SSHFileCopy: types.NewBoolOption(false),
IDP: &types.IdPOptions{
Expand Down Expand Up @@ -483,6 +486,7 @@ func TestRoleParse(t *testing.T) {
BPF: apidefaults.EnhancedEvents(),
DesktopClipboard: types.NewBoolOption(true),
DesktopDirectorySharing: types.NewBoolOption(true),
CreateDesktopUser: types.NewBoolOption(false),
CreateHostUser: types.NewBoolOption(false),
SSHFileCopy: types.NewBoolOption(true),
IDP: &types.IdPOptions{
Expand Down Expand Up @@ -576,6 +580,7 @@ func TestRoleParse(t *testing.T) {
BPF: apidefaults.EnhancedEvents(),
DesktopClipboard: types.NewBoolOption(true),
DesktopDirectorySharing: types.NewBoolOption(true),
CreateDesktopUser: types.NewBoolOption(false),
CreateHostUser: types.NewBoolOption(false),
SSHFileCopy: types.NewBoolOption(true),
IDP: &types.IdPOptions{
Expand Down
56 changes: 47 additions & 9 deletions lib/srv/desktop/windows_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,12 @@ func (s *WindowsService) tlsConfigForLDAP() (*tls.Config, error) {
using to sign in. This is set to become a strict requirement by May 2023,
please update your configuration file before then.`)
}
certDER, keyDER, err := s.generateCredentials(s.closeCtx, user, s.cfg.Domain, windowsDesktopServiceCertTTL, s.cfg.SID)
certDER, keyDER, err := s.generateCredentials(s.closeCtx, generateCredentialsRequest{
username: user,
domain: s.cfg.Domain,
ttl: windowsDesktopServiceCertTTL,
activeDirectorySID: s.cfg.SID,
})
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -857,10 +862,16 @@ func (s *WindowsService) connectRDP(ctx context.Context, log logrus.FieldLogger,
tdpConn.OnRecv = s.makeTDPReceiveHandler(ctx, sw, delay, &identity, string(sessionID), desktop.GetAddr(), tdpConn)

sessionStartTime := s.cfg.Clock.Now().UTC().Round(time.Millisecond)
groups, err := authCtx.Checker.DesktopGroups(desktop)
if err != nil && !trace.IsAccessDenied(err) {
s.onSessionStart(ctx, sw, &identity, sessionStartTime, windowsUser, string(sessionID), desktop, err)
return trace.Wrap(err)
}
createUsers := err == nil
rdpc, err := rdpclient.New(rdpclient.Config{
Log: log,
GenerateUserCert: func(ctx context.Context, username string, ttl time.Duration) (certDER, keyDER []byte, err error) {
return s.generateUserCert(ctx, username, ttl, desktop)
return s.generateUserCert(ctx, username, ttl, desktop, createUsers, groups)
},
CertTTL: windows.CertTTL,
Addr: desktop.GetAddr(),
Expand Down Expand Up @@ -1097,7 +1108,7 @@ func timer() func() int64 {

// generateUserCert generates a keypair for the given Windows username,
// optionally querying LDAP for the user's Security Identifier.
func (s *WindowsService) generateUserCert(ctx context.Context, username string, ttl time.Duration, desktop types.WindowsDesktop) (certDER, keyDER []byte, err error) {
func (s *WindowsService) generateUserCert(ctx context.Context, username string, ttl time.Duration, desktop types.WindowsDesktop, createUsers bool, groups []string) (certDER, keyDER []byte, err error) {
var activeDirectorySID string
if !desktop.NonAD() {
// Find the user's SID
Expand Down Expand Up @@ -1130,24 +1141,51 @@ func (s *WindowsService) generateUserCert(ctx context.Context, username string,
}
s.cfg.Log.Debugf("Found objectSid %v for Windows username %v", activeDirectorySID, username)
}
return s.generateCredentials(ctx, username, desktop.GetDomain(), ttl, activeDirectorySID)
return s.generateCredentials(ctx, generateCredentialsRequest{
username: username,
domain: desktop.GetDomain(),
ttl: ttl,
activeDirectorySID: activeDirectorySID,
createUser: createUsers,
groups: groups,
})
}

// generateCredentialsRequest are the request parameters for generating a windows cert/key pair
type generateCredentialsRequest struct {
// username is the Windows username
username string
// domain is the Windows domain
domain string
// ttl for the certificate
ttl time.Duration
// activeDirectorySID is the SID of the Windows user
// specified by Username. If specified (!= ""), it is
// encoded in the certificate per https://go.microsoft.com/fwlink/?linkid=2189925.
activeDirectorySID string
// createUser specifies if Windows user should be created if missing
createUser bool
// groups are groups that user should be member of
groups []string
}

// generateCredentials generates a private key / certificate pair for the given
// Windows username. The certificate has certain special fields different from
// the regular Teleport user certificate, to meet the requirements of Active
// Directory. See:
// https://docs.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-certificate-requirements-and-enumeration
func (s *WindowsService) generateCredentials(ctx context.Context, username, domain string, ttl time.Duration, activeDirectorySID string) (certDER, keyDER []byte, err error) {
func (s *WindowsService) generateCredentials(ctx context.Context, request generateCredentialsRequest) (certDER, keyDER []byte, err error) {
return windows.GenerateWindowsDesktopCredentials(ctx, &windows.GenerateCredentialsRequest{
CAType: types.UserCA,
Username: username,
Domain: domain,
TTL: ttl,
Username: request.username,
Domain: request.domain,
TTL: request.ttl,
ClusterName: s.clusterName,
ActiveDirectorySID: activeDirectorySID,
ActiveDirectorySID: request.activeDirectorySID,
LDAPConfig: s.cfg.LDAPConfig,
AuthClient: s.cfg.AuthClient,
CreateUser: request.createUser,
Groups: request.groups,
})
}

Expand Down
7 changes: 6 additions & 1 deletion lib/srv/desktop/windows_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ func TestGenerateCredentials(t *testing.T) {
activeDirectorySID: testSid,
},
} {
certb, keyb, err := w.generateCredentials(ctx, user, domain, windows.CertTTL, test.activeDirectorySID)
certb, keyb, err := w.generateCredentials(ctx, generateCredentialsRequest{
username: user,
domain: domain,
ttl: windows.CertTTL,
activeDirectorySID: test.activeDirectorySID,
})
require.NoError(t, err)
require.NotNil(t, certb)
require.NotNil(t, keyb)
Expand Down
3 changes: 3 additions & 0 deletions lib/tlsca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ var (
// PinnedIPASN1ExtensionOID is an extension ID used when encoding/decoding
// the IP the certificate is pinned to.
PinnedIPASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 15}

// CreateWindowsUserOID
CreateWindowsUserOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 16}
)

// Device Trust OIDs.
Expand Down
1 change: 1 addition & 0 deletions lib/web/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ spec:
deny: {}
options:
cert_format: standard
create_desktop_user: false
create_host_user: false
desktop_clipboard: true
desktop_directory_sharing: true
Expand Down
Loading