Auth Proxy incompatible with Google Workspace LDAP?

Hi there :wave:

I’ve got an instance of a Google Workspace LDAP server. We’re a small organisation, and it’s worked well for us in the past. We have had OpenVPN authenticating happily directly to Google’s LDAP and I thought we could put Duo’s auth proxy in the middle in order to get a 2FA prompt.

Google’s LDAP service is perhaps a little more complicated than others I’ve encountered. In order to use it correctly, you generate a “Client” and get a set of certificates that you use to connect that client. If you need the equivalent of a Service account, you generate some “Application Credentials”, which are valid only for that “Client”. If the certificate thing is incompatible with your client, you can utilise sTunnel as a proxy, which takes care of encryption/certificates for you and essentially allows clients to connect as if it’s regular unsecured ldap. I decided to use sTunnel for Duo just to take away that part of the equation.

So my diagram of what’s going on is:

User -> OpenVPN -> Duo Auth Proxy (LDAP) -> sTunnel -> Google

OpenVPN, Auth Proxy and sTunnel are all running on the same machine. My configuration file looks like:

[main]
debug=true

[ad_client]
host=127.0.0.1
port=1636
transport=clear
service_account_username=<service_account>
service_account_password=<service_account_password>
search_dn=dc=mydomain,dc=com
username_attribute=uid

[ldap_server_auto]
client=ad_client
ikey=XXXXX
skey=XXXXX
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
failmode=secure
exempt_primary_bind=false
; I know this isn't a real DN
exempt_ou_1=uid=<service_account>,dc=mydomain,dc=com

OpenVPN is set up without a set of bind credentials, because if I do put in the access credentials, I get:

2022-08-31T22:10:53.709096+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Starting factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7ff916cc5580>
2022-08-31T22:10:53.709826+0000 [duoauthproxy.lib.log#info] Connection made between client: X.X.X.X:42250 and the server section listening via X.X.X.X:389.
2022-08-31T22:10:53.710246+0000 [ldap_server_auto,3,X.X.X.X] S<-C LDAPMessage(id=1, value=LDAPBindRequest(version=3, dn='<service_account>', auth='****', sasl=False), controls=None)
2022-08-31T22:10:53.710500+0000 [ldap_server_auto,3,X.X.X.X] C->S LDAPMessage(id=20, value=LDAPBindRequest(version=3, dn='<service_account>', auth='****', sasl=False), controls=None)
2022-08-31T22:10:55.024839+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=20, value=LDAPBindResponse(resultCode=0, errorMessage='Valid access code'), controls=None)
2022-08-31T22:10:55.025257+0000 [duoauthproxy.lib.log#info] Bind Request did not have a full DN. Attempting to lookup full DN for <service_account>
2022-08-31T22:10:55.025446+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Starting factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7ff916cc8b20>
2022-08-31T22:10:55.026206+0000 [Uninitialized] C->S LDAPMessage(id=21, value=LDAPBindRequest(version=3, dn='', auth='****', sasl=True), controls=None)
2022-08-31T22:10:56.241469+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=21, value=LDAPBindResponse(resultCode=7), controls=None)
2022-08-31T22:10:56.242630+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Stopping factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7ff916cc8b20>

This is a bit of a pain. From what I can tell, Google don’t actually give you a bind DN for these service accounts, so I’ve removed the bind account for OpenVPN.

If I don’t have exempt_primary_bind = false, I get through no problems - except no 2FA prompt is spawned (and it says excluding primary bind). So I’ve set that to false. If that happens, I get these logs:

2022-08-31T08:52:19.442373+0000 [duoauthproxy.lib.log#info] {'■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■',
	 'client': 'ad_client',
	 'debug': 'True',
	 'exempt_ou_1': 'uid=<service_account>,dc=mydomain,dc=com',
	 'exempt_primary_bind': 'false',
	 'failmode': 'secure',
	 'ikey': '********',
	 'skey': '*****[40]'}
2022-08-31T08:52:19.442481+0000 [duoauthproxy.lib.log#info] SSL disabled. No server key and certificate configured.
2022-08-31T08:52:19.442580+0000 [duoauthproxy.lib.log#info] Duo Security Authentication Proxy 5.7.3 - Init Complete
2022-08-31T08:52:28.787470+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Starting factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f70fa1c70>
2022-08-31T08:52:28.789375+0000 [ldap_server_auto,0,X.X.X.X] S<-C LDAPMessage(id=1, value=LDAPSearchRequest(baseObject='dc=mydomain,dc=com', scope=2, derefAliases=0, sizeLimit=1024, timeLimit=60, typesOnly=0, filter=LDAPFilter_equalityMatch(attributeDesc=BEROctetString(value='uid'), assertionValue=BEROctetString(value='<authenticating_user>')), attributes=[]), controls=None)
2022-08-31T08:52:28.790080+0000 [duoauthproxy.lib.log#info] Connection made between client: X.X.X.X:54800 and the server section listening via X.X.X.X:389.
2022-08-31T08:52:28.790398+0000 [Uninitialized] C->S LDAPMessage(id=1, value=LDAPSearchRequest(baseObject='dc=mydomain,dc=com', scope=2, derefAliases=0, sizeLimit=1024, timeLimit=60, typesOnly=0, filter=LDAPFilter_equalityMatch(attributeDesc=BEROctetString(value='uid'), assertionValue=BEROctetString(value='<authenticating_user>')), attributes=[]), controls=None)
2022-08-31T08:52:30.432718+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=1, value=L■■■■■■■■■■■■■■■■■■■■(objectName='uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com', attributes=[('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount']), ('uid', ['<authenticating_user>']), <various_other_ldap_attributes>, controls=None)
2022-08-31T08:52:30.433110+0000 [_ADServiceClientProtocol,client] S->C LDAPMessage(id=1, value=L■■■■■■■■■■■■■■■■■■■■(objectName='uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com', attributes=[('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount']), ('uid', ['<authenticating_user>']), <various_other_ldap_attributes>, controls=None)
2022-08-31T08:52:30.433939+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=1, value=LDAPSearchResultDone(resultCode=0), controls=None)
2022-08-31T08:52:30.434169+0000 [_ADServiceClientProtocol,client] S->C LDAPMessage(id=1, value=LDAPSearchResultDone(resultCode=0), controls=None)
2022-08-31T08:52:30.434982+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Starting factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f6fbe9430>
2022-08-31T08:52:30.435902+0000 [ldap_server_auto,1,X.X.X.X] S<-C LDAPMessage(id=1, value=LDAPBindRequest(version=3, dn='uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com', auth='****', sasl=False), controls=None)
2022-08-31T08:52:30.436263+0000 [duoauthproxy.lib.log#info] Connection made between client: X.X.X.X:54806 and the server section listening via X.X.X.X:389.
2022-08-31T08:52:30.436469+0000 [Uninitialized] C->S LDAPMessage(id=2, value=LDAPBindRequest(version=3, dn='uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com', auth='****', sasl=False), controls=None)
2022-08-31T08:52:32.276913+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=2, value=LDAPBindResponse(resultCode=0), controls=None)
2022-08-31T08:52:32.277594+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Starting factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f6fbefa30>
2022-08-31T08:52:32.279149+0000 [Uninitialized] C->S LDAPMessage(id=3, value=LDAPBindRequest(version=3, dn='', auth='****', sasl=True), controls=None)
2022-08-31T08:52:33.512049+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=3, value=LDAPBindResponse(resultCode=7), controls=None)
2022-08-31T08:52:33.512599+0000 [duoauthproxy.lib.log#info] Failed to look up username for 2FA: 'authMethodNotSupported'
2022-08-31T08:52:33.512850+0000 [duoauthproxy.lib.log#info] [Request from X.X.X.X:54806] Cannot find username
2022-08-31T08:52:33.513103+0000 [_ADServiceClientProtocol,client] S->C LDAPMessage(id=1, value=LDAPBindResponse(resultCode=49, errorMessage='Cannot find username'), controls=None)
2022-08-31T08:52:33.513476+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Stopping factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f6fbefa30>
2022-08-31T08:52:33.514637+0000 [ldap_server_auto,1,X.X.X.X] S<-C LDAPMessage(id=2, value=LDAPUnbindRequest(), controls=None)
2022-08-31T08:52:33.514904+0000 [ldap_server_auto,1,X.X.X.X] C->S LDAPMessage(id=4, value=LDAPUnbindRequest(), controls=None)
2022-08-31T08:52:33.515234+0000 [ldap_server_auto,0,X.X.X.X] S<-C LDAPMessage(id=2, value=LDAPUnbindRequest(), controls=None)
2022-08-31T08:52:33.515431+0000 [ldap_server_auto,0,X.X.X.X] C->S LDAPMessage(id=5, value=LDAPUnbindRequest(), controls=None)
2022-08-31T08:52:33.515991+0000 [duoauthproxy.lib.log#info] Closing the connection between the downstream application and the Authentication Proxy. Reason: Connection was closed cleanly.
2022-08-31T08:52:33.516446+0000 [duoauthproxy.lib.log#info] Closing the connection between the downstream application and the Authentication Proxy. Reason: Connection was closed cleanly.
2022-08-31T08:52:33.516724+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Stopping factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f6fbe9430>
2022-08-31T08:52:33.517184+0000 [duoauthproxy.modules.ad_client._ADServiceClientFactory#info] Stopping factory <duoauthproxy.modules.ad_client._ADServiceClientFactory object at 0x7f4f70fa1c70>

From what I can see here, it binds with the authenticating user no problems. Then it tries and binds with a blank dn, and Google returns with AUTH_METHOD_NOT_SUPPORTED (Secure LDAP service: Error code descriptions - Google Workspace Admin Help). I kind of understand that because there is no DN for that service account - it’s just Google magic for that client.

As I started clutching at straws, I thought I’d use a real account in the Duo auth proxy instead of one of Google’s fake service accounts. I’d really prefer not to do this, as every account in Google costs $ per month, but I thought I’d try anyway.

The config was much the same, but I specified auth_type=plain and a bind_dn in addition to changing the service account user/pass.

The debug messages are the same up until the binding as a blank user. Logs are below: it gets further, but then it thinks it can’t find the uid of the authenticating user.

2022-09-03T00:54:28.965250+0000 [Uninitialized] C->S LDAPMessage(id=3, value=LDAPBindRequest(version=3, dn='uid=<a_real_account_instead_of_a_service_account>,ou=Users,dc=mydomain,dc=com', auth='****', sasl=False), controls=None)
2022-09-03T00:54:29.889899+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=3, value=LDAPBindResponse(resultCode=0), controls=None)
2022-09-03T00:54:29.890476+0000 [_ADServiceClientProtocol,client] C->S LDAPMessage(id=4, value=LDAPSearchRequest(baseObject='uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com', scope=2, derefAliases=0, sizeLimit=0, timeLimit=0, typesOnly=0, filter=LDAPFilter_and(value=[LDAPFilter_or(value=[LDAPFilter_and(value=[LDAPFilter_equalityMatch(attributeDesc=L■■■■■■■■■■■■■■■■■■■■ion(value='objectClass'), assertionValue=LDAPAssertionValue(value='user')), LDAPFilter_equalityMatch(attributeDesc=L■■■■■■■■■■■■■■■■■■■■ion(value='objectCategory'), assertionValue=LDAPAssertionValue(value='person'))]), LDAPFilter_equalityMatch(attributeDesc=L■■■■■■■■■■■■■■■■■■■■ion(value='objectClass'), assertionValue=LDAPAssertionValue(value='inetOrgPerson')), LDAPFilter_equalityMatch(attributeDesc=L■■■■■■■■■■■■■■■■■■■■ion(value='objectClass'), assertionValue=LDAPAssertionValue(value='organizationalPerson'))])]), attributes=('uid',)), controls=None)
2022-09-03T00:54:30.314894+0000 [_ADServiceClientProtocol,client] C<-S LDAPMessage(id=4, value=LDAPSearchResultDone(resultCode=0), controls=None)
2022-09-03T00:54:30.315077+0000 [duoauthproxy.lib.log#info] Could not find username searching with attribute uid
2022-09-03T00:54:30.315252+0000 [_ADServiceClientProtocol,client] C->S LDAPMessage(id=5, value=LDAPUnbindRequest(), controls=None)
2022-09-03T00:54:30.315635+0000 [duoauthproxy.lib.log#info] [Request from 172.16.1.59:33032] Cannot find username
2022-09-03T00:54:30.315840+0000 [_ADServiceClientProtocol,client] S->C LDAPMessage(id=1, value=LDAPBindResponse(resultCode=49, errorMessage='Cannot find username'), controls=None)

Google have an LDAP audit log, and I copied the query the auth proxy appears to be doing (and the base object, which from what I can tell is the authenticating user itself) and did an ldapsearch:

LDAPTLS_CIPHER_SUITE='NORMAL:!VERS-TLS1.3' LDAPTLS_CERT=/var/ldap-client.crt LDAPTLS_KEY=/var/ldap-client.key ldapsearch -H ldaps://ldap.google.com -b uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com '(&(|(&(objectClass=user)(objectCategory=person))(objectClass=inetOrgPerson)(objectClass=organizationalPerson)))'
SASL/EXTERNAL authentication started
SASL username: st=California,c=US,ou=GSuite,cn=LDAP Client,l=Mountain View,o=Google Inc.
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com> with scope subtree
# filter: (&(|(&(objectClass=user)(objectCategory=person))(objectClass=inetOrgPerson)(objectClass=organizationalPerson)))
# requesting: ALL
#

# <authenticating_user>, Users, mydomain.com
dn: uid=<authenticating_user>,ou=Users,dc=mydomain,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
uid: <authenticating_user>
googleUid: <authenticating_user>
posixUid: <authenticating_user>
cn: <authenticating_user>
...
mail: <authenticating_user>@mydomain.com
...
suspended: false
...
memberOf: cn=xxx,ou=Groups,dc=mydomain,dc=com
...
# search result
search: 3
result: 0 Success
# numResponses: 2
# numEntries: 1

As expected, the uid is definitely there. It’s also spewed out in the Auth proxy logs, although I notice there it’s represented as an array (eg. ['<authenticating_user>']) rather than a single string value. I also tried googleUid and posixUid as they’re all the same (and these all exist in Duo).

I also noticed the “SASL username” outputted as part of the LDAP search and tried to use that as the bind DN in various places (OpenVPN, Auth proxy etc) but alas no luck there.

Has anyone successfully used Duo Auth proxy with Google Workspace LDAP? Is it supported? Is anyone able to give me some pointers? I’d really like this to work.

Hello @pandaillusion, welcome to the Duo Community!

Google LDAP is not currently supported as an integration with Duo, but there are some configurations that might work. This Duo Community thread has more info and ideas. There might be an issue with access permissions, which is why we typically suggest binding as a service user.

For more detailed advice for your specific account, I would recommend reaching out to our support team and opening a support case. This post has further information and links.

I hope this helps!

-Lauren