Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for identities without userPrincipalName #44

Closed
mbrancato opened this issue Feb 22, 2020 · 20 comments
Closed

Support for identities without userPrincipalName #44

mbrancato opened this issue Feb 22, 2020 · 20 comments
Labels
enhancement New feature or request

Comments

@mbrancato
Copy link
Contributor

I've done some digging and it doesn't look like there is any way to lookup an authenticated object without a userPrincipalName attribute.

This is because if the upndomain is set to blank, this rejects all authentication:

if identity.Domain() != ldapCfg.ConfigEntry.UPNDomain {
w.WriteHeader(400)
_, _ = w.Write([]byte(fmt.Sprintf("identity domain of %q doesn't match LDAP upndomain of %q", identity.Domain(), ldapCfg.ConfigEntry.UPNDomain)))
return
}

if the upndomain is not blank, and using a binddn and password, this forces the search filter to use userPrincipalName:

if cfg.UPNDomain != "" {
filter = fmt.Sprintf("(userPrincipalName=%s@%s)", EscapeLDAPValue(username), cfg.UPNDomain)
}

Is there some way to support authenticating service accounts, or similar? For example, I'm trying to authenticate an account without userPrincipalName:

$ ldapsearch -H ldap://dc.domain.local -D [email protected] -w .... -b "DC=domain,DC=local" -LLL "(&(objectClass=user)(sAMAccountName=fancy_app))"
dn: CN=fancy_app,CN=Users,DC=domain,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: fancy_app
distinguishedName: CN=fancy_app,CN=Users,DC=domain,DC=local
instanceType: 4
whenCreated: 20200221063445.0Z
whenChanged: 20200221184555.0Z
uSNCreated: 13072
uSNChanged: 18822
name: fancy_app
objectGUID:: TZWHFhw9X0iw2oT1HURC2w==
userAccountControl: 512
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 0
lastLogoff: 0
lastLogon: 132268243395845360
pwdLastSet: 132267843550188649
primaryGroupID: 513
objectSid:: AQUAAAAAAAUVAAAAl8sz2f3jwaur0hP1UgQAAA==
accountExpires: 9223372036854775807
logonCount: 23
sAMAccountName: fancy_app
sAMAccountType: 805306368
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=domain,DC=local
dSCorePropagationData: 16010101000000.0Z
lastLogonTimestamp: 132267657119613902

Here you can see a successful authentication, but failed lookup as the forced filter to userPrincipalName:

Feb 22 06:50:51 vault vault[828]: 2020-02-22T06:50:51.525Z [INFO]  
auth.kerberos.auth_kerberos_f738412c.kerberos.vault-plugin-auth-kerberos: 192.168.1.15:8080 
[email protected] - SPNEGO authentication succeeded: timestamp=2020-02-22T06:50:51.525Z

Feb 22 06:50:51 vault vault[828]: 2020-02-22T06:50:51.526Z [DEBUG] 
auth.kerberos.auth_kerberos_f738412c.kerberos.vault-plugin-auth-kerberos: identity: &
{username:fancy_app displayName:fancy_app realm:DOMAIN.LOCAL cname:{NameType:1 
NameString:[fancy_app]} keytab:0xc000097060 password: 
attributes:map[gokrb5AttributeKeyADCredentials:{EffectiveName:fancy_app FullName: 
UserID:1106 PrimaryGroupID:513 LogOnTime:{wall:753296500 ext:63717907639 loc:<nil>} 
LogOffTime:{wall:709551516 ext:68937867273 loc:<nil>} PasswordLastSet:{wall:18864900 
ext:63717907555 loc:<nil>} GroupMembershipSIDs:[S-1-5-21-3644050327-2881610749-
4111717035-513 S-1-18-1] LogonDomainName:DOMAIN LogonDomainID:S-1-5-21-3644050327-
2881610749-4111717035 LogonServer:DC}] validUntil:{wall:0 ext:63717983539 loc:<nil>} 
authenticated:true human:true authTime:{wall:524843423 ext:63717951051 loc:<nil>} 
groupMembership:map[S-1-18-1:true S-1-5-21-3644050327-2881610749-4111717035-513:true] 
sessionID:f395d4e4-abaa-15e4-d662-0c7df16caa78}: timestamp=2020-02-22T06:50:51.525Z

Feb 22 06:50:51 vault vault[828]: 2020-02-22T06:50:51.530Z [DEBUG] 
auth.kerberos.auth_kerberos_f738412c.kerberos.vault-plugin-auth-kerberos: discovering user: 
filter=([email protected]) userdn=DC=domain,DC=local 
timestamp=2020-02-22T06:50:51.530Z

Feb 22 06:50:51 vault vault[828]: 2020-02-22T06:50:51.532Z [TRACE] 
auth.kerberos.auth_kerberos_f738412c.kerberos: handle request: transport=gRPC path=login 
status=finished err="unable to get user binddn: LDAP search for binddn 0 or not unique" 
took=9.467029ms
@mbrancato
Copy link
Contributor Author

I'm automating this user creation with Ansible and using the win_domain_user module. I can specify a UPN to get past this immediate block / specific use-case. However, thinking beyond this to pure SPNs and Computer accounts in Active Directory and others. The UPN is not required or typically does not exist in those objects. I think there is a long-term use-case here for things like Vault Agent once this is merged. Kerberos / AD Auth is currently not an auto-auth method for Vault Agent, but it becomes a possibility once this plugin is included in Vault. I can see valid use-cases to authenticate as the machine account / NetworkService as a secret zero solution. In this way, it would act very similarly to how other cloud provider identities work with instance identities.

@tyrannosaurus-becks
Copy link
Contributor

Hi! We made a change to this very recently. Can you please take a look and let us know what version of this plugin you're on? Is it from the recently released beta, or is it something built off master? If master, what version or commit? Thanks!

@mbrancato
Copy link
Contributor Author

Hi @tyrannosaurus-becks - I was building from master at the time using go get. I believe the commit was the one referenced above: b19d831cffac1888fd15a6b102859ed8709b7166

@tyrannosaurus-becks
Copy link
Contributor

Hi @mbrancato , thanks for the elaboration.

So, good news, when this is released as part of Vault 1.4 GA, the Kerberos auth method will be included in the agent. It's definitely included in the beta, which is currently out.

More to your point, I have two questions -

  1. Have you tried including the userPrincipalName but setting it to an empty string?
  2. If you have and it's still not working, can you post your LDAP configuration in Vault? (Sensitive values redacted.) I could use that to look through and further debug the issue.

@mbrancato
Copy link
Contributor Author

mbrancato commented Mar 7, 2020

sorry @tyrannosaurus-becks - where are you wanting me to set userPrincipalName to an empty string? Isn't it the plugin / ldaputil that is adding the userPrincipalName field to the search?

I thought you might be suggesting setting upndomain to an empty string in the ldap config.

If it is helpful, my POC is here:
https://github.com/mbrancato/vagrant-vault-kerberos

My current fix is to manually add a UPN to the account at creation here: https://github.com/mbrancato/vagrant-vault-kerberos/blob/2db52d31f6ec7818ee85cc466788f280ea641445/main.yml#L65

Also, will the vault agent support Windows hosts and AD similar to winkerberos in python?

@timotheencl
Copy link

timotheencl commented Mar 12, 2020

Hi same for me, I run an OpenLDAP server. The userPrincipalName field is not present in OpenLDAP for my binddn account, the field is krbPrincipalName instead

@tyrannosaurus-becks tyrannosaurus-becks added the enhancement New feature or request label Mar 13, 2020
@tyrannosaurus-becks
Copy link
Contributor

Thanks for the background, both of you! I've marked this as an enhancement for now.

@thomashashi
Copy link

One thing to be mindful of - people may be using LDAP schemas where there's no equivalent of userPrincipalName or krbPrincipalName in a user's entry at all, and simply assuming that a user's Kerberos principal name is something like their LDAP uid followed by @<LOCAL KERBEROS REALM NAME>

@optiz0r
Copy link
Contributor

optiz0r commented Apr 23, 2020

Yup, this affects me too where my userPrincipalName attribute is set to email address for integration with email provider. The changes in #47 solves the problem for me.

@tyrannosaurus-becks
Copy link
Contributor

I have a couple questions for the group. I'm looking at #47 and specifically that it allows one to bypass this check if the LDAP upnDomain is unset:

if identity.Domain() != ldapCfg.ConfigEntry.UPNDomain

My concern is, we added that line for security to prevent a Kerberos identity from a different domain being used to log in to a particular LDAP environment.

My questions are:

  1. Is there a different parameter you'd recommend we verify to avoid this potential attack?
  2. If there isn't, in your opinion, would it be sufficient to allow the check to be bypassed, but only if explicitly configured to be OK by a highly privileged person?

Thanks!

@optiz0r
Copy link
Contributor

optiz0r commented Apr 24, 2020

Hi @tyrannosaurus-becks,

In response to your questions:

Option 1

Keep the check but make it flexible for LDAP directories with different layouts:

Looking at the attributes on my LDAP users, there is no single attribute that has a value ${uid}@${realm} so merely allowing to configure some attribute other than userPrincipalName would not be enough without also changes to our domain.

If just the attribute checked were configurable, we could look at extending the AD schema with an additional attribute which replicates userPrincipalName for this check (or reuse one of the built-in localN attributes in the default AD schema). I'd prefer not to do this, and it would significantly raise the barrier to entry for other users.

There are various attributes which contain the required information in other forms, so could be used to replicate the check, but would require additional logic in the plugin to construct the correct values to match against.

  • sAMAccountName or uid containing the username part (e.g. usera)
  • distinguishedName ends with dc=realm,dc=example,dc=com matching the realm in lowercase (e.g. realm.example.com)
  • canonicalName begins with realm.example.com
  • msSFU30Name contains the username part, msSFU30NisDomain contains the shortened form of the realm name (e.g. REALM)
  • msDS-PrincipalName contains ${short_realm_name}\${uid} (e.g. REALM\usera)

I think the closest match is msDS-PrincipalName. The plugin would have to be aware of the short realm name as well as the fully qualified realm name (already captured by upndomain), and have a way to specify which pattern out of "{{.attribute}}={{.uid}}@{{.realm}}" or "{{.attribute}={{.short_realm_name}}{{.uid}}" to match against.

Alternative solution

  • A new configuration option named upnattr (or similar) which defaults to userPrincipalName
  • If the option is set in the form of a bareword, continue as before, construct the value {{.uid}}@{{.upndomain}} and match it against the value of this attribute from LDAP
  • If the option is set in the form attribute=value e.g. msDS-PrincipalName=REALM\{{.uid}}
    • treat the value as a template
    • interpolate in {{.uid}} with the username portion of the presented kerberos id
    • interpolate in {{.upndomain}} with the value of the upndomain attribute if present
    • match the resulting string against the value of the attribute from LDAP

This would allow the following cases:

  • The default case where userPrincipalName contains the kerberos identity (upnattr=UserPrincipalName)
  • A similar attribute exists but under another name (upnattr=differentUserPrincipalName)
  • The attribute is in a different format (upnattr=msDS-PrincpalName={{.upndomain}}\{{.uid}} with upndomain=REALM, or upnattr=msDS-PrincipalName=REALM\{{.uid}})
  • Disabling the check intentionally and allow users with the same name in any domain in the same forest to be treated the same (upnattr=uid={{.uid}})

Option 2

Optionally disabling the check:

If I understand correctly:

  • This check only has value in a multi-domain forest where the KDC will issue verifiable service tickets for multiple domains?
  • In a single-domain forest there's no risk of this, so the check adds no value and disabling the check would not be dangerous?
  • In either case, users cannot present spoofed service tickets for the same username in a completely different domain issued by a rogue KDC because they will not be decryptable by the vault plugin's keytab secret and so discarded before the LDAP checks take place

I can completely understand not wanting to disable this check by default to prevent administrators of multi-domain forests to make themselves less secure through inaction of not setting the upndomain attribute. Rather than allowing an empty upndomain to control the check, perhaps an extra option like ignore_upndomain which defaults to false for safety, but an administrator can set true would be a better approach?

Option 1 would be more secure and flexible. Option 2 would definitely be the easier way to go. Either one would work for us.

Regards,
Ben

@mbrancato
Copy link
Contributor Author

mbrancato commented Apr 24, 2020

Hi @tyrannosaurus-becks - when opening this, I didn't want to try and specify a solution as I'm not sure of all the LDAP helper interactions. Obviously changes to that have wider effects inside Vault and other plugins like LDAP auth.

Without regard to breaking changes in other use-cases, it sounds to me like the "option 1 - alternative solution" above is most aligned with what I was pointing out in the code below. If there was a flexible way to change how this filter is created, I do think there could be some "defaults" that help prevent breaking changes in other components.

if cfg.UPNDomain != "" {
filter = fmt.Sprintf("(userPrincipalName=%s@%s)", EscapeLDAPValue(username), cfg.UPNDomain)
}

I know the team inside HashiCorp probably has this planned out, but it would be good if there was some diagram of how the LDAP filter flow works. There have been some recent changes in the LDAP helper, and I know some breaking changes in the past. A flow of when a filter is applied and required fields might help make this more clear and help support the most LDAP implementations in a secure manner. Obviously, I think the checks for a single result are very important, so any filter changes would still probably need to call this out more explicitly in the docs for how to configure this.

Edit: I know I mention a flow - but maybe a truth table is a better way to represent when and what filters are applied and maybe other information.

@thomashashi
Copy link

I have a couple questions for the group. I'm looking at #47 and specifically that it allows one to bypass this check if the LDAP upnDomain is unset:

if identity.Domain() != ldapCfg.ConfigEntry.UPNDomain

My concern is, we added that line for security to prevent a Kerberos identity from a different domain being used to log in to a particular LDAP environment.

My questions are:

  1. Is there a different parameter you'd recommend we verify to avoid this potential attack?
  2. If there isn't, in your opinion, would it be sufficient to allow the check to be bypassed, but only if explicitly configured to be OK by a highly privileged person?

Thanks!

To be painfully specific, the Kerberos identity from a different realm is not logging into a particular LDAP server --- the caller's identity is never used to log into LDAP, only Vault binds to LDAP to look up directory information. It's a tiny distinction, but I wanted to be super clear.

In order for [email protected], who exists as a directory entry, say, cn=user,ou=Users,dc=malicious,dc=realm in the LDAP server for malicious.realm, to somehow get access to the same groups as [email protected], who exists as a directory entry, say, cn=user,ou=Users,dc=friendly,dc=realm in the LDAP server for friendly.realm, the following would have to happen:

  1. [email protected] would have to authenticate to a Vault server a the Kerberos auth method which can verify MALICIOUS.REALM tickets. This means that a) the Vault administrator has to have configured the Kerberos auth method to talk, and get a keytab which can be used, to talk to a KDC which can properly verify that a kerberos ticket presented for [email protected] is actually [email protected]. I.e. the Vault administrator trusts the KDC they want Vault to talk to.
    2a. The Vault administrator has to have configured that same Kerberos auth method to talk to the LDAP server for friendly.realm (not the LDAP server for malicious.realm). E.g. they, for some reason, trust a completely different LDAP server to have user directory information; or
    2b. The LDAP server the Vault administrator has configured the Kerberos auth method to talk to has to allow the user who holds the credentials for [email protected] to change the userPrincipalName attribute for cn=user,ou=Users,dc=friendly,dc=realm. That means the LDAP administrator has to trust users to change that attribute, which a) would be incredibly odd in my experience; or b) well, they trust the users to do that.

We assume that [email protected] cannot get a kerberos ticket which identifies them as [email protected]. If they can, we cannot do anything about that, because as far as Kerberos is concerned, any bearer of a valid ticket for [email protected] is [email protected].

I think it's useful to do that check, if it makes sense to the Vault administrator, I just want to understand what exactly we're protecting against and how exactly that could be exploited, which I'm not yet seeing - but I'm definitely open to understanding what I'm not seeing.

To be honest, I think a much simpler flow is:

  1. You define a userfilter which defines exactly how you want to find a user. That can access, say, {{krbUsername}}, {{krbRealm}}so you could do, say:
    a. (cn={{krbUsername}} (i.e. the common name for this user should be the first part of the Kerberos principal name, and either I don't care about the principal check, or my directory doesn't even contain that information)
    b. (&(cn={{krbUsername}})(userPrincipalName={{krbUsername}}@{{krbRealm})) (i.e. the common name for this user is the first part of the Kerberos principal name and the userPrincipalName attribute matches the supplied Kerberos principal)
    c (userPrincipalName={{krbUsername}}@{{krbRealm})) (i.e. simply look up the user by the userPrincipalName attribute)
    d. (krbPrincipalName={{krbUsername}}@{{krbRealm}} (i.e. simply look up the user by their Kerberos principal and I'm using a directory that isn't AD and that's what I call that attribute)
  2. If userfilter isn't defined, you don't even consult LDAP, and simply create a Entity/Entity Alias in the Vault Identity secret engine with reasonable name, and the Vault administrator can use the Identity secret engine to control which Vault policies this Entity gets.

This covers all use cases, allows the check if the Vault administrator wants it, and makes no assumptions at all about what's actually in the LDAP directory, how we want to find users there, or that you're even using an LDAP directory.

@tyrannosaurus-becks
Copy link
Contributor

Thank you, everyone, for that information and discussion. I'm going to see if we can get this incorporated into the linked PR.

@DrDaveD
Copy link
Contributor

DrDaveD commented Apr 24, 2020

That sounds like a good approach, and I think I could probably implement it if I am given more details and if it doesn't get too complicated.

Say some more about exactly how this would affect the existing configuration parameters.

Would userfilter be a kerberos/config option, and if it is not set, there would be no need to have an ldap configuration at all? Or would it be an ldap configuration option, and people could just leave out the ldap configuration completely if they didn't want the extra check? To be honest I have been quite mystified about why there's ldap configuration at all associated with the kerberos plugin.

So if people do want the ldap userfilter, which ldap config options will become obsolete? The set of options that I got to work with our ldap (and with #47 applied) was very simple, setting only url, userdn, userattr, and token_policies. I expect that userdn and userattr would be replaced by userfilter. What else would become obsolete? upndomain?

There will still need to be some way to select the token_policies so if we want to be able to completely leave out the ldap config then that would have to be associated with the base kerberos config.

@mbrancato
Copy link
Contributor Author

Yes @thomashashi - I think this generally solves the problems I was hitting. The userfilter that is enforced by the use of the LDAP helper. It seems like this solution would work, but may be a much bigger change.

AFAIK, the reason that LDAP is integrated is because Kerberos, as you point out, only provides an identity. We need this with humans, but mapping might be available thru group aliases as well.

@optiz0r
Copy link
Contributor

optiz0r commented Apr 24, 2020

The approach described by @thomashashi sounds good to me also, and would cover my use cases. We could setup a filter like (msDS-PrincipalName=REALM\{{krbUsername}}, using a string literal for the realm name without having to make any further changes to the directory and gaining the benefits the check provides in case we ever add a second realm in future.

To be honest I have been quite mystified about why there's ldap configuration at all associated with the kerberos plugin.

(Apologies for going off topic on a tangent, but helps clarify the direction we'd like kerberos auth to go in future, so a bit relevant here I think)

Same here. I do understand why it would have been done this way initially as a quicker way to implement kerberos support without needing core vault code changes.

AFAIK, the reason that LDAP is integrated is because Kerberos, as you point out, only provides an identity. We need this with humans, but mapping might be available thru group aliases as well.

It would be more useful for us if kerberos were provided via additional authentication methods on top of the LDAP backend rather than an entirely separate auth backend with duplicate configuration and entity aliases.

It complicates the identity external group mapping for us. I have users who might login via LDAP (e.g. vault webui, cli*) or via kerberos (production applications), and while the "identity" and group membership is sourced from the same place in both cases, because the external groups have to be sourced from one place we have to pick one of these two auth methods as the authoritative source. I have picked LDAP as the authoritative source but if a user logs in for the first time via kerberos, their group membership isn't sync'd into the identity system and they have no privileges. I therefore have to ask human users to login via LDAP at least once and then run a scheduled task that matches together the entity with the LDAP alias (with group memebrship) and the entity with the kerberos alias (without group membership), so that the next kerberos login has group membership. Our production users cannot login via LDAP (passwords are not known), so I'm having to duplicate policy mappings onto each production user manually rather than allowing this to match human users from LDAP group membership). It's all a bit messy.

  • For now, only because these don't support reusing an existing TGT from the user's environment. We intend to open tickets/issues and if we can contribute PRs to add kerberos login to the webui, and to extend the cli login method reuse existing TGT rather than using a keytab to create a new TGT, after which we can probably drop the LDAP login method entirely in favour of the kerberos method. Some other applications (e.g. apache mod_auth_kerb/mod_auth_gssapi) also support password authentication by taking a password and doing the check serverside for clients that don't support doing the kerberos on the client side. If similar were added to the kerberos auth backend (either by acquiring a TGT server side using the password, or just falling back to standard LDAP bind) also then it could even replace the ldap backend entirely?

@thomashashi
Copy link

  • For now, only because these don't support reusing an existing TGT from the user's environment. We intend to open tickets/issues and if we can contribute PRs to add kerberos login to the webui, and to extend the cli login method reuse existing TGT rather than using a keytab to create a new TGT, after which we can probably drop the LDAP login method entirely in favour of the kerberos method. [...]

I looked a little into this when I was doing some internal training for folks, and for personal use - it appears that it would be rather difficult to add this to the Vault CLI because the gokrb5 library is a pure Golang implementation of Kerberos, and while the format/access to keytab files is relatively standard, accessing the different ccaches the various Kerberos implementations have isn't. Other languages get around this by simply wrapping the relevant C/C++/Whatever Kerberos libraries; pure Golang here doesn't.

The Vault Kerberos auth method, however, doesn't care how the client does SPNEGO negotiation to get the blob is sends it, only that it has. It was pretty easy to take the exemplar Python script and make it read a ccache:

#!/usr/bin/env python
import kerberos
import requests

service = "vault/vault.example.local"
rc, vc = kerberos.authGSSClientInit(service=service, mech_oid=kerberos.GSS_MECH_OID_SPNEGO)
kerberos.authGSSClientStep(vc, "")
kerberos_token = kerberos.authGSSClientResponse(vc)

r = requests.post("http://127.0.0.1:8200/v1/auth/kerberos/login",
                  headers={"Authorization": 'Negotiate ' + kerberos_token})
print(r.text)

Replace the service with the service principal in the Vault keytab, and the http://127.0.0.1:8200 with your actual Vault server address.

@optiz0r
Copy link
Contributor

optiz0r commented Apr 24, 2020

I looked a little into this when I was doing some internal training for folks, and for personal use - it appears that it would be rather difficult to add this to the Vault CLI because the gokrb5 library is a pure Golang implementation of Kerberos, and while the format/access to keytab files is relatively standard, accessing the different ccaches the various Kerberos implementations have isn't.

OK, that's useful to know, thanks! Perhaps for the cli we'll stick to a python script as an alternative to the cli login. I imagine kerberos login for webui should be more straightforward.

@tyrannosaurus-becks
Copy link
Contributor

Thought about this more and decided to approve and merge the linked PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants