Skip to content
Open
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
15 changes: 13 additions & 2 deletions docs/pages/enroll-resources/desktop-access/active-directory.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -855,13 +855,24 @@ windows_desktop_service:
addr: example.com:636
```

Alternatively, you can override the KDC address by specifying the `kdc_address`
in your Teleport configuration file.
Alternatively, you can override the KDC address by either specifying the `kdc_address`
or enabling `locate_kdc_server` in your Teleport configuration file.
`locate_kdc_server` discovers a KDC server from the DNS SRV records of your specified
`domain`. This is useful if you have multiple KDC servers for high availability and
load balancing.

```yaml
windows_desktop_service:
enabled: true

# Using the kdc_address method
kdc_address: kdc.example.com # defaults to port 88 if unspecified

# Or, the locate_kdc_server method
locate_kdc_server:
enabled: true
site: "my-site" # optional
port: 88 # optional, overrides DNS port
```

To enable NLA, set the `TELEPORT_ENABLE_RDP_NLA` environment variable to `yes`
Expand Down
11 changes: 11 additions & 0 deletions docs/pages/includes/config-reference/desktop-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ windows_desktop_service:
# no effect when connecting to desktops as local Windows users.
kdc_address: "$KDC_SERVER_ADDRESS"

# locate_kdc_server gets a list of available KDC servers from the AD
# domain's SRV records. When enabled, kdc_address is ignored.
locate_kdc_server:
enabled: true
# (optional) Site is the logical AD site that locate_kdc_server should return.
# Ignored if locate_server is false.
site: "$KDC_SITE_NAME"
# (optional) The port to use for KDC.
# Uses port returned by DNS SRV record, if not set.
port: 88

# (optional) static_hosts is a list of hosts to register as WindowsDesktop
# objects in Teleport. You can define host name and labels directly.
static_hosts:
Expand Down
6 changes: 6 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2278,6 +2278,12 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *servicecfg.Config) error {

cfg.WindowsDesktop.KDCAddr = fc.WindowsDesktop.KDCAddress

cfg.WindowsDesktop.LocateKDCServer = servicecfg.LocateKDCServer{
Enabled: fc.WindowsDesktop.LocateKDCServer.Enabled,
Site: fc.WindowsDesktop.LocateKDCServer.Site,
Port: fc.WindowsDesktop.LocateKDCServer.Port,
}

var hlrs []servicecfg.HostLabelRule
for _, rule := range fc.WindowsDesktop.HostLabels {
r, err := regexp.Compile(rule.Match)
Expand Down
16 changes: 16 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2551,6 +2551,20 @@ type DebugService struct {
Service `yaml:",inline"`
}

// LocateKDCServer automatically locates the KDC server using DNS SRV records
type LocateKDCServer struct {
// Enabled will automatically locate the KDC server using DNS SRV records.
// When enabled, Domain must be set, KDCAddress will be ignored
// https://web.mit.edu/kerberos/krb5-1.4/krb5-1.4.1/doc/krb5-admin/Hostnames-for-KDCs.html
Enabled bool `yaml:"enabled,omitempty"`
// Site is an KDC site to locate servers from a specific logical site.
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/b645c125-a7da-4097-84a1-2fa7cea07714#gt_8abdc986-5679-42d9-ad76-b11eb5a0daba
Site string `yaml:"site,omitempty"`
// Port is the port that should be used to connect to the KDC server.
// This will override the port returned by SRV records.
Port string `yaml:"port,omitempty"`
}

// WindowsDesktopService contains configuration for windows_desktop_service.
type WindowsDesktopService struct {
Service `yaml:",inline"`
Expand All @@ -2575,6 +2589,8 @@ type WindowsDesktopService struct {
// Note: NLA is only supported in Active Directory environments - this field has
// no effect when connecting to desktops as local Windows users.
KDCAddress string `yaml:"kdc_address"`
// LocateKDCServer is the config that enables KDC server location using DNS SRV records.
LocateKDCServer LocateKDCServer `yaml:"locate_kdc_server"`
// Discovery configures desktop discovery via LDAP.
// New usages should prever DiscoveryConfigs instead, which allows for multiple searches.
Discovery LDAPDiscoveryConfig `yaml:"discovery,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions lib/service/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(logger *slog
ShowDesktopWallpaper: cfg.WindowsDesktop.ShowDesktopWallpaper,
LDAPConfig: cfg.WindowsDesktop.LDAP,
KDCAddr: cfg.WindowsDesktop.KDCAddr,
LocateKDCServer: cfg.WindowsDesktop.LocateKDCServer,
PKIDomain: cfg.WindowsDesktop.PKIDomain,
Discovery: cfg.WindowsDesktop.Discovery,
DiscoveryInterval: cfg.WindowsDesktop.DiscoveryInterval,
Expand Down
17 changes: 16 additions & 1 deletion lib/service/servicecfg/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ type WindowsDesktopConfig struct {
// If empty, the LDAP address will be used instead.
// Note: NLA is only supported in Active Directory environments - this field has
// no effect when connecting to desktops as local Windows users.
KDCAddr string
KDCAddr string
LocateKDCServer LocateKDCServer

// Discovery configures automatic desktop discovery via LDAP.
Discovery []LDAPDiscoveryConfig
Expand Down Expand Up @@ -181,6 +182,20 @@ type LDAPConfig struct {
CA *x509.Certificate
}

// LocateKDCServer automatically locates the KDC server using DNS SRV records
type LocateKDCServer struct {
// Enabled will automatically locate the KDC server using DNS SRV records.
// When enabled, Domain must be set, KDCAddress will be ignored
// https://web.mit.edu/kerberos/krb5-1.4/krb5-1.4.1/doc/krb5-admin/Hostnames-for-KDCs.html
Enabled bool
// Site is an KDC site to locate servers from a specific logical site.
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/b645c125-a7da-4097-84a1-2fa7cea07714#gt_8abdc986-5679-42d9-ad76-b11eb5a0daba
Site string
// Port is the port that should be used to connect to the KDC server.
// This will override the port returned by SRV records.
Port string
}

// CheckAndSetDefaults verifies this LDAPConfig
func (cfg *LDAPConfig) CheckAndSetDefaults() error {
if cfg.Addr == "" && !cfg.LocateServer.Enabled {
Expand Down
2 changes: 1 addition & 1 deletion lib/srv/desktop/rdp/rdpclient/client_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type Config struct {
// KDCAddr is the address of Key Distribution Center.
// This is used to support RDP Network Level Authentication (NLA)
// when connecting to hosts enrolled in Active Directory.
// This filed is not used when AD is false.
// This field is not used when AD is false.
KDCAddr string

// AD indicates whether the desktop is part of an Active Directory domain.
Expand Down
84 changes: 83 additions & 1 deletion lib/srv/desktop/windows_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"log/slog"
"maps"
"net"
"os"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -181,6 +182,8 @@ type WindowsServiceConfig struct {
// If empty LDAP address will be used.
// Used for NLA support when AD is true.
KDCAddr string
// LocateKDCServer automatically locates the KDC server using DNS SRV records
servicecfg.LocateKDCServer
// Discovery contains policies for configuring LDAP-based discovery.
Discovery []servicecfg.LDAPDiscoveryConfig
// DiscoveryInterval configures how frequently the discovery process runs.
Expand Down Expand Up @@ -788,7 +791,11 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
}
log = log.With("computer_name", computerName)

kdcAddr := s.cfg.KDCAddr
kdcAddr, err := s.getKDCAddress(ctx)
if err != nil {
return trace.Wrap(err, "getting KDC address")
}

if !desktop.NonAD() && kdcAddr == "" && s.cfg.LDAPConfig.Addr != "" {
if kdcAddr, err = utils.Host(s.cfg.LDAPConfig.Addr); err != nil {
return trace.Wrap(err, "KDC address is unspecified and LDAP address is invalid")
Expand Down Expand Up @@ -1330,3 +1337,78 @@ func (s *WindowsService) runCRLUpdateLoop(tlsConfig *tls.Config) {
}
}
}

func (s *WindowsService) getKDCAddress(ctx context.Context) (string, error) {
if !s.cfg.LocateKDCServer.Enabled {
return s.cfg.KDCAddr, nil
}

s.cfg.Logger.DebugContext(
ctx,
"Looking for KDC server",
"Domain", s.cfg.Domain,
"Site", s.cfg.LocateKDCServer.Site,
"Port", s.cfg.LocateKDCServer.Port,
)
dialer := net.Dialer{}
dial := func(dialCtx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(dialCtx, network, address)
}

// In development environments, the system's default resolver is unlikely to be
// able to resolve the Active Directory SRV records needed for server location,
// so we allow overriding the resolver.
if resolverAddr := os.Getenv("TELEPORT_KDC_RESOLVER"); resolverAddr != "" {
s.cfg.Logger.DebugContext(ctx, "Using custom DNS resolver address", "address", resolverAddr)
// Check if resolver address has a port
host, port, err := net.SplitHostPort(resolverAddr)
if err != nil {
host = resolverAddr
port = "53"
}

customResolverAddr := net.JoinHostPort(host, port)
dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, customResolverAddr)
}
}

resolver := &net.Resolver{
PreferGo: true,
Dial: dial,
}
Comment on lines +1353 to +1379
Copy link
Contributor

Choose a reason for hiding this comment

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

Could all of this be abstracted behind some kind of "resolver" interface? One that we could initialize and pass to WindowsService during construction?

It seems like picking a resolver strategy is a separate concern from actually using it to resolve the KDC address, and the logic for each may evolve differently.


servers, err := winpki.LocateServerBySRV(
ctx,
s.cfg.Domain,
s.cfg.LocateKDCServer.Site,
resolver,
"kerberos",
s.cfg.LocateKDCServer.Port,
)
if err != nil {
return "", trace.Wrap(err, "locating KDC server")
}

if len(servers) == 0 {
return "", trace.NotFound("no KDC servers found for domain %q", s.cfg.Domain)
}

var lastErr error
for _, server := range servers {
conn, err := net.DialTimeout("tcp", server, 5*time.Second)
if conn != nil {
conn.Close()
}

if err == nil {
s.cfg.Logger.InfoContext(ctx, "Found KDC server", "server", server)
return server, nil
}
lastErr = err

s.cfg.Logger.InfoContext(ctx, "Error connecting to KDC server, trying next available server", "server", server, "error", err)
}

return "", trace.NotFound("no KDC servers responded successfully for domain %q: %v", s.cfg.Domain, lastErr)
}
2 changes: 1 addition & 1 deletion lib/winpki/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ func (c *LDAPConfig) createConnection(ctx context.Context, ldapTLSConfig *tls.Co
}

var err error
if servers, err = locateLDAPServer(ctx, c.Domain, c.LocateServer.Site, resolver); err != nil {
if servers, err = LocateServerBySRV(ctx, c.Domain, c.LocateServer.Site, resolver, "ldap", "636"); err != nil {
return nil, trace.Wrap(err, "locating LDAP server")
}
}
Expand Down
24 changes: 15 additions & 9 deletions lib/winpki/locate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,26 @@ package winpki
import (
"context"
"net"
"strconv"

"github.com/gravitational/trace"
)

// locateLDAPServer looks up the LDAP server in an Active Directory
// environment by implementing the DNS-based discovery DC locator
// process.
// LocateServerBySRV looks up a server of a given service and port
// in an Active Directory environment by implementing the
// DNS-based discovery DC locator process.
//
// See https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/dc-locator?tabs=dns-based-discovery
func locateLDAPServer(ctx context.Context, domain string, site string, resolver *net.Resolver) ([]string, error) {
func LocateServerBySRV(ctx context.Context, domain string, site string, resolver *net.Resolver, service string, port string) ([]string, error) {
tryDomain := domain
if site != "" {
tryDomain = site + "._sites." + domain
}

_, records, err := resolver.LookupSRV(ctx, "ldap", "tcp", tryDomain)
_, records, err := resolver.LookupSRV(ctx, service, "tcp", tryDomain)
if err != nil && site != "" {
// If the site lookup fails, try the domain directly.
_, records, err = resolver.LookupSRV(ctx, "ldap", "tcp", domain)
_, records, err = resolver.LookupSRV(ctx, service, "tcp", domain)
}

if err != nil {
Expand All @@ -49,9 +50,14 @@ func locateLDAPServer(ctx context.Context, domain string, site string, resolver
// note: LookupSRV already returns records sorted by priority and takes in to account weights
var result []string
for _, record := range records {
// SRV records will likely return the insecure LDAP port,
// so we ignore it and hard code the LDAPS port.
result = append(result, net.JoinHostPort(record.Target, "636"))
// If a port has been passed, use that.
// If not, use the port returned by the SRV record.
usePort := strconv.Itoa(int(record.Port))
if port != "" {
usePort = port
}

result = append(result, net.JoinHostPort(record.Target, usePort))
}

return result, nil
Expand Down
Loading