Skip to content

Commit

Permalink
[v9] Label desktops based on the content of LDAP attributes (#13238)
Browse files Browse the repository at this point in the history
* Label desktops based on the content of LDAP attributes

This allows users to configure an optional set of LDAP attributes
which will be included in all LDAP queries. Teleport uses these
attributes when labeling desktops.

Updates #12326
  • Loading branch information
zmb3 authored Jun 7, 2022
1 parent af8e672 commit c60a318
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 16 deletions.
38 changes: 30 additions & 8 deletions docs/pages/desktop-access/rbac.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ use wildcards (`"*"`) to match all desktop labels.

Windows desktops acquire labels in two ways:

1. The `host_labels` rules defined in the `windows_desktop_service` section of
your Teleport configuration file.
2. Automatic `teleport.dev/` labels applied by Teleport (for desktops discovered
via LDAP only)
1. Using the `host_labels` rules defined in the `windows_desktop_service` section
of your Teleport configuration file.
2. Using LDAP (for desktops discovered via LDAP only)

For example, the following `host_labels` configuration would apply the
`environment: dev` label to a Windows desktop named `test.dev.example.com`
and the `environment: prod` label to `desktop.prod.example.com`:
### Using `host_labels`

The following `host_labels` configuration would apply the `environment: dev`
label to a Windows desktop named `test.dev.example.com` and the
`environment: prod` label to `desktop.prod.example.com`:

```yaml
host_labels:
Expand All @@ -76,7 +77,14 @@ host_labels:
environment: prod
```

For desktops discovered via LDAP, Teleport applies the following labels automatically:
### Using LDAP

Teleport Desktop Access can automatically discover Windows Desktops and register
them with the Teleport Cluster by periodically querying an LDAP server. There
are several ways that these desktops can be labeled:

Teleport applies the following labels automatically to all desktops discovered
via LDAP:

| Label | LDAP Attribute | Example |
| ----------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |
Expand All @@ -88,6 +96,20 @@ For desktops discovered via LDAP, Teleport applies the following labels automati
| `teleport.dev/is_domain_controller` | `primaryGroupID` | `true` |
| `teleport.dev/ou` | Derived from `distinguishedName` | `OU=IT,DC=goteleport,DC=com` |

Additionally, users can configure
[LDAP attributes](https://docs.microsoft.com/en-us/windows/win32/adschema/attributes-all)
which will be converted into Teleport labels. For example, consider the
following Desktop Access configuration:

```yaml
discovery:
label_attributes:
- location
```

For a desktop with a `location` attribute of `Oakland`, Teleport would apply a
label with key `ldap/location` and value `Oakland`.

## Logins

The `windows_desktop_logins` role setting lists the Windows user accounts that
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/includes/desktop-access/desktop-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ windows_desktop_service:
filters:
- '(location=Oakland)'
- '(!(primaryGroupID=516))' # exclude domain controllers
# (optional) LDAP attributes to convert into Teleport labels.
# The key of the label will be "ldap/" + the value of the attribute.
label_attributes:
- location
# Rules for applying labels to Windows hosts based on regular expressions
# matched against the host name. If multiple rules match, the desktop will
# get the union of all matching labels.
Expand Down
7 changes: 7 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,13 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *service.Config) error {
return trace.BadParameter("WindowsDesktopService specifies invalid LDAP filter %q", filter)
}
}

for _, attributeName := range fc.WindowsDesktop.Discovery.LabelAttributes {
if !types.IsValidLabelKey(attributeName) {
return trace.BadParameter("WindowsDesktopService specifies label_attribute %q which is not a valid label key", attributeName)
}
}

cfg.WindowsDesktop.Discovery = fc.WindowsDesktop.Discovery

var err error
Expand Down
7 changes: 7 additions & 0 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,13 @@ func TestWindowsDesktopService(t *testing.T) {
}
},
},
{
desc: "NOK - invalid label key for LDAP attribute",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery.LabelAttributes = []string{"this?is not* a valid key 🚨"}
},
},
{
desc: "OK - valid config",
expectError: require.NoError,
Expand Down
6 changes: 6 additions & 0 deletions lib/service/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,12 @@ type LDAPDiscoveryConfig struct {
// Filters are additional LDAP filters to apply to the search.
// See: https://ldap.com/ldap-filters/
Filters []string `yaml:"filters"`
// LabelAttributes are LDAP attributes to apply to hosts discovered
// via LDAP. Teleport labels hosts by prefixing the attribute with
// "ldap/" - for example, a value of "location" here would result in
// discovered desktops having a label with key "ldap/location" and
// the value being the value of the "location" attribute.
LabelAttributes []string `yaml:"label_attributes"`
}

// HostLabelRules is a collection of rules describing how to apply labels to hosts.
Expand Down
24 changes: 18 additions & 6 deletions lib/srv/desktop/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
// computerAttributes are the attributes we fetch when discovering
// Windows hosts via LDAP
// see: https://docs.microsoft.com/en-us/windows/win32/adschema/c-computer#windows-server-2012-attributes
var computerAttribtes = []string{
var computerAttributes = []string{
attrName,
attrCommonName,
attrDistinguishedName,
Expand Down Expand Up @@ -130,7 +130,11 @@ func (s *WindowsService) getDesktopsFromLDAP() types.ResourcesWithLabelsMap {
filter := s.ldapSearchFilter()
s.cfg.Log.Debugf("searching for desktops with LDAP filter %v", filter)

entries, err := s.lc.readWithFilter(s.cfg.DiscoveryBaseDN, filter, computerAttribtes)
var attrs []string
attrs = append(attrs, computerAttributes...)
attrs = append(attrs, s.cfg.DiscoveryLDAPAttributeLabels...)

entries, err := s.lc.readWithFilter(s.cfg.DiscoveryBaseDN, filter, attrs)
if trace.IsConnectionProblem(err) {
// If the connection was broken, re-initialize the LDAP client so that it's
// ready for the next reconcile loop. Return the last known set of desktops
Expand Down Expand Up @@ -180,26 +184,34 @@ func (s *WindowsService) deleteDesktop(ctx context.Context, r types.ResourceWith
return s.cfg.AuthClient.DeleteWindowsDesktop(ctx, d.GetHostID(), d.GetName())
}

func applyLabelsFromLDAP(entry *ldap.Entry, labels map[string]string) {
func (s *WindowsService) applyLabelsFromLDAP(entry *ldap.Entry, labels map[string]string) {
// apply common LDAP labels by default
labels[types.OriginLabel] = types.OriginDynamic

labels[types.TeleportNamespace+"/dns_host_name"] = entry.GetAttributeValue(attrDNSHostName)
labels[types.TeleportNamespace+"/computer_name"] = entry.GetAttributeValue(attrName)
labels[types.TeleportNamespace+"/os"] = entry.GetAttributeValue(attrOS)
labels[types.TeleportNamespace+"/os_version"] = entry.GetAttributeValue(attrOSVersion)

// attempt to compute the desktop's OU from its DN
dn := entry.GetAttributeValue(attrDistinguishedName)
cn := entry.GetAttributeValue(attrCommonName)

if len(dn) > 0 && len(cn) > 0 {
ou := strings.TrimPrefix(dn, "CN="+cn+",")
labels[types.TeleportNamespace+"/ou"] = ou
}

// label domain controllers
switch entry.GetAttributeValue(attrPrimaryGroupID) {
case writableDomainControllerGroupID, readOnlyDomainControllerGroupID:
labels[types.TeleportNamespace+"/is_domain_controller"] = "true"
}

// apply any custom labels per the discovery configuration
for _, attr := range s.cfg.DiscoveryLDAPAttributeLabels {
if v := entry.GetAttributeValue(attr); v != "" {
labels["ldap/"+attr] = v
}
}
}

// ldapEntryToWindowsDesktop generates the Windows Desktop resource
Expand All @@ -208,7 +220,7 @@ func (s *WindowsService) ldapEntryToWindowsDesktop(ctx context.Context, entry *l
hostname := entry.GetAttributeValue(attrDNSHostName)
labels := getHostLabels(hostname)
labels[types.TeleportNamespace+"/windows_domain"] = s.cfg.Domain
applyLabelsFromLDAP(entry, labels)
s.applyLabelsFromLDAP(entry, labels)

addrs, err := s.dnsResolver.LookupHost(ctx, hostname)
if err != nil || len(addrs) == 0 {
Expand Down
20 changes: 18 additions & 2 deletions lib/srv/desktop/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,34 @@ func TestAppliesLDAPLabels(t *testing.T) {
attrOSVersion: {"6.1"},
attrDistinguishedName: {"CN=foo,OU=IT,DC=goteleport,DC=com"},
attrCommonName: {"foo"},
"bar": {"baz"},
"quux": {""},
})
applyLabelsFromLDAP(entry, l)

s := &WindowsService{
cfg: WindowsServiceConfig{
DiscoveryLDAPAttributeLabels: []string{"bar"},
},
}
s.applyLabelsFromLDAP(entry, l)

// check default labels
require.Equal(t, l[types.OriginLabel], types.OriginDynamic)
require.Equal(t, l[types.TeleportNamespace+"/dns_host_name"], "foo.example.com")
require.Equal(t, l[types.TeleportNamespace+"/computer_name"], "foo")
require.Equal(t, l[types.TeleportNamespace+"/os"], "Windows Server")
require.Equal(t, l[types.TeleportNamespace+"/os_version"], "6.1")

// check OU label
require.Equal(t, l[types.TeleportNamespace+"/ou"], "OU=IT,DC=goteleport,DC=com")

// check custom labels
require.Equal(t, l["ldap/bar"], "baz")
require.Empty(t, l["ldap/quux"])
}

func TestLabelsDomainControllers(t *testing.T) {
s := &WindowsService{}
for _, test := range []struct {
desc string
entry *ldap.Entry
Expand Down Expand Up @@ -110,7 +126,7 @@ func TestLabelsDomainControllers(t *testing.T) {
} {
t.Run(test.desc, func(t *testing.T) {
l := make(map[string]string)
applyLabelsFromLDAP(test.entry, l)
s.applyLabelsFromLDAP(test.entry, l)

b, _ := strconv.ParseBool(l[types.TeleportNamespace+"/is_domain_controller"])
test.assert(t, b)
Expand Down
3 changes: 3 additions & 0 deletions lib/srv/desktop/windows_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ type WindowsServiceConfig struct {
// Windows Desktops. If multiple filters are specified, they are ANDed
// together into a single search.
DiscoveryLDAPFilters []string
// DiscoveryLDAPAttributeLabels are optional LDAP attributes to convert
// into Teleport labels.
DiscoveryLDAPAttributeLabels []string
// Hostname of the windows desktop service
Hostname string
}
Expand Down

0 comments on commit c60a318

Please sign in to comment.