mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-01 16:03:55 +08:00
desktop discovery: support multi-valued LDAP attributes
LDAP attributes aren't always a single-key to single-value mapping. Prior to this change, Teleport would take the first value for attributes that specify multiple values. This change introduces a new opt-in discovery config setting that allows users to configure Teleport to join multiple values into a single value.
This commit is contained in:
@@ -160,7 +160,7 @@ To create the service account:
|
||||
```code
|
||||
$ Get-ADUser -Identity svc-teleport
|
||||
```
|
||||
|
||||
|
||||
- If the `dsacls` commands fail, ensure you're running PowerShell as a Domain Administrator.
|
||||
|
||||
- Verify the LDAP containers were created by checking `CN=Teleport,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration` in AD.
|
||||
@@ -475,14 +475,14 @@ the performance of remote desktop connections.
|
||||
$ gpresult /r
|
||||
```
|
||||
|
||||
- Verify the Teleport CA is in the NTAuth store:
|
||||
- Verify the Teleport CA is in the NTAuth store:
|
||||
|
||||
```code
|
||||
$ certutil -viewstore -enterprise NTAuth
|
||||
```
|
||||
|
||||
- Verify the Teleport CA is in the Root store:
|
||||
|
||||
- Verify the Teleport CA is in the Root store:
|
||||
|
||||
```code
|
||||
$ certutil -viewstore -enterprise Root
|
||||
```
|
||||
@@ -709,14 +709,14 @@ To configure Teleport to protect access to Windows desktops:
|
||||
description="Confirm that the Teleport Windows Desktop Service has started and connected to your cluster."
|
||||
>
|
||||
Here are some troubleshooting tips:
|
||||
- Check the service status on the Linux server:
|
||||
|
||||
- Check the service status on the Linux server:
|
||||
|
||||
```code
|
||||
$ systemctl status teleport
|
||||
```
|
||||
|
||||
- Review logs for connection errors:
|
||||
|
||||
- Review logs for connection errors:
|
||||
|
||||
```code
|
||||
$ sudo journalctl -u teleport
|
||||
```
|
||||
@@ -725,8 +725,8 @@ To configure Teleport to protect access to Windows desktops:
|
||||
|
||||
- Ensure the Linux server can reach the Teleport Proxy and the LDAP server on port 636.
|
||||
|
||||
- Test LDAP connectivity:
|
||||
|
||||
- Test LDAP connectivity:
|
||||
|
||||
```code
|
||||
$ nc -vz $LDAP_SERVER 636
|
||||
```
|
||||
@@ -802,8 +802,8 @@ To connect to a Windows desktop:
|
||||
|
||||
- Ensure your Teleport user is assigned the `windows-desktop-admins` role or a role with the same permissions.
|
||||
|
||||
- Review the Windows Desktop Service logs:
|
||||
|
||||
- Review the Windows Desktop Service logs:
|
||||
|
||||
```code
|
||||
$ sudo journalctl -u teleport
|
||||
```
|
||||
@@ -827,7 +827,7 @@ appropriate user rights, and they must be added to the 'Remote Desktop Users' AD
|
||||
|
||||
### Add users/groups to the 'Remote Desktop Users' AD group
|
||||
|
||||
1. Open **Active Directory Users and Computers**
|
||||
1. Open **Active Directory Users and Computers**
|
||||
1. Navigate to **`$YOUR_DOMAIN -> Builtin`** and double-click **Remote Desktop Users**
|
||||
1. Select the **Members** tab and click **Add**
|
||||
1. (Optional) Click **Locations** and select the domain for which you wish to grant logon access to users/groups.
|
||||
@@ -894,7 +894,26 @@ For example, if an AD computer object had a location attribute with a value of O
|
||||
and a department attribute with a value of Engineering, the Teleport resource for this
|
||||
host would have both `ldap/location=Oakland` and `ldap/department=Engineering` labels.
|
||||
|
||||
In addition, you can also specify a set of static labels that apply to all hosts discovered
|
||||
Teleport's default behavior for multi-valued attributes is to pick the first one.
|
||||
For example, a host that has an LDAP attribute `usage` with values `db` and
|
||||
`ca` would result in a Teleport label of `ldap/usage=db`. You can override this
|
||||
behavior by setting `label_attribute_mode: join` in your discovery config:
|
||||
|
||||
```yaml
|
||||
windows_desktop_service:
|
||||
enabled: true
|
||||
discovery_configs:
|
||||
- base_dn: '*'
|
||||
label_attributes:
|
||||
- 'usage'
|
||||
label_attribute_mode: 'join'
|
||||
label_attribute_join_separator: '_'
|
||||
```
|
||||
|
||||
With this config, the Teleport label for this host would be
|
||||
`ldap/usage=db_ca`. The join separator defaults to `|` if unspecified.
|
||||
|
||||
Lastly, you can also specify a set of static labels that apply to all hosts discovered
|
||||
via this policy:
|
||||
|
||||
```yaml
|
||||
@@ -1008,7 +1027,7 @@ the external forest. Multiple CA certificates can be written to the `ldap_ca_cer
|
||||
```yaml
|
||||
windows_desktop_service:
|
||||
enabled: true
|
||||
|
||||
|
||||
ldap:
|
||||
addr: example.com:636
|
||||
username: 'DEV\svc-teleport'
|
||||
|
||||
@@ -109,10 +109,16 @@ 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
|
||||
# For multi-valued attributes Teleport takes the first value
|
||||
# by default. This can be overridden with the "join" mode.
|
||||
label_attribute_mode: join # (optional) defaults to "first"
|
||||
label_attribute_join_separator: "_" # (optional) defaults to '|'
|
||||
|
||||
# (optional) static labels to apply to all hosts discovered via this policy
|
||||
labels:
|
||||
env: prod
|
||||
|
||||
+19
-22
@@ -2376,22 +2376,15 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
||||
cfg.WindowsDesktop.ListenAddr = *listenAddr
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
for _, filter := range fc.WindowsDesktop.Discovery.Filters {
|
||||
if _, err := ldap.CompileFilter(filter); err != nil {
|
||||
return trace.BadParameter("WindowsDesktopService specifies invalid LDAP filter %q", filter)
|
||||
}
|
||||
}
|
||||
|
||||
if fc.WindowsDesktop.Discovery.BaseDN != "" && len(fc.WindowsDesktop.DiscoveryConfigs) > 0 {
|
||||
return trace.BadParameter("WindowsDesktopService specifies both discovery and discovery_configs: move the discovery section to discovery_configs to continue")
|
||||
}
|
||||
|
||||
// append the old (singular) discovery config to the new format that supports multiple configs
|
||||
if fc.WindowsDesktop.Discovery.BaseDN != "" {
|
||||
fc.WindowsDesktop.DiscoveryConfigs = append(fc.WindowsDesktop.DiscoveryConfigs, fc.WindowsDesktop.Discovery)
|
||||
}
|
||||
|
||||
for _, discoveryConfig := range fc.WindowsDesktop.DiscoveryConfigs {
|
||||
for _, filter := range discoveryConfig.Filters {
|
||||
if _, err := ldap.CompileFilter(filter); err != nil {
|
||||
@@ -2408,16 +2401,18 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
||||
return trace.BadParameter("WindowsDesktopService specifies label_attribute %q which is not a valid label key", attributeName)
|
||||
}
|
||||
}
|
||||
switch mode := discoveryConfig.LabelAttributeMode; mode {
|
||||
case servicecfg.LabelAttributeModeJoin:
|
||||
case servicecfg.LabelAttributeModeFirst, "":
|
||||
default:
|
||||
return trace.BadParameter("WindowsDesktopService specifies invalid label_attribute_mode %q", mode)
|
||||
}
|
||||
|
||||
if p := discoveryConfig.RDPPort; p < 0 || p > 65535 {
|
||||
return trace.BadParameter("WindowsDesktopService specifies invalid RDP port %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
// append the old (singular) discovery config to the new format that supports multiple configs
|
||||
if fc.WindowsDesktop.Discovery.BaseDN != "" {
|
||||
fc.WindowsDesktop.DiscoveryConfigs = append(fc.WindowsDesktop.DiscoveryConfigs, fc.WindowsDesktop.Discovery)
|
||||
}
|
||||
|
||||
cfg.WindowsDesktop.Discovery = make([]servicecfg.LDAPDiscoveryConfig, 0, len(fc.WindowsDesktop.DiscoveryConfigs))
|
||||
for _, dc := range fc.WindowsDesktop.DiscoveryConfigs {
|
||||
if dc.BaseDN == "" {
|
||||
@@ -2425,11 +2420,13 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
||||
}
|
||||
cfg.WindowsDesktop.Discovery = append(cfg.WindowsDesktop.Discovery,
|
||||
servicecfg.LDAPDiscoveryConfig{
|
||||
BaseDN: dc.BaseDN,
|
||||
Filters: dc.Filters,
|
||||
Labels: dc.Labels,
|
||||
LabelAttributes: dc.LabelAttributes,
|
||||
RDPPort: cmp.Or(dc.RDPPort, int(defaults.RDPListenPort)),
|
||||
BaseDN: dc.BaseDN,
|
||||
Filters: dc.Filters,
|
||||
Labels: dc.Labels,
|
||||
LabelAttributes: dc.LabelAttributes,
|
||||
LabelAttributeMode: cmp.Or(dc.LabelAttributeMode, string(servicecfg.LabelAttributeModeFirst)),
|
||||
LabelAttributeJoinSeparator: dc.LabelAttributeJoinSeparator,
|
||||
RDPPort: cmp.Or(dc.RDPPort, int(defaults.RDPListenPort)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2382,6 +2382,7 @@ uQM=
|
||||
desc: "NOK - invalid label key for LDAP attribute",
|
||||
expectError: require.Error,
|
||||
mutate: func(fc *FileConfig) {
|
||||
fc.WindowsDesktop.Discovery.BaseDN = "*"
|
||||
fc.WindowsDesktop.Discovery.LabelAttributes = []string{"this?is not* a valid key 🚨"}
|
||||
},
|
||||
},
|
||||
@@ -2451,6 +2452,19 @@ uQM=
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "OK - new discovery specified and ldap specified",
|
||||
expectError: require.NoError,
|
||||
mutate: func(fc *FileConfig) {
|
||||
fc.WindowsDesktop.DiscoveryConfigs = []LDAPDiscoveryConfig{
|
||||
{BaseDN: "something"},
|
||||
}
|
||||
fc.WindowsDesktop.LDAP = LDAPConfig{
|
||||
Addr: "something",
|
||||
Domain: "example.com",
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "OK - discovery not specified and ldap not specified",
|
||||
expectError: require.NoError,
|
||||
@@ -2491,6 +2505,30 @@ uQM=
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "NOK - invalid label attribute mode",
|
||||
expectError: require.Error,
|
||||
mutate: func(fc *FileConfig) {
|
||||
fc.WindowsDesktop.DiscoveryConfigs = []LDAPDiscoveryConfig{
|
||||
{
|
||||
BaseDN: "*",
|
||||
LabelAttributes: []string{"foo"},
|
||||
LabelAttributeMode: "invalid",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "NOK - invalid label attribute (legacy)",
|
||||
expectError: require.Error,
|
||||
mutate: func(fc *FileConfig) {
|
||||
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
|
||||
BaseDN: "*",
|
||||
LabelAttributes: []string{"foo"},
|
||||
LabelAttributeMode: "invalid",
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "NOK - invalid RDP port",
|
||||
expectError: require.Error,
|
||||
|
||||
+10
-3
@@ -3015,6 +3015,9 @@ type LDAPDiscoveryConfig struct {
|
||||
// Filters are additional LDAP filters to apply to the search.
|
||||
// See: https://ldap.com/ldap-filters/
|
||||
Filters []string `yaml:"filters"`
|
||||
// RDPPort is the port to use for RDP for hosts discovered with this configuration.
|
||||
// Optional, defaults to 3389 if unspecified.
|
||||
RDPPort int `yaml:"rdp_port"`
|
||||
// Labels are static labels applied to all hosts discovered via
|
||||
// this policy.
|
||||
Labels map[string]string `yaml:"labels,omitempty"`
|
||||
@@ -3024,9 +3027,13 @@ type LDAPDiscoveryConfig struct {
|
||||
// discovered desktops having a label with key "ldap/location" and
|
||||
// the value being the value of the "location" attribute.
|
||||
LabelAttributes []string `yaml:"label_attributes"`
|
||||
// RDPPort is the port to use for RDP for hosts discovered with this configuration.
|
||||
// Optional, defaults to 3389 if unspecified.
|
||||
RDPPort int `yaml:"rdp_port"`
|
||||
// LabelAttributeMode determines how multi-valued LDAP attributes are
|
||||
// treated. Valid values are:
|
||||
// - "first" (the default if unspecified): use the first attribute value
|
||||
// - "join": multi-valued attributes are joined with the specified separator
|
||||
LabelAttributeMode string `yaml:"label_attribute_mode"`
|
||||
// LabelAttributeJoinSeparator is used when LabelAttributeMode is "join".
|
||||
LabelAttributeJoinSeparator string `yaml:"label_attribute_join_separator"`
|
||||
}
|
||||
|
||||
// TracingService contains configuration for the tracing_service.
|
||||
|
||||
@@ -100,16 +100,33 @@ type LDAPDiscoveryConfig struct {
|
||||
Filters []string
|
||||
// Labels are static labels to apply to hosts discovered via LDAP.
|
||||
Labels map[string]string
|
||||
// RDPPort is the RDP port to register for each host discovered with this configuration.
|
||||
RDPPort int
|
||||
|
||||
// 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
|
||||
// RDPPort is the RDP port to register for each host discovered with this configuration.
|
||||
RDPPort int
|
||||
// LabelAttributeMode determines how multi-valued LDAP attributes are
|
||||
// treated. Valid values are:
|
||||
// - "first" (the default if unspecified): use the first attribute value
|
||||
// - "join": multi-valued attributes are joined with the specified separator
|
||||
LabelAttributeMode string
|
||||
// LabelAttributeJoinSeparator is used when LabelAttributeMode is "join".
|
||||
LabelAttributeJoinSeparator string
|
||||
}
|
||||
|
||||
const (
|
||||
// LabelAttributeModeFirst configures Teleport to select the first attribute
|
||||
// value for multi-value attributes.
|
||||
LabelAttributeModeFirst = "first"
|
||||
// LabelAttributeModeJoin configures Teleport to join all attribute values
|
||||
// into a single string.
|
||||
LabelAttributeModeJoin = "join"
|
||||
)
|
||||
|
||||
// HostLabelRules is a collection of rules describing how to apply labels to hosts.
|
||||
type HostLabelRules struct {
|
||||
rules []HostLabelRule
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -295,9 +296,27 @@ func (s *WindowsService) applyLabelsFromLDAP(entry *ldap.Entry, labels map[strin
|
||||
}
|
||||
|
||||
// apply any custom labels per the discovery configuration
|
||||
const maxAttributeLabelLen = 512
|
||||
for _, attr := range cfg.LabelAttributes {
|
||||
if v := entry.GetAttributeValue(attr); v != "" {
|
||||
labels[types.DiscoveryLabelLDAPPrefix+attr] = v
|
||||
values := entry.GetAttributeValues(attr)
|
||||
values = slices.DeleteFunc(values, func(v string) bool { return strings.TrimSpace(v) == "" })
|
||||
if len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Take only the first value when not in join mode.
|
||||
if cfg.LabelAttributeMode != servicecfg.LabelAttributeModeJoin && len(values) > 1 {
|
||||
values = values[:1]
|
||||
}
|
||||
|
||||
// Sort the attributes so the Teleport label is consistent even if AD
|
||||
// returns them in a different order.
|
||||
slices.Sort(values)
|
||||
|
||||
// At this point we can do an unconditional join, because
|
||||
// strings.Join is a no-op on a single-element slice.
|
||||
if value := strings.Join(values, cmp.Or(cfg.LabelAttributeJoinSeparator, "|")); len(value) > 0 && len(value) <= maxAttributeLabelLen {
|
||||
labels[types.DiscoveryLabelLDAPPrefix+attr] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +88,14 @@ func TestAppliesLDAPLabels(t *testing.T) {
|
||||
attrCommonName: {"foo"},
|
||||
"bar": {"baz"},
|
||||
"quux": {""},
|
||||
"multi": {"value1", "value2"},
|
||||
"empty": {"", ""},
|
||||
})
|
||||
|
||||
s := new(WindowsService)
|
||||
s.applyLabelsFromLDAP(entry, l, &servicecfg.LDAPDiscoveryConfig{
|
||||
BaseDN: "*",
|
||||
LabelAttributes: []string{"bar"},
|
||||
LabelAttributes: []string{"bar", "multi"},
|
||||
})
|
||||
|
||||
// check default labels
|
||||
@@ -109,6 +111,26 @@ func TestAppliesLDAPLabels(t *testing.T) {
|
||||
// check custom labels
|
||||
require.Equal(t, "baz", l["ldap/bar"])
|
||||
require.Empty(t, l["ldap/quux"])
|
||||
require.Equal(t, "value1", l["ldap/multi"]) // take first value by default
|
||||
|
||||
// Verify multi-valued attributes
|
||||
clear(l)
|
||||
s.applyLabelsFromLDAP(entry, l, &servicecfg.LDAPDiscoveryConfig{
|
||||
BaseDN: "*",
|
||||
LabelAttributes: []string{"multi", "empty"},
|
||||
LabelAttributeMode: "join",
|
||||
LabelAttributeJoinSeparator: "_",
|
||||
})
|
||||
require.Equal(t, "value1_value2", l["ldap/multi"])
|
||||
require.NotContains(t, l, "ldap/empty")
|
||||
|
||||
clear(l)
|
||||
s.applyLabelsFromLDAP(entry, l, &servicecfg.LDAPDiscoveryConfig{
|
||||
BaseDN: "*",
|
||||
LabelAttributes: []string{"multi"},
|
||||
LabelAttributeMode: "join",
|
||||
})
|
||||
require.Equal(t, "value1|value2", l["ldap/multi"]) // default separator
|
||||
}
|
||||
|
||||
func TestDNToDomain(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user