Skip to content

Commit

Permalink
Windows user creation (#24780)
Browse files Browse the repository at this point in the history
* Windows auto user creation

* changes in role

* fix roles

* make grpc

* fix imports

* fix test

* fix test

* fix test

* fix test

* fix test

* windows labels

* rename OID, add json tags

* params to struct

* grpc

* Update lib/services/role.go

Co-authored-by: Isaiah Becker-Mayer <isaiah@goteleport.com>

* Update lib/services/access_checker.go

Co-authored-by: Isaiah Becker-Mayer <isaiah@goteleport.com>

* grpc

* bump e

* only add extension when we create user

---------

Co-authored-by: Isaiah Becker-Mayer <isaiah@goteleport.com>
  • Loading branch information
probakowski and ibeckermayer committed Apr 28, 2023
1 parent 867c3cd commit 4b4fe55
Show file tree
Hide file tree
Showing 13 changed files with 5,588 additions and 2,660 deletions.
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 @@ -2383,6 +2383,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 @@ -2558,6 +2565,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 @@ -833,6 +856,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
8,057 changes: 5,411 additions & 2,646 deletions api/types/types.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e
Submodule e updated from c7a7ce to 0d34c0
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 @@ -142,6 +166,10 @@ type GenerateCredentialsRequest struct {
LDAPConfig LDAPConfig
// AuthClient is the windows AuthInterface
AuthClient AuthInterface
// 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 @@ -421,6 +421,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 @@ -2715,6 +2718,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 @@ -450,7 +450,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 @@ -852,10 +857,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 @@ -1092,7 +1103,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 @@ -1125,23 +1136,50 @@ 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{
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 @@ -217,6 +217,7 @@ spec:
deny: {}
options:
cert_format: standard
create_desktop_user: false
create_host_user: false
desktop_clipboard: true
desktop_directory_sharing: true
Expand Down

0 comments on commit 4b4fe55

Please sign in to comment.