From dc00f354a6f3ede42f56dd9c7ade3fbbac0ce57b Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Sat, 14 May 2022 18:54:47 -0600 Subject: [PATCH] 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 --- docs/pages/desktop-access/rbac.mdx | 38 +++++++++++++++---- .../desktop-access/desktop-config.yaml | 4 ++ lib/config/configuration.go | 7 ++++ lib/config/configuration_test.go | 7 ++++ lib/service/cfg.go | 6 +++ lib/service/desktop.go | 9 +++-- lib/srv/desktop/discovery.go | 22 ++++++++--- lib/srv/desktop/discovery_test.go | 20 +++++++++- lib/srv/desktop/windows_server.go | 3 ++ 9 files changed, 97 insertions(+), 19 deletions(-) diff --git a/docs/pages/desktop-access/rbac.mdx b/docs/pages/desktop-access/rbac.mdx index e746ecfab05ea..be158b34b57db 100644 --- a/docs/pages/desktop-access/rbac.mdx +++ b/docs/pages/desktop-access/rbac.mdx @@ -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. Via the `host_labels` rules defined in the `windows_desktop_service` section + of your Teleport configuration file. +2. Via 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`: +### With `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: @@ -76,7 +77,16 @@ host_labels: environment: prod ``` -For desktops discovered via LDAP, Teleport applies the following labels automatically: +### With LDAP + +There are several ways that desktops discovered via LDAP can be labeled: + +1. Automatic `teleport.dev/` labels, applied unconditionally +2. Custom `ldap/` labels, sourced from LDAP attributes and applied based on + discovery configuration + +Teleport applies the following labels automatically to all desktops discovered +via LDAP: | Label | LDAP Attribute | Example | | ----------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- | @@ -88,6 +98,18 @@ 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 which will be converted into +Teleport labels. For example, consider the following 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 diff --git a/docs/pages/includes/desktop-access/desktop-config.yaml b/docs/pages/includes/desktop-access/desktop-config.yaml index c8fd1bce12110..7872a0b756e9f 100644 --- a/docs/pages/includes/desktop-access/desktop-config.yaml +++ b/docs/pages/includes/desktop-access/desktop-config.yaml @@ -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. diff --git a/lib/config/configuration.go b/lib/config/configuration.go index e5be9ee7e91e5..46764914bc7c0 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -1360,6 +1360,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 diff --git a/lib/config/configuration_test.go b/lib/config/configuration_test.go index 2f427f172e372..ee6c414a52bf2 100644 --- a/lib/config/configuration_test.go +++ b/lib/config/configuration_test.go @@ -1615,6 +1615,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, diff --git a/lib/service/cfg.go b/lib/service/cfg.go index 56b03629c5908..70dd97b61d227 100644 --- a/lib/service/cfg.go +++ b/lib/service/cfg.go @@ -1053,6 +1053,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. diff --git a/lib/service/desktop.go b/lib/service/desktop.go index 33c8cb7de531f..718827fa1450a 100644 --- a/lib/service/desktop.go +++ b/lib/service/desktop.go @@ -223,10 +223,11 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(log *logrus. StaticHosts: cfg.WindowsDesktop.Hosts, OnHeartbeat: process.onHeartbeat(teleport.ComponentWindowsDesktop), }, - LDAPConfig: desktop.LDAPConfig(cfg.WindowsDesktop.LDAP), - DiscoveryBaseDN: cfg.WindowsDesktop.Discovery.BaseDN, - DiscoveryLDAPFilters: cfg.WindowsDesktop.Discovery.Filters, - Hostname: cfg.Hostname, + LDAPConfig: desktop.LDAPConfig(cfg.WindowsDesktop.LDAP), + DiscoveryBaseDN: cfg.WindowsDesktop.Discovery.BaseDN, + DiscoveryLDAPFilters: cfg.WindowsDesktop.Discovery.Filters, + DiscoveryLDAPAttributeLabels: cfg.WindowsDesktop.Discovery.LabelAttributes, + Hostname: cfg.Hostname, }) if err != nil { return trace.Wrap(err) diff --git a/lib/srv/desktop/discovery.go b/lib/srv/desktop/discovery.go index c73c4f9361aa2..dcf3c0c7c50b6 100644 --- a/lib/srv/desktop/discovery.go +++ b/lib/srv/desktop/discovery.go @@ -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, computerAttribtes...) + 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 @@ -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 @@ -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 { diff --git a/lib/srv/desktop/discovery_test.go b/lib/srv/desktop/discovery_test.go index f0491024b17f3..aa83c2c0b3740 100644 --- a/lib/srv/desktop/discovery_test.go +++ b/lib/srv/desktop/discovery_test.go @@ -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 @@ -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) diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index 789051b1f7469..3961c0cae7811 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -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 }