diff --git a/cmd/config-current.go b/cmd/config-current.go index 40b63d2ba..d469b7ff8 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -273,9 +273,19 @@ func validateConfig(s config.Config) error { return err } - if _, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default], - globalRootCAs); err != nil { - return err + { + cfg, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default], + globalRootCAs) + if err != nil { + return err + } + if cfg.Enabled { + conn, cerr := cfg.Connect() + if cerr != nil { + return cerr + } + conn.Close() + } } if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], diff --git a/cmd/config/identity/ldap/config.go b/cmd/config/identity/ldap/config.go index 933176228..9b8a12e27 100644 --- a/cmd/config/identity/ldap/config.go +++ b/cmd/config/identity/ldap/config.go @@ -21,7 +21,8 @@ import ( "crypto/x509" "errors" "fmt" - "regexp" + "net" + "strings" "time" "github.com/minio/minio/cmd/config" @@ -44,11 +45,15 @@ type Config struct { STSExpiryDuration string `json:"stsExpiryDuration"` // Format string for usernames - UsernameFormat string `json:"usernameFormat"` + UsernameFormat string `json:"usernameFormat"` + UsernameFormats []string `json:"-"` + UsernameSearchFilter string `json:"-"` + UsernameSearchBaseDNS []string `json:"-"` - GroupSearchBaseDN string `json:"groupSearchBaseDN"` - GroupSearchFilter string `json:"groupSearchFilter"` - GroupNameAttribute string `json:"groupNameAttribute"` + GroupSearchBaseDN string `json:"groupSearchBaseDN"` + GroupSearchBaseDNS []string `json:"-"` + GroupSearchFilter string `json:"groupSearchFilter"` + GroupNameAttribute string `json:"groupNameAttribute"` stsExpiryDuration time.Duration // contains converted value tlsSkipVerify bool // allows skipping TLS verification @@ -58,23 +63,27 @@ type Config struct { // LDAP keys and envs. const ( - ServerAddr = "server_addr" - STSExpiry = "sts_expiry" - UsernameFormat = "username_format" - GroupSearchFilter = "group_search_filter" - GroupNameAttribute = "group_name_attribute" - GroupSearchBaseDN = "group_search_base_dn" - TLSSkipVerify = "tls_skip_verify" - ServerInsecure = "server_insecure" - - EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR" - EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY" - EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY" - EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE" - EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT" - EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER" - EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE" - EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN" + ServerAddr = "server_addr" + STSExpiry = "sts_expiry" + UsernameFormat = "username_format" + UsernameSearchFilter = "username_search_filter" + UsernameSearchBaseDN = "username_search_base_dn" + GroupSearchFilter = "group_search_filter" + GroupNameAttribute = "group_name_attribute" + GroupSearchBaseDN = "group_search_base_dn" + TLSSkipVerify = "tls_skip_verify" + ServerInsecure = "server_insecure" + + EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR" + EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY" + EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY" + EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE" + EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT" + EnvUsernameSearchFilter = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER" + EnvUsernameSearchBaseDN = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN" + EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER" + EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE" + EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN" ) // DefaultKVS - default config for LDAP config @@ -85,11 +94,15 @@ var ( Value: "", }, config.KV{ - Key: STSExpiry, - Value: "1h", + Key: UsernameFormat, + Value: "", }, config.KV{ - Key: UsernameFormat, + Key: UsernameSearchFilter, + Value: "", + }, + config.KV{ + Key: UsernameSearchBaseDN, Value: "", }, config.KV{ @@ -104,6 +117,10 @@ var ( Key: GroupSearchBaseDN, Value: "", }, + config.KV{ + Key: STSExpiry, + Value: "1h", + }, config.KV{ Key: TLSSkipVerify, Value: config.EnableOff, @@ -115,16 +132,131 @@ var ( } ) +const ( + dnDelimiter = ";" +) + +func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) { + var groups []string + sres, err := conn.Search(sreq) + if err != nil { + return nil, err + } + for _, entry := range sres.Entries { + // We only queried one attribute, + // so we only look up the first one. + groups = append(groups, entry.Attributes[0].Values...) + } + return groups, nil +} + +func (l *Config) bind(conn *ldap.Conn, username, password string) ([]string, error) { + var bindDNS = make([]string, len(l.UsernameFormats)) + for i, usernameFormat := range l.UsernameFormats { + bindDN := fmt.Sprintf(usernameFormat, username) + // Bind with user credentials to validate the password + if err := conn.Bind(bindDN, password); err != nil { + return nil, err + } + bindDNS[i] = bindDN + } + return bindDNS, nil +} + +var standardAttributes = []string{ + "givenName", + "sn", + "cn", + "memberOf", + "email", +} + +// Bind - binds to ldap, searches LDAP and returns list of groups. +func (l *Config) Bind(username, password string) ([]string, error) { + conn, err := l.Connect() + if err != nil { + return nil, err + } + defer conn.Close() + + bindDNS, err := l.bind(conn, username, password) + if err != nil { + return nil, err + } + + var groups []string + if l.UsernameSearchFilter != "" { + for _, userSearchBase := range l.UsernameSearchBaseDNS { + filter := strings.Replace(l.UsernameSearchFilter, "%s", + ldap.EscapeFilter(username), -1) + + searchRequest := ldap.NewSearchRequest( + userSearchBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + standardAttributes, + nil, + ) + + groups, err = getGroups(conn, searchRequest) + if err != nil { + return nil, err + } + } + } + + if l.GroupSearchFilter != "" { + for _, groupSearchBase := range l.GroupSearchBaseDNS { + var filters []string + if l.GroupNameAttribute == "" { + filters = []string{strings.Replace(l.GroupSearchFilter, "%s", + ldap.EscapeFilter(username), -1)} + } else { + // With group name attribute specified, make sure to + // include search queries for CN distinguished name + for _, bindDN := range bindDNS { + filters = append(filters, strings.Replace(l.GroupSearchFilter, "%s", + ldap.EscapeFilter(bindDN), -1)) + } + } + for _, filter := range filters { + searchRequest := ldap.NewSearchRequest( + groupSearchBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + standardAttributes, + nil, + ) + + var newGroups []string + newGroups, err = getGroups(conn, searchRequest) + if err != nil { + return nil, err + } + + groups = append(groups, newGroups...) + } + } + } + + return groups, nil +} + // Connect connect to ldap server. func (l *Config) Connect() (ldapConn *ldap.Conn, err error) { if l == nil { - // Happens when LDAP is not configured. - return + return nil, errors.New("LDAP is not configured") + } + + if _, _, err = net.SplitHostPort(l.ServerAddr); err != nil { + // User default LDAP port if none specified "636" + l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636") } if l.serverInsecure { return ldap.Dial("tcp", l.ServerAddr) } + return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{ InsecureSkipVerify: l.tlsSkipVerify, RootCAs: l.rootCAs, @@ -178,102 +310,44 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) { } } if v := env.Get(EnvUsernameFormat, kvs.Get(UsernameFormat)); v != "" { - subs, err := NewSubstituter("username", "test") - if err != nil { - return l, err - } - if _, err := subs.Substitute(v); err != nil { - return l, err + if !strings.Contains(v, "%s") { + return l, errors.New("LDAP username format doesn't have '%s' substitution") } - l.UsernameFormat = v + l.UsernameFormats = strings.Split(v, dnDelimiter) } else { return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat) } + if v := env.Get(EnvUsernameSearchFilter, kvs.Get(UsernameSearchFilter)); v != "" { + if !strings.Contains(v, "%s") { + return l, errors.New("LDAP username search filter doesn't have '%s' substitution") + } + l.UsernameSearchFilter = v + } + + if v := env.Get(EnvUsernameSearchBaseDN, kvs.Get(UsernameSearchBaseDN)); v != "" { + l.UsernameSearchBaseDNS = strings.Split(v, dnDelimiter) + } + grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter)) grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute)) grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN)) // Either all group params must be set or none must be set. - allNotSet := grpSearchFilter == "" && grpSearchNameAttr == "" && grpSearchBaseDN == "" - allSet := grpSearchFilter != "" && grpSearchNameAttr != "" && grpSearchBaseDN != "" - if !allNotSet && !allSet { - return l, errors.New("All group related parameters must be set") + var allSet bool + if grpSearchFilter != "" { + if grpSearchNameAttr == "" || grpSearchBaseDN == "" { + return l, errors.New("All group related parameters must be set") + } + allSet = true } if allSet { - subs, err := NewSubstituter("username", "test", "usernamedn", "test2") - if err != nil { - return l, err - } - if _, err := subs.Substitute(grpSearchFilter); err != nil { - return l, fmt.Errorf("Only username and usernamedn may be substituted in the group search filter string: %s", err) - } l.GroupSearchFilter = grpSearchFilter l.GroupNameAttribute = grpSearchNameAttr - subs, err = NewSubstituter("username", "test", "usernamedn", "test2") - if err != nil { - return l, err - } - if _, err := subs.Substitute(grpSearchBaseDN); err != nil { - return l, fmt.Errorf("Only username and usernamedn may be substituted in the base DN string: %s", err) - } - l.GroupSearchBaseDN = grpSearchBaseDN + l.GroupSearchBaseDNS = strings.Split(grpSearchBaseDN, dnDelimiter) } l.rootCAs = rootCAs return l, nil } - -// Substituter - This type is to allow restricted runtime -// substitutions of variables in LDAP configuration items during -// runtime. -type Substituter struct { - vals map[string]string -} - -// NewSubstituter - sets up the substituter for usage, for e.g.: -// -// subber := NewSubstituter("username", "john") -func NewSubstituter(v ...string) (Substituter, error) { - if len(v)%2 != 0 { - return Substituter{}, errors.New("Need an even number of arguments") - } - vals := make(map[string]string) - for i := 0; i < len(v); i += 2 { - vals[v[i]] = v[i+1] - } - return Substituter{vals: vals}, nil -} - -// Substitute - performs substitution on the given string `t`. Returns -// an error if there are any variables in the input that do not have -// values in the substituter. E.g.: -// -// subber.Substitute("uid=${username},cn=users,dc=example,dc=com") -// -// or -// -// subber.Substitute("uid={username},cn=users,dc=example,dc=com") -// -// returns "uid=john,cn=users,dc=example,dc=com" -// -// whereas: -// -// subber.Substitute("uid=${usernamedn}") -// -// returns an error. -func (s *Substituter) Substitute(t string) (string, error) { - for k, v := range s.vals { - reDollar := regexp.MustCompile(fmt.Sprintf(`\$\{%s\}`, k)) - t = reDollar.ReplaceAllLiteralString(t, v) - reFlower := regexp.MustCompile(fmt.Sprintf(`\{%s\}`, k)) - t = reFlower.ReplaceAllLiteralString(t, v) - } - // Check if all requested substitutions have been made. - re := regexp.MustCompile(`\{.*\}`) - if re.MatchString(t) { - return "", errors.New("unsupported substitution requested") - } - return t, nil -} diff --git a/cmd/config/identity/ldap/config_test.go b/cmd/config/identity/ldap/config_test.go deleted file mode 100644 index dd0f93f4c..000000000 --- a/cmd/config/identity/ldap/config_test.go +++ /dev/null @@ -1,93 +0,0 @@ -/* - * MinIO Cloud Storage, (C) 2019 MinIO, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ldap - -import ( - "testing" -) - -func TestSubstituter(t *testing.T) { - tests := []struct { - KV []string - SubstitutableStr string - SubstitutedStr string - ErrExpected bool - }{ - { - KV: []string{"username", "john"}, - SubstitutableStr: "uid=${username},cn=users,dc=example,dc=com", - SubstitutedStr: "uid=john,cn=users,dc=example,dc=com", - ErrExpected: false, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "uid={username},cn=users,dc=example,dc=com", - SubstitutedStr: "uid=john,cn=users,dc=example,dc=com", - ErrExpected: false, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "(&(objectclass=group)(member=${username}))", - SubstitutedStr: "(&(objectclass=group)(member=john))", - ErrExpected: false, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "(&(objectclass=group)(member={username}))", - SubstitutedStr: "(&(objectclass=group)(member=john))", - ErrExpected: false, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "uid=${{username}},cn=users,dc=example,dc=com", - ErrExpected: true, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com", - ErrExpected: true, - }, - { - KV: []string{"username"}, - SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com", - ErrExpected: true, - }, - { - KV: []string{"username", "john"}, - SubstitutableStr: "(&(objectclass=user)(sAMAccountName={username})(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))", - SubstitutedStr: "(&(objectclass=user)(sAMAccountName=john)(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))", - ErrExpected: false, - }, - } - - for _, test := range tests { - test := test - t.Run(test.SubstitutableStr, func(t *testing.T) { - subber, err := NewSubstituter(test.KV...) - if err != nil && !test.ErrExpected { - t.Errorf("Unexpected failure %s", err) - } - gotStr, err := subber.Substitute(test.SubstitutableStr) - if err != nil && !test.ErrExpected { - t.Errorf("Unexpected failure %s", err) - } - if gotStr != test.SubstitutedStr { - t.Errorf("Expected %s, got %s", test.SubstitutedStr, gotStr) - } - }) - } -} diff --git a/cmd/config/identity/ldap/help.go b/cmd/config/identity/ldap/help.go index 879ffb859..b3d14b0ce 100644 --- a/cmd/config/identity/ldap/help.go +++ b/cmd/config/identity/ldap/help.go @@ -28,24 +28,33 @@ var ( }, config.HelpKV{ Key: UsernameFormat, - Description: `username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`, + Description: `";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`, + Type: "list", + }, + config.HelpKV{ + Key: UsernameSearchFilter, + Description: `user search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"`, Type: "string", }, config.HelpKV{ Key: GroupSearchFilter, Description: `search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"`, - Optional: true, Type: "string", }, config.HelpKV{ - Key: GroupNameAttribute, - Description: `search attribute for group name e.g. "cn"`, + Key: GroupSearchBaseDN, + Description: `";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"`, + Type: "list", + }, + config.HelpKV{ + Key: UsernameSearchBaseDN, + Description: `";" separated list of username search DNs`, + Type: "list", Optional: true, - Type: "string", }, config.HelpKV{ - Key: GroupSearchBaseDN, - Description: `group search base DNs e.g. "dc=myldapserver,dc=com"`, + Key: GroupNameAttribute, + Description: `search attribute for group name e.g. "cn"`, Optional: true, Type: "string", }, @@ -63,7 +72,7 @@ var ( }, config.HelpKV{ Key: ServerInsecure, - Description: `allow plain text connection to AD/LDAP server, defaults to "off" (TLS)`, + Description: `allow plain text connection to AD/LDAP server, defaults to "off"`, Optional: true, Type: "on|off", }, diff --git a/cmd/iam.go b/cmd/iam.go index ff2a2a24a..7212587a4 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -369,6 +369,10 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error { return errServerNotInitialized } + if globalLDAPConfig.Enabled { + sys.EnableLDAPSys() + } + sys.Lock() if globalEtcdClient == nil { sys.store = newIAMObjectStore() @@ -1791,22 +1795,18 @@ func (sys *IAMSys) removeGroupFromMembershipsMap(group string) { } } +// EnableLDAPSys - enable ldap system users type. +func (sys *IAMSys) EnableLDAPSys() { + sys.Lock() + defer sys.Unlock() + + sys.usersSysType = LDAPUsersSysType +} + // NewIAMSys - creates new config system object. func NewIAMSys() *IAMSys { - // Check global server configuration to determine the type of - // users system configured. - - // The default users system - var utype UsersSysType - switch { - case globalLDAPConfig.Enabled: - utype = LDAPUsersSysType - default: - utype = MinIOUsersSysType - } - return &IAMSys{ - usersSysType: utype, + usersSysType: MinIOUsersSysType, iamUsersMap: make(map[string]auth.Credentials), iamPolicyDocsMap: make(map[string]iampolicy.Policy), iamUserPolicyMap: make(map[string]MappedPolicy), diff --git a/cmd/sts-handlers.go b/cmd/sts-handlers.go index 7d5557b5e..0cea13b05 100644 --- a/cmd/sts-handlers.go +++ b/cmd/sts-handlers.go @@ -24,14 +24,12 @@ import ( "net/http" "github.com/gorilla/mux" - xldap "github.com/minio/minio/cmd/config/identity/ldap" "github.com/minio/minio/cmd/config/identity/openid" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/wildcard" - ldap "gopkg.in/ldap.v3" ) const ( @@ -477,61 +475,13 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r * } } - ldapConn, err := globalLDAPConfig.Connect() + groups, err := globalLDAPConfig.Bind(ldapUsername, ldapPassword) if err != nil { - writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("LDAP server connection failure: %w", err)) - return - } - if ldapConn == nil { - writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("LDAP server not configured: %w", err)) - return - } - - // Close ldap connection to avoid leaks. - defer ldapConn.Close() - - usernameSubs, _ := xldap.NewSubstituter("username", ldapUsername) - // We ignore error below as we already validated the username - // format string at startup. - usernameDN, _ := usernameSubs.Substitute(globalLDAPConfig.UsernameFormat) - // Bind with user credentials to validate the password - if err = ldapConn.Bind(usernameDN, ldapPassword); err != nil { - err = fmt.Errorf("LDAP authentication failure: %w", err) + err = fmt.Errorf("LDAP server connection failure: %w", err) writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) return } - groups := []string{} - if globalLDAPConfig.GroupSearchFilter != "" { - // Verified user credentials. Now we find the groups they are - // a member of. - searchSubs, _ := xldap.NewSubstituter( - "username", ldapUsername, - "usernamedn", usernameDN, - ) - // We ignore error below as we already validated the search string - // at startup. - groupSearchFilter, _ := searchSubs.Substitute(globalLDAPConfig.GroupSearchFilter) - baseDN, _ := searchSubs.Substitute(globalLDAPConfig.GroupSearchBaseDN) - searchRequest := ldap.NewSearchRequest( - baseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - groupSearchFilter, - []string{globalLDAPConfig.GroupNameAttribute}, - nil, - ) - - sr, err := ldapConn.Search(searchRequest) - if err != nil { - writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("LDAP search failure: %w", err)) - return - } - for _, entry := range sr.Entries { - // We only queried one attribute, so we only look up - // the first one. - groups = append(groups, entry.Attributes[0].Values...) - } - } expiryDur := globalLDAPConfig.GetExpiryDuration() m := map[string]interface{}{ expClaim: UTCNow().Add(expiryDur).Unix(), diff --git a/docs/sts/ldap.md b/docs/sts/ldap.md index 9a3435e9b..39aaf44fa 100644 --- a/docs/sts/ldap.md +++ b/docs/sts/ldap.md @@ -38,45 +38,45 @@ LDAP configuration is designed to be simple for the MinIO administrator. The ful MinIO can be configured to find the groups of a user from AD/LDAP by specifying the **MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER** and **MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE** environment variables. When a user logs in via the STS API, the MinIO server queries the AD/LDAP server with the given search filter and extracts the given attribute from the search results. These values represent the groups that the user is a member of. On each access MinIO applies the IAM policies attached to these groups in MinIO. +MinIO sends LDAP credentials to LDAP server for validation. So we _strongly recommend_ to use MinIO with AD/LDAP server over TLS _only_. Using plain-text connection between MinIO and LDAP server means _credentials can be compromised_ by anyone listening to network traffic. + LDAP is configured via the following environment variables: -| Variable | Required? | Purpose | -|----------------------------------------------|-------------------------|-------------------------------------------------------------------------| -| **MINIO_IDENTITY_LDAP_SERVER_ADDR** | **YES** | AD/LDAP server address | -| **MINIO_IDENTITY_LDAP_USERNAME_FORMAT** | **YES** | Format of full username DN | -| **MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN** | **NO** | Base DN in AD/LDAP hierarchy to use in search requests | -| **MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER** | **NO** | Search filter to find groups of a user | -| **MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE** | **NO** | Attribute of search results to use as group name | -| **MINIO_IDENTITY_LDAP_STS_EXPIRY** | **NO** (default: "1h") | STS credentials validity duration | -| **MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY** | **NO** (default: "off") | Set this to 'on', to disable client verification of server certificates | -| **MINIO_IDENTITY_LDAP_SERVER_INSECURE** | **NO** (default: "off") | Set this to 'on', to allow plain text connection to LDAP/AD Server (only for testing) | +``` +$ mc admin config set myminio/ identity_ldap --env +KEY: +identity_ldap enable LDAP SSO support + +ARGS: +MINIO_IDENTITY_LDAP_SERVER_ADDR* (address) AD/LDAP server address e.g. "myldapserver.com:636" +MINIO_IDENTITY_LDAP_USERNAME_FORMAT* (list) ";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com" +MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER* (string) user search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" +MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER* (string) search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))" +MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN* (list) ";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com" +MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN (list) ";" separated list of username search DNs +MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE (string) search attribute for group name e.g. "cn" +MINIO_IDENTITY_LDAP_STS_EXPIRY (duration) temporary credentials validity duration in s,m,h,d. Default is "1h" +MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY (on|off) trust server TLS without verification, defaults to "off" (verify) +MINIO_IDENTITY_LDAP_SERVER_INSECURE (on|off) allow plain text connection to AD/LDAP server, defaults to "off" +MINIO_IDENTITY_LDAP_COMMENT (sentence) optionally add a comment to this setting +``` MinIO sends LDAP credentials to LDAP server for validation. So we _strongly recommend_ to use MinIO with AD/LDAP server over TLS _only_. Using plain-text connection between MinIO and LDAP server means _credentials can be compromised_ by anyone listening to network traffic. If a self-signed certificate is being used, the certificate can be added to MinIO's certificates directory, so it can be trusted by the server. An example setup for development or experimentation: -``` shell +```shell export MINIO_IDENTITY_LDAP_SERVER_ADDR=myldapserver.com:636 -export MINIO_IDENTITY_LDAP_USERNAME_FORMAT="uid={username},cn=accounts,dc=myldapserver,dc=com" +export MINIO_IDENTITY_LDAP_USERNAME_FORMAT="uid=%s,cn=accounts,dc=myldapserver,dc=com" export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="dc=myldapserver,dc=com" -export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member={usernamedn})$)" -export MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE="cn" +export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(memberUid=%s)$)" +export MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE=cn export MINIO_IDENTITY_LDAP_STS_EXPIRY=60h -export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY="on" +export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on ``` ### Variable substitution in AD/LDAP configuration strings - -In the configuration values described above, some values support runtime substitutions. The substitution syntax is simply `${variable}` - this substring is replaced with the (string) value of `variable`. The following substitutions will be available: - -| Variable | Example Runtime Value | Description | -|--------------|------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| *username* | "james" | The AD/LDAP username of a user. | -| *usernamedn* | "uid=james,cn=accounts,dc=myldapserver,dc=com" | The AD/LDAP username DN of a user. This is constructed from the AD/LDAP user DN format string provided to the server and the actual AD/LDAP username. | - -The **MINIO_IDENTITY_LDAP_USERNAME_FORMAT** environment variable supports substitution of the *username* variable only. - -The **MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER** and **MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN** environment variables support substitution of the *username* and *usernamedn* variables only. +`%s` is replaced with *username* automatically for construction bind_dn, search_filter and group_search_filter. ### Notes on configuring with Microsoft Active Directory (AD) @@ -102,19 +102,19 @@ member: CN=John,CN=Users,DC=minioad,DC=local ... ``` -The lines with "..." represent skipped content not shown here from brevity. Based on the output above, we see that the username format variable looks like `cn={username},cn=users,dc=minioad,dc=local`. +The lines with "..." represent skipped content not shown here from brevity. Based on the output above, we see that the username format variable looks like `cn=%s,cn=users,dc=minioad,dc=local`. -The group search filter looks like `(&(objectclass=group)(member={usernamedn}))` and the group name attribute is clearly `cn`. +The group search filter looks like `(&(objectclass=group)(memberUid=%s))` and the group name attribute is clearly `cn`. Thus the key configuration parameters look like: ``` MINIO_IDENTITY_LDAP_SERVER_ADDR='my.ldap-active-dir-server.com:636' -MINIO_IDENTITY_LDAP_USERNAME_FORMAT='cn={username},cn=users,dc=minioad,dc=local' +MINIO_IDENTITY_LDAP_USERNAME_FORMAT='cn=%s,ou=Users,ou=BUS1,ou=LOB,dc=somedomain,dc=com' MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN='dc=minioad,dc=local' -MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER='(&(objectclass=group)(member={usernamedn}))' +MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER='(&(objectclass=group)(member=%s))' MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE='cn' -MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY="on" +MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on ``` ## Managing User/Group Access Policy @@ -212,13 +212,15 @@ http://minio.cluster:9000?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=foouser ``` ## Testing + +With multiple OU heirarchies for users, and multiple group search base DN's. ``` $ export MINIO_ACCESS_KEY=minio $ export MINIO_SECRET_KEY=minio123 $ export MINIO_IDENTITY_LDAP_SERVER_ADDR='my.ldap-active-dir-server.com:636' -$ export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='cn={username},cn=users,dc=minioad,dc=local' -$ export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN='dc=minioad,dc=local' -$ export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER='(&(objectclass=group)(member={usernamedn}))' +$ export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='cn=%s,ou=Users,ou=BUS1,ou=LOB,dc=somedomain,dc=com;cn=%s,ou=Users,ou=BUS2,ou=LOB,dc=somedomain,dc=com' +$ export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN='dc=minioad,dc=local;dc=somedomain,dc=com' +$ export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER='(&(objectclass=group)(member=%s))' $ export MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE='cn' $ minio server ~/test ```