Google LDAP auth

729 views
Skip to first unread message

Krzysztof Toczyski

unread,
Jan 20, 2022, 8:24:55 AM1/20/22
to NetBox
Hi,

I try connect Netbox (v3.0.12) to Google LDAP via stunnel.

My configs:

cat ldap_config.py
 
import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType, NestedGroupOfNamesType


# Baseline configuration.
AUTH_LDAP_SERVER_URI = "ldap://localhost:1636"

AUTH_LDAP_BIND_DN = "cn=USER"
AUTH_LDAP_BIND_PASSWORD = "PASS"
AUTH_LDAP_PROTOCOL_VERSION = 3
#AUTH_LDAP_USER_SEARCH = LDAPSearch(
#    "ou=Users,dc=XXX,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=posixAccount)(memberOf=cn=sa,ou=Groups,dc=XXX,dc=com)"
#)
# Or:
AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=Users,dc=XXX,dc=com'

# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    "ou=Groups,dc=XXX,dc=com",
    ldap.SCOPE_SUBTREE,
    "(objectClass=groupOfNames)",
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")

# Simple group restrictions
AUTH_LDAP_REQUIRE_GROUP = "cn=sa,ou=Groups,dc=XXX,dc=com"
#AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=Groups,dc=XXX,dc=com"

# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail",
}

AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    "is_active": "cn=sa,ou=Groups,dc=XXX,dc=com",
    "is_staff": "cn=sa,ou=Groups,dc=XXX,dc=com",
    "is_superuser": "cn=sa,ou=Groups,dc=XXX,dc=com",
}

# This is the default, but I like to be explicit.
AUTH_LDAP_ALWAYS_UPDATE_USER = False

# Use LDAP group membership to calculate group permissions.
AUTH_LDAP_FIND_GROUP_PERMS = True

# Cache distinguished names and group memberships for an hour to minimize
# LDAP traffic.
AUTH_LDAP_CACHE_TIMEOUT = 3600

# Keep ModelBackend around for per-user permissions and maybe a local
# superuser.
#AUTHENTICATION_BACKENDS = (
#    "django_auth_ldap.backend.LDAPBackend",
#    "django.contrib.auth.backends.ModelBackend",

configuration.py

# Remote authentication support
REMOTE_AUTH_ENABLED = False
#REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER'
REMOTE_AUTH_AUTO_CREATE_USER = True
REMOTE_AUTH_DEFAULT_GROUPS = []
REMOTE_AUTH_DEFAULT_PERMISSIONS = {}

But when I try log in to Netbox I see information

Errors

  • Please enter a correct username and password. Note that both fields may be case-sensitive.
and logs:

Caught LDAPError while authenticating krzysztof.toczyski: PROTOCOL_ERROR({'msgtype': 111, 'msgid': 3, 'result': 2, 'desc': 'Protocol error', 'ctrls': []})

Do you know how to resolve this issue?

Brian Candler

unread,
Jan 20, 2022, 11:35:25 AM1/20/22
to NetBox
What do you mean by "Google LDAP"?

You can't mean "Google Secure LDAP" because that's a cloud service at ldap.google.com (see config instructions).  You're pointing your configuration to localhost:1636. i.e. some process running on the local server.  Therefore, what software are you running that's listening on port 1636?

Note also that port 636 is traditionally for LDAPS (i.e. LDAP over SSL), but you have put "ldap" instead of "ldaps" in your URL.  This might explain the PROTOCOL_ERROR.

Yusuf Tran

unread,
Jan 20, 2022, 12:06:31 PM1/20/22
to NetBox
I recently got this working, stunnel with certs to ldap.google.com and netbox routing through stunnel, settings I used:

stunnel.conf:
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
verify = 2
syslog = no
delay = yes
foreground = yes
CApath = /etc/ssl/certs
client = yes

[ldap]
accept = 1636
connect = ldap.google.com:636
cert = /certs/client.crt
key = /certs/client.key

ldap_config.py: (uses mostly env vars)
from importlib import import_module
from os import environ

import ldap
from django_auth_ldap.config import LDAPSearch, LDAPGroupQuery

# Logging details
import logging, logging.handlers

my_logger = logging.getLogger("django_auth_ldap")
my_logger.setLevel(logging.DEBUG)
# my_logger.setLevel(logging.INFO)
my_logger.addHandler(logging.StreamHandler())
# logfile = "/ldaplog/auth.log"
# handler = logging.handlers.RotatingFileHandler(
# logfile, maxBytes=1024 * 500, backupCount=5)
# my_logger.addHandler(handler)


# Read secret from file
def _read_secret(secret_name, default=None):
    try:
        f = open("/run/secrets/" + secret_name, "r", encoding="utf-8")
    except EnvironmentError:
        return default
    else:
        with f:
            return f.readline().strip()


# Import and return the group type based on string name
def _import_group_type(group_type_name):
    mod = import_module("django_auth_ldap.config")
    try:
        return getattr(mod, group_type_name)()
    except:
        return None


# Server URI
AUTH_LDAP_SERVER_URI = environ.get("AUTH_LDAP_SERVER_URI", "")

# The following may be needed if you are binding to Active Directory.
AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS: 0}

# Set the DN and password for the NetBox service account.
AUTH_LDAP_BIND_DN = environ.get("AUTH_LDAP_BIND_DN", "")
AUTH_LDAP_BIND_PASSWORD = _read_secret("auth_ldap_bind_password", environ.get("AUTH_LDAP_BIND_PASSWORD", ""))

# Set a string template that describes any user’s distinguished name based on the username.
AUTH_LDAP_USER_DN_TEMPLATE = environ.get("AUTH_LDAP_USER_DN_TEMPLATE", None)

# Enable STARTTLS for ldap authentication.
AUTH_LDAP_START_TLS = environ.get("AUTH_LDAP_START_TLS", "False").lower() == "true"

# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert.
# Note that this is a NetBox-specific setting which sets:
#     ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = environ.get("LDAP_IGNORE_CERT_ERRORS", "False").lower() == "true"

AUTH_LDAP_USER_SEARCH_BASEDN = environ.get("AUTH_LDAP_USER_SEARCH_BASEDN", "")
AUTH_LDAP_USER_SEARCH_ATTR = environ.get("AUTH_LDAP_USER_SEARCH_ATTR", "sAMAccountName")
AUTH_LDAP_USER_SEARCH = LDAPSearch(
    AUTH_LDAP_USER_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, "(" + AUTH_LDAP_USER_SEARCH_ATTR + "=%(user)s)"
)

# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
# heirarchy.
AUTH_LDAP_GROUP_SEARCH_BASEDN = environ.get("AUTH_LDAP_GROUP_SEARCH_BASEDN", "")
AUTH_LDAP_GROUP_SEARCH_CLASS = environ.get("AUTH_LDAP_GROUP_SEARCH_CLASS", "group")
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, "(objectClass=" + AUTH_LDAP_GROUP_SEARCH_CLASS + ")"
)
AUTH_LDAP_GROUP_TYPE = _import_group_type(environ.get("AUTH_LDAP_GROUP_TYPE", "GroupOfNamesType"))

# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = environ.get("AUTH_LDAP_REQUIRE_GROUP_DN")

# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {}

# "is_staff": (LDAPGroupQuery(environ.get("AUTH_LDAP_IS_ADMIN_DN", "")) | LDAPGroupQuery(environ.get("AUTH_LDAP_IS_SUPERUSER_DN", ""))) ,
if AUTH_LDAP_REQUIRE_GROUP is not None:
    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
        "is_active": environ.get("AUTH_LDAP_REQUIRE_GROUP_DN", ""),
        "is_staff": environ.get("AUTH_LDAP_IS_ADMIN_DN", ""),
        "is_superuser": environ.get("AUTH_LDAP_IS_SUPERUSER_DN", ""),
    }

# For more granular permissions, we can map LDAP groups to Django groups.
AUTH_LDAP_FIND_GROUP_PERMS = environ.get("AUTH_LDAP_FIND_GROUP_PERMS", "True").lower() == "true"
AUTH_LDAP_MIRROR_GROUPS = environ.get("AUTH_LDAP_MIRROR_GROUPS", "").lower() == "true"

# Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_TIMEOUT = int(environ.get("AUTH_LDAP_CACHE_TIMEOUT", 3600))


# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": environ.get("AUTH_LDAP_ATTR_FIRSTNAME", "givenName"),
    "last_name": environ.get("AUTH_LDAP_ATTR_LASTNAME", "sn"),
    "email": environ.get("AUTH_LDAP_ATTR_MAIL", "mail"),
}


Provided env vars:
LDAP_IGNORE_CERT_ERRORS=true
AUTH_LDAP_START_TLS=False
AUTH_LDAP_SERVER_URI=ldap://localhost:1636
AUTH_LDAP_USER_SEARCH_ATTR=uid
AUTH_LDAP_USER_SEARCH_BASEDN=OU=Users,DC=XXX,DC=com
AUTH_LDAP_GROUP_SEARCH_BASEDN=OU=Groups,DC=XXX,DC=ai
AUTH_LDAP_FIND_GROUP_PERMS=True
AUTH_LDAP_MIRROR_GROUPS=True
AUTH_LDAP_GROUP_TYPE=NestedGroupOfNamesType
AUTH_LDAP_GROUP_SEARCH_CLASS=groupOfNames
AUTH_LDAP_REQUIRE_GROUP_DN=CN=engineering,OU=Groups,DC=XXX,DC=com
AUTH_LDAP_IS_ADMIN_DN=CN=netbox-admins,OU=Groups,DC=XXX,DC=com
AUTH_LDAP_IS_SUPERUSER_DN=CN=netbox-admins,OU=Groups,DC=XXX,DC=com


In google workspace / gsuite, I add the relevant groups to 'netbox-admins' so allow nested groups!
I recall this being important to get working:

AUTH_LDAP_USER_SEARCH_ATTR=uid

else it couldn't find the users.

Hope this helps

Yusuf

Krzysztof Toczyski

unread,
Jan 21, 2022, 8:00:46 AM1/21/22
to NetBox
 Cloud service at ldap.google.com. I use stunnel as a proxy.

"For clients that don't offer a way to authenticate to LDAP with a client certificate, use stunnel as a proxy. 

Configure stunnel to provide the client certificate to the LDAP server and configure your client to connect to stunnel. Ideally, you'll run stunnel on the same server(s) a"

https://support.google.com/a/answer/9089736?hl=en section Optional.

Brian Candler

unread,
Jan 21, 2022, 8:44:53 AM1/21/22
to NetBox

Krzysztof Toczyski

unread,
Jan 21, 2022, 9:37:04 AM1/21/22
to NetBox
I tried this but doesn't work.

This is working config with Google LDAP via stunnel proxy:

import ldap
from django_auth_ldap.config import LDAPSearch, NestedGroupOfNamesType


# Baseline configuration.
AUTH_LDAP_SERVER_URI = "ldap://localhost:1636"
AUTH_LDAP_BIND_DN = "cn=USER"
AUTH_LDAP_BIND_PASSWORD = "PASS"
AUTH_LDAP_START_TLS = False

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    "ou=Users,dc=XXX,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
)


# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    "ou=Groups,dc=XXX,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
)

AUTH_LDAP_GROUP_TYPE = NestedGroupOfNamesType()


# Simple group restrictions
AUTH_LDAP_REQUIRE_GROUP = "cn=sa,ou=Groups,dc=XXX,dc=com"
#AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=Groups,dc=XXX,dc=com"

# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail",
}

AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    "is_active": "cn=sa,ou=Groups,dc=XXX,dc=com",
    "is_staff": "cn=sa,ou=Groups,dc=XXX,dc=com",
    "is_superuser": "cn=sa,ou=Groups,dc=XXX,dc=com",
}

# This is the default, but I like to be explicit.
AUTH_LDAP_ALWAYS_UPDATE_USER = False

# Use LDAP group membership to calculate group permissions.
AUTH_LDAP_FIND_GROUP_PERMS = True
AUTH_LDAP_MIRROR_GROUPS = True


# Cache distinguished names and group memberships for an hour to minimize
# LDAP traffic.
AUTH_LDAP_CACHE_TIMEOUT = 3600

Reply all
Reply to author
Forward
0 new messages