]> granicus.if.org Git - sudo/commitdiff
Add support for querying netgroups directly via LDAP since there
authorTodd C. Miller <Todd.Miller@courtesan.com>
Thu, 29 Jan 2015 21:08:30 +0000 (14:08 -0700)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Thu, 29 Jan 2015 21:08:30 +0000 (14:08 -0700)
is no other way to look up all the netgroups for a user (unlike
regular groups).  This introduces netgroup_base and netgroup_search_filter
options to ldap.conf.  Based on a diff from Steven Soulen.

NEWS
doc/sudoers.ldap.cat
doc/sudoers.ldap.man.in
doc/sudoers.ldap.mdoc.in
plugins/sudoers/ldap.c

diff --git a/NEWS b/NEWS
index 9e910b3ad49b1d5334bf0c4cab91d9cc035227c0..49828e02a8943a8635882e51f6473b247af06055 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -55,6 +55,12 @@ What's new in Sudo 1.8.12
    This limit was introduced when sudo's version of fnmatch()
    was replaced in sudo 1.8.4.
 
+ * LDAP-based sudoers can now query an LDAP server for a user's
+   netgroups directly.  This is often much faster than fetching
+   every sudoRole object containing a sudoUser that begins with a
+   `+' prefix and checking whether the user is a member of any of
+   the returned netgroups.
+
 What's new in Sudo 1.8.11p2
 
  * Fixed a bug where dynamic shared objects loaded from a plugin
index 13e821162590eb859d555f3c809ba3b94c3e4468..23c8ef8af82b6cc4ead01926693fe700c63a0a84 100644 (file)
@@ -189,13 +189,34 @@ D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN
      The second is to match against the user's name and the groups that the
      user belongs to.  (The special ALL tag is matched in this query too.)  If
      no match is returned for the user's name and groups, a third query
-     returns all entries containing user netgroups and checks to see if the
-     user belongs to any of them.
+     returns all entries containing user netgroups and other non-Unix groups
+     and checks to see if the user belongs to any of them.
 
      If timed entries are enabled with the S\bSU\bUD\bDO\bOE\bER\bRS\bS_\b_T\bTI\bIM\bME\bED\bD configuration
      directive, the LDAP queries include a sub-filter that limits retrieval to
      entries that satisfy the time constraints, if any.
 
+     If the N\bNE\bET\bTG\bGR\bRO\bOU\bUP\bP_\b_B\bBA\bAS\bSE\bE configuration directive is present, queries are
+     performed to determine the list of netgroups the user belongs to before
+     the sudoers query.  This makes it possible to include netgroups in the
+     sudoers query string in the same manner as Unix groups.  The third query
+     mentioned above is not performed unless a group provider plugin is also
+     configured.  The actual LDAP queries performed by s\bsu\bud\bdo\bo are as follows:
+
+     1.   Match all nisNetgroup records with a nisNetgroupTriple containing
+          the user and host.  The query will match nisNetgroupTriple entries
+          with either the short or long form of the host name or no host name
+          specified in the tuple.  A wildcard is used to match any domain name
+          but be aware that the NIS schema used by some LDAP servers may not
+          support wild cards for nisNetgroupTriple.
+
+     2.   Repeated queries are performed to find any nested nisNetgroup
+          records with a memberNisNetgroup entry that refers to an already-
+          matched record.
+
+     For sites with a large number of netgroups, using N\bNE\bET\bTG\bGR\bRO\bOU\bUP\bP_\b_B\bBA\bAS\bSE\bE can
+     significantly speed up s\bsu\bud\bdo\bo's execution time.
+
    D\bDi\bif\bff\bfe\ber\bre\ben\bnc\bce\bes\bs b\bbe\bet\btw\bwe\bee\ben\bn L\bLD\bDA\bAP\bP a\ban\bnd\bd n\bno\bon\bn-\b-L\bLD\bDA\bAP\bP s\bsu\bud\bdo\boe\ber\brs\bs
      There are some subtle differences in the way sudoers is handled once in
      LDAP.  Probably the biggest is that according to the RFC, LDAP ordering
@@ -337,6 +358,35 @@ D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN
            The version of the LDAP protocol to use when connecting to the
            server.  The default value is protocol version 3.
 
+     N\bNE\bET\bTG\bGR\bRO\bOU\bUP\bP_\b_B\bBA\bAS\bSE\bE _\bb_\ba_\bs_\be
+           The base DN to use when performing LDAP netgroup queries.
+           Typically this is of the form ou=netgroup,dc=example,dc=com for the
+           domain example.com.  Multiple N\bNE\bET\bTG\bGR\bRO\bOU\bUP\bP_\b_B\bBA\bAS\bSE\bE lines may be specified,
+           in which case they are queried in the order specified.
+
+           This option can be used to query a user's netgroups directly via
+           LDAP which is usually faster than fetching every sudoRole object
+           containing a sudoUser that begins with a `+' prefix.  The NIS
+           schema used by some LDAP servers need a modificaton to support
+           querying the nisNetgroup object by its nisNetgroupTriple member.
+           OpenLDAP's s\bsl\bla\bap\bpd\bd requires the following change to the
+           nisNetgroupTriple attribute:
+
+               attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
+                   DESC 'Netgroup triple'
+                   EQUALITY caseIgnoreIA5Match
+                   SUBSTR caseIgnoreIA5SubstringsMatch
+                   SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+
+     N\bNE\bET\bTG\bGR\bRO\bOU\bUP\bP_\b_S\bSE\bEA\bAR\bRC\bCH\bH_\b_F\bFI\bIL\bLT\bTE\bER\bR _\bl_\bd_\ba_\bp_\b__\bf_\bi_\bl_\bt_\be_\br
+           An LDAP filter which is used to restrict the set of records
+           returned when performing an LDAP netgroup query.  Typically, this
+           is of the form attribute=value or
+           (&(attribute=value)(attribute2=value2)).  The default search filter
+           is: objectClass=nisNetgroup.  If _\bl_\bd_\ba_\bp_\b__\bf_\bi_\bl_\bt_\be_\br is omitted, no search
+           filter will be used.  This option is only when querying netgroups
+           directly via LDAP.
+
      N\bNE\bET\bTW\bWO\bOR\bRK\bK_\b_T\bTI\bIM\bME\bEO\bOU\bUT\bT _\bs_\be_\bc_\bo_\bn_\bd_\bs
            An alias for B\bBI\bIN\bND\bD_\b_T\bTI\bIM\bME\bEL\bLI\bIM\bMI\bIT\bT provided for OpenLDAP compatibility.
 
index 8e7b842b7653e733ff4b112de415cd46f8e6b665..6804b45be5e3943a3e29ad6ed1fb303960f4cea1 100644 (file)
@@ -367,13 +367,52 @@ the user belongs to.
 \fRALL\fR
 tag is matched in this query too.)
 If no match is returned for the user's name and groups, a third
-query returns all entries containing user netgroups and checks
-to see if the user belongs to any of them.
+query returns all entries containing user netgroups and other
+non-Unix groups and checks to see if the user belongs to any of them.
 .PP
 If timed entries are enabled with the
 \fBSUDOERS_TIMED\fR
 configuration directive, the LDAP queries include a sub-filter that
 limits retrieval to entries that satisfy the time constraints, if any.
+.PP
+If the
+\fBNETGROUP_BASE\fR
+configuration directive is present, queries are performed to determine
+the list of netgroups the user belongs to before the sudoers query.
+This makes it possible to include netgroups in the sudoers query
+string in the same manner as Unix groups.
+The third query mentioned above is not performed unless a group provider
+plugin is also configured.
+The actual LDAP queries performed by
+\fBsudo\fR
+are as follows:
+.TP 5n
+1.\&
+Match all
+\fRnisNetgroup\fR
+records with a
+\fRnisNetgroupTriple\fR
+containing the user and host.
+The query will match
+\fRnisNetgroupTriple\fR
+entries with either the short or long form of the host name or
+no host name specified in the tuple.
+A wildcard is used to match any domain name but be aware that the
+NIS schema used by some LDAP servers may not support wild cards for
+\fRnisNetgroupTriple\fR.
+.TP 5n
+2.\&
+Repeated queries are performed to find any nested
+\fRnisNetgroup\fR
+records with a
+\fRmemberNisNetgroup\fR
+entry that refers to an already-matched record.
+.PP
+For sites with a large number of netgroups, using
+\fBNETGROUP_BASE\fR
+can significantly speed up
+\fBsudo\fR's
+execution time.
 .SS "Differences between LDAP and non-LDAP sudoers"
 There are some subtle differences in the way sudoers is handled
 once in LDAP.
@@ -601,6 +640,61 @@ This option is only relevant when using SASL authentication (see below).
 The version of the LDAP protocol to use when connecting to the server.
 The default value is protocol version 3.
 .TP 6n
+\fBNETGROUP_BASE\fR \fIbase\fR
+The base DN to use when performing LDAP netgroup queries.
+Typically this is of the form
+\fRou=netgroup,dc=example,dc=com\fR
+for the domain
+\fRexample.com\fR.
+Multiple
+\fBNETGROUP_BASE\fR
+lines may be specified, in which case they are queried in the order specified.
+.sp
+This option can be used to query a user's netgroups directly via LDAP
+which is usually faster than fetching every
+\fRsudoRole\fR
+object containing a
+\fRsudoUser\fR
+that begins with a
+\(oq+\(cq
+prefix.
+The NIS schema used by some LDAP servers need a modificaton to
+support querying the
+\fRnisNetgroup\fR
+object by its
+\fRnisNetgroupTriple\fR
+member.
+OpenLDAP's
+\fBslapd\fR
+requires the following change to the
+\fRnisNetgroupTriple\fR
+attribute:
+.nf
+.sp
+.RS 10n
+attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
+    DESC 'Netgroup triple'
+    EQUALITY caseIgnoreIA5Match
+    SUBSTR caseIgnoreIA5SubstringsMatch
+    SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+.RE
+.fi
+.TP 6n
+\fBNETGROUP_SEARCH_FILTER\fR \fIldap_filter\fR
+An LDAP filter which is used to restrict the set of records returned
+when performing an LDAP netgroup query.
+Typically, this is of the
+form
+\fRattribute=value\fR
+or
+\fR(&(attribute=value)(attribute2=value2))\fR.
+The default search filter is:
+\fRobjectClass=nisNetgroup\fR.
+If
+\fIldap_filter\fR
+is omitted, no search filter will be used.
+This option is only when querying netgroups directly via LDAP.
+.TP 6n
 \fBNETWORK_TIMEOUT\fR \fIseconds\fR
 An alias for
 \fBBIND_TIMELIMIT\fR
index e2c0c90e47d927dda619ac98b4e95f7bfa3e7d60..f16290e9044ea841a89b5c20a080d949ea7092a4 100644 (file)
@@ -345,13 +345,52 @@ the user belongs to.
 .Li ALL
 tag is matched in this query too.)
 If no match is returned for the user's name and groups, a third
-query returns all entries containing user netgroups and checks
-to see if the user belongs to any of them.
+query returns all entries containing user netgroups and other
+non-Unix groups and checks to see if the user belongs to any of them.
 .Pp
 If timed entries are enabled with the
 .Sy SUDOERS_TIMED
 configuration directive, the LDAP queries include a sub-filter that
 limits retrieval to entries that satisfy the time constraints, if any.
+.Pp
+If the
+.Sy NETGROUP_BASE
+configuration directive is present, queries are performed to determine
+the list of netgroups the user belongs to before the sudoers query.
+This makes it possible to include netgroups in the sudoers query
+string in the same manner as Unix groups.
+The third query mentioned above is not performed unless a group provider
+plugin is also configured.
+The actual LDAP queries performed by
+.Nm sudo
+are as follows:
+.Bl -enum
+.It
+Match all
+.Li nisNetgroup
+records with a
+.Li nisNetgroupTriple
+containing the user and host.
+The query will match
+.Li nisNetgroupTriple
+entries with either the short or long form of the host name or
+no host name specified in the tuple.
+A wildcard is used to match any domain name but be aware that the
+NIS schema used by some LDAP servers may not support wild cards for
+.Li nisNetgroupTriple .
+.It
+Repeated queries are performed to find any nested
+.Li nisNetgroup
+records with a
+.Li memberNisNetgroup
+entry that refers to an already-matched record.
+.El
+.Pp
+For sites with a large number of netgroups, using
+.Sy NETGROUP_BASE
+can significantly speed up
+.Nm sudo Ns 's
+execution time.
 .Ss Differences between LDAP and non-LDAP sudoers
 There are some subtle differences in the way sudoers is handled
 once in LDAP.
@@ -561,6 +600,56 @@ This option is only relevant when using SASL authentication (see below).
 .It Sy LDAP_VERSION Ar number
 The version of the LDAP protocol to use when connecting to the server.
 The default value is protocol version 3.
+.It Sy NETGROUP_BASE Ar base
+The base DN to use when performing LDAP netgroup queries.
+Typically this is of the form
+.Li ou=netgroup,dc=example,dc=com
+for the domain
+.Li example.com .
+Multiple
+.Sy NETGROUP_BASE
+lines may be specified, in which case they are queried in the order specified.
+.Pp
+This option can be used to query a user's netgroups directly via LDAP
+which is usually faster than fetching every
+.Li sudoRole
+object containing a
+.Li sudoUser
+that begins with a
+.Ql +
+prefix.
+The NIS schema used by some LDAP servers need a modificaton to
+support querying the
+.Li nisNetgroup
+object by its
+.Li nisNetgroupTriple
+member.
+OpenLDAP's
+.Sy slapd
+requires the following change to the
+.Li nisNetgroupTriple
+attribute:
+.Bd -literal -offset 4n
+attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
+    DESC 'Netgroup triple'
+    EQUALITY caseIgnoreIA5Match
+    SUBSTR caseIgnoreIA5SubstringsMatch
+    SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+.Ed
+.It Sy NETGROUP_SEARCH_FILTER Ar ldap_filter
+An LDAP filter which is used to restrict the set of records returned
+when performing an LDAP netgroup query.
+Typically, this is of the
+form
+.Li attribute=value
+or
+.Li (&(attribute=value)(attribute2=value2)) .
+The default search filter is:
+.Li objectClass=nisNetgroup .
+If
+.Ar ldap_filter
+is omitted, no search filter will be used.
+This option is only when querying netgroups directly via LDAP.
 .It Sy NETWORK_TIMEOUT Ar seconds
 An alias for
 .Sy BIND_TIMELIMIT
index 69f681354d04de56ba249f22379f2d3926a83679..0ede38fb0371b523be933aa23311cd01f5914946 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003-2014 Todd C. Miller <Todd.Miller@courtesan.com>
+ * Copyright (c) 2003-2015 Todd C. Miller <Todd.Miller@courtesan.com>
  *
  * This code is derived from software contributed by Aaron Spangler.
  *
@@ -162,6 +162,9 @@ extern int ldapssl_set_strength(LDAP *ldap, int strength);
 /* Default search filter. */
 #define DEFAULT_SEARCH_FILTER  "(objectClass=sudoRole)"
 
+/* Default netgroup search filter. */
+#define DEFAULT_NETGROUP_SEARCH_FILTER "(objectClass=nisNetgroup)"
+
 /* The TIMEFILTER_LENGTH is the length of the filter when timed entries
    are used. The length is computed as follows:
        81       for the filter itself
@@ -207,6 +210,16 @@ struct ldap_result {
 };
 #define        ALLOCATION_INCREMENT    100
 
+/*
+ * The ldap_netgroup structure implements a singly-linked tail queue of
+ * netgroups a user is a member of when querying netgroups directly.
+ */
+struct ldap_netgroup {
+    STAILQ_ENTRY(ldap_netgroup) entries;
+    char *name;
+};
+STAILQ_HEAD(ldap_netgroup_list, ldap_netgroup);
+
 struct ldap_config_table {
     const char *conf_str;      /* config file string */
     int type;                  /* CONF_BOOL, CONF_INT, CONF_STR */
@@ -218,7 +231,6 @@ struct ldap_config_str {
     STAILQ_ENTRY(ldap_config_str) entries;
     char val[1];
 };
-
 STAILQ_HEAD(ldap_config_str_list, ldap_config_str);
 
 /* LDAP configuration structure */
@@ -242,7 +254,9 @@ static struct ldap_config {
     char *bindpw;
     char *rootbinddn;
     struct ldap_config_str_list base;
+    struct ldap_config_str_list netgroup_base;
     char *search_filter;
+    char *netgroup_search_filter;
     char *ssl;
     char *tls_cacertfile;
     char *tls_cacertdir;
@@ -315,6 +329,8 @@ static struct ldap_config_table ldap_conf_global[] = {
     { "sudoers_base", CONF_LIST_STR, -1, &ldap_conf.base },
     { "sudoers_timed", CONF_BOOL, -1, &ldap_conf.timed },
     { "sudoers_search_filter", CONF_STR, -1, &ldap_conf.search_filter },
+    { "netgroup_base", CONF_LIST_STR, -1, &ldap_conf.netgroup_base },
+    { "netgroup_search_filter", CONF_STR, -1, &ldap_conf.netgroup_search_filter },
 #ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
     { "use_sasl", CONF_BOOL, -1, &ldap_conf.use_sasl },
     { "sasl_auth_id", CONF_STR, -1, &ldap_conf.sasl_auth_id },
@@ -408,7 +424,6 @@ struct sudo_nss sudo_nss_ldap = {
 static bool
 sudo_ldap_conf_add_ports(void)
 {
-
     char *host, *port, defport[13];
     char hostbuf[LINE_MAX * 2];
     int len;
@@ -744,18 +759,18 @@ sudo_ldap_check_runas_user(LDAP *ld, LDAPMessage *entry)
 
     /*
      * BUG:
-     * 
+     *
      * if runas is not specified on the command line, the only information
      * as to which user to run as is in the runas_default option.  We should
      * check to see if we have the local option present.  Unfortunately we
      * don't parse these options until after this routine says yes or no.
      * The query has already returned, so we could peek at the attribute
      * values here though.
-     * 
+     *
      * For now just require users to always use -u option unless its set
      * in the global defaults. This behaviour is no different than the global
      * /etc/sudoers.
-     * 
+     *
      * Sigh - maybe add this feature later
      */
 
@@ -1227,19 +1242,198 @@ done:
     return dlen + (s - src);   /* count does not include NUL */
 }
 
+/*
+ * Check the netgroups list beginning at "start" for nesting.
+ * Parent nodes with a memberNisNetgroup that match one of the
+ * netgroups are added to the list and checked for further nesting.
+ * Return true on success or false if there was an internal overflow.
+ */
+static bool
+sudo_netgroup_lookup_nested(LDAP *ld, char *base, struct timeval *timeout,
+    struct ldap_netgroup_list *netgroups, struct ldap_netgroup *start)
+{
+    struct ldap_netgroup *ng, *old_tail;
+    LDAPMessage *entry, *result;
+    size_t filt_len;
+    char *filt;
+    int rc;
+    debug_decl(sudo_netgroup_lookup_nested, SUDOERS_DEBUG_LDAP, sudoers_debug_instance);
+
+    DPRINTF1("Checking for nested netgroups from netgroup_base '%s'", base);
+    do {
+       old_tail = STAILQ_LAST(netgroups, ldap_netgroup, entries);
+       filt_len = strlen(ldap_conf.netgroup_search_filter) + 7;
+       for (ng = start; ng != NULL; ng = STAILQ_NEXT(ng, entries)) {
+           filt_len += sudo_ldap_value_len(ng->name) + 20;
+       }
+       filt = sudo_emalloc(filt_len);
+       CHECK_STRLCPY(filt, "(&", filt_len);
+       CHECK_STRLCAT(filt, ldap_conf.netgroup_search_filter, filt_len);
+       CHECK_STRLCAT(filt, "(|", filt_len);
+       for (ng = start; ng != NULL; ng = STAILQ_NEXT(ng, entries)) {
+           CHECK_STRLCAT(filt, "(memberNisNetgroup=", filt_len);
+           CHECK_LDAP_VCAT(filt, ng->name, filt_len);
+           CHECK_STRLCAT(filt, ")", filt_len);
+       }
+       CHECK_STRLCAT(filt, "))", filt_len);
+       DPRINTF1("ldap netgroup search filter: '%s'", filt);
+       result = NULL;
+       rc = ldap_search_ext_s(ld, base, LDAP_SCOPE_SUBTREE, filt,
+           NULL, 0, NULL, NULL, timeout, 0, &result);
+       if (rc == LDAP_SUCCESS) {
+           LDAP_FOREACH(entry, ld, result) {
+               struct berval **bv;
+
+               bv = ldap_get_values_len(ld, entry, "cn");
+               if (bv != NULL) {
+                   /* Don't add a netgroup twice. */
+                   STAILQ_FOREACH(ng, netgroups, entries) {
+                       /* Assumes only one cn per entry. */
+                       if (strcasecmp(ng->name, (*bv)->bv_val) == 0)
+                           break;
+                   }
+                   if (ng == NULL) {
+                       ng = sudo_emalloc(sizeof(*ng));
+                       ng->name = sudo_estrdup((*bv)->bv_val);
+                       STAILQ_INSERT_TAIL(netgroups, ng, entries);
+                       DPRINTF1("Found new netgroup %s for %s", ng->name, base);
+                   }
+                   ldap_value_free_len(bv);
+               }
+           }
+       }
+       if (result)
+           ldap_msgfree(result);
+       sudo_efree(filt);
+
+       /* Check for nested netgroups in what we added. */
+       start = old_tail ? STAILQ_NEXT(old_tail, entries) : STAILQ_FIRST(netgroups);
+    } while (start != NULL);
+
+    debug_return_bool(true);
+overflow:
+    sudo_warnx(U_("internal error, %s overflow"), __func__);
+    debug_return_bool(false);
+}
+
+/*
+ * Look up netgroups that the specified user is a member of.
+ * Appends new entries to the netgroups list.
+ * Return true on success or false if there was an internal overflow.
+ */
+static bool
+sudo_netgroup_lookup(LDAP *ld, struct passwd *pw,
+    struct ldap_netgroup_list *netgroups)
+{
+    struct ldap_config_str *base;
+    struct ldap_netgroup *ng, *old_tail;
+    struct timeval tv, *tvp = NULL;
+    LDAPMessage *entry, *result;
+    size_t filt_len;
+    char *filt;
+    int rc;
+    debug_decl(sudo_netgroup_lookup, SUDOERS_DEBUG_LDAP, sudoers_debug_instance);
+
+    if (ldap_conf.timeout > 0) {
+       tv.tv_sec = ldap_conf.timeout;
+       tv.tv_usec = 0;
+       tvp = &tv;
+    }
+
+    STAILQ_FOREACH(base, &ldap_conf.netgroup_base, entries) {
+       /* Build query. */
+       filt_len = 2 + strlen(ldap_conf.netgroup_search_filter) +
+           24 + (2 * sudo_ldap_value_len(pw->pw_name)) + 26 +
+           sudo_ldap_value_len(user_shost) + 1 + 7 + 1;
+       if (user_host != user_shost) {
+           filt_len += 26 + sudo_ldap_value_len(user_host) + 1 +
+               sudo_ldap_value_len(pw->pw_name);
+       }
+       filt = sudo_emalloc(filt_len);
+       DPRINTF1("searching from netgroup_base '%s'", base->val);
+       CHECK_STRLCPY(filt, "(&", filt_len);
+       CHECK_STRLCAT(filt, ldap_conf.netgroup_search_filter, filt_len);
+       CHECK_STRLCAT(filt, "(|(nisNetgroupTriple=\\(,", filt_len);
+       CHECK_LDAP_VCAT(filt, pw->pw_name, filt_len);
+       CHECK_STRLCAT(filt, ",*\\))(nisNetgroupTriple=\\(", filt_len);
+       CHECK_LDAP_VCAT(filt, user_shost, filt_len);
+       CHECK_STRLCAT(filt, ",", filt_len);
+       CHECK_LDAP_VCAT(filt, pw->pw_name, filt_len);
+       if (user_host != user_shost) {
+           CHECK_STRLCAT(filt, ",*\\))(nisNetgroupTriple=\\(", filt_len);
+           CHECK_LDAP_VCAT(filt, user_host, filt_len);
+           CHECK_STRLCAT(filt, ",", filt_len);
+           CHECK_LDAP_VCAT(filt, pw->pw_name, filt_len);
+       }
+       CHECK_STRLCAT(filt, ",*\\))))", filt_len);
+
+       DPRINTF1("ldap netgroup search filter: '%s'", filt);
+       result = NULL;
+       rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt,
+           NULL, 0, NULL, NULL, tvp, 0, &result);
+       if (rc != LDAP_SUCCESS) {
+           DPRINTF1("nothing found for '%s'", filt);
+           if (result)
+               ldap_msgfree(result);
+           sudo_efree(filt);
+           continue;
+       }
+       sudo_efree(filt);
+
+       old_tail = STAILQ_LAST(netgroups, ldap_netgroup, entries);
+       LDAP_FOREACH(entry, ld, result) {
+           struct berval **bv;
+
+           bv = ldap_get_values_len(ld, entry, "cn");
+           if (bv != NULL) {
+               /* Don't add a netgroup twice. */
+               STAILQ_FOREACH(ng, netgroups, entries) {
+                   /* Assumes only one cn per entry. */
+                   if (strcasecmp(ng->name, (*bv)->bv_val) == 0)
+                       break;
+               }
+               if (ng == NULL) {
+                   ng = sudo_emalloc(sizeof(*ng));
+                   ng->name = sudo_estrdup((*bv)->bv_val);
+                   STAILQ_INSERT_TAIL(netgroups, ng, entries);
+                   DPRINTF1("Found new netgroup %s for %s", ng->name,
+                       base->val);
+               }
+               ldap_value_free_len(bv);
+           }
+       }
+       ldap_msgfree(result);
+
+       /* Check for nested netgroups in what we added. */
+       ng = old_tail ? STAILQ_NEXT(old_tail, entries) : STAILQ_FIRST(netgroups);
+       if (ng != NULL) {
+           if (!sudo_netgroup_lookup_nested(ld, base->val, tvp, netgroups, ng))
+               debug_return_bool(false);
+       }
+    }
+    debug_return_bool(true);
+overflow:
+    sudo_warnx(U_("internal error, %s overflow"), __func__);
+    debug_return_bool(false);
+}
+
 /*
  * Builds up a filter to check against LDAP.
  */
 static char *
-sudo_ldap_build_pass1(struct passwd *pw)
+sudo_ldap_build_pass1(LDAP *ld, struct passwd *pw)
 {
-    struct group *grp;
     char *buf, timebuffer[TIMEFILTER_LENGTH + 1], gidbuf[MAX_UID_T_LEN + 1];
+    struct ldap_netgroup_list netgroups;
+    struct ldap_netgroup *ng, *nextng;
     struct group_list *grlist;
+    struct group *grp;
     size_t sz = 0;
     int i;
     debug_decl(sudo_ldap_build_pass1, SUDOERS_DEBUG_LDAP, sudoers_debug_instance)
 
+    STAILQ_INIT(&netgroups);
+
     /* If there is a filter, allocate space for the global AND. */
     if (ldap_conf.timed || ldap_conf.search_filter)
        sz += 3;
@@ -1269,6 +1463,23 @@ sudo_ldap_build_pass1(struct passwd *pw)
        }
     }
 
+    /* Add space for user netgroups if netgroup_base specified. */
+    if (!STAILQ_EMPTY(&ldap_conf.netgroup_base)) {
+       DPRINTF1("Looking up netgroups for %s", pw->pw_name);
+       if (sudo_netgroup_lookup(ld, pw, &netgroups)) {
+           STAILQ_FOREACH(ng, &netgroups, entries) {
+               sz += 14 + strlen(ng->name);
+           }
+       } else {
+           /* sudo_netgroup_lookup() failed, clean up. */
+           STAILQ_FOREACH_SAFE(ng, &netgroups, entries, nextng) {
+               sudo_efree(ng->name);
+               sudo_efree(ng);
+           }
+           STAILQ_INIT(&netgroups);
+       }
+    }
+
     /* If timed, add space for time limits. */
     if (ldap_conf.timed)
        sz += TIMEFILTER_LENGTH;
@@ -1327,7 +1538,16 @@ sudo_ldap_build_pass1(struct passwd *pw)
     if (grp != NULL)
        sudo_gr_delref(grp);
 
-    /* Add ALL to list and end the global OR */
+    /* Add netgroups (if any), freeing the list as we go. */
+    STAILQ_FOREACH_SAFE(ng, &netgroups, entries, nextng) {
+       CHECK_STRLCAT(buf, "(sudoUser=+", sz);
+       CHECK_LDAP_VCAT(buf, ng->name, sz);
+       CHECK_STRLCAT(buf, ")", sz);
+       sudo_efree(ng->name);
+       sudo_efree(ng);
+    }
+
+    /* Add ALL to list and end the global OR. */
     CHECK_STRLCAT(buf, "(sudoUser=ALL)", sz);
 
     /* Add the time restriction, or simply end the global OR. */
@@ -1354,32 +1574,36 @@ static char *
 sudo_ldap_build_pass2(void)
 {
     char *filt, timebuffer[TIMEFILTER_LENGTH + 1];
+    bool query_netgroups = def_use_netgroups;
     debug_decl(sudo_ldap_build_pass2, SUDOERS_DEBUG_LDAP, sudoers_debug_instance)
 
-    /* Short circuit if no non-Unix group support. */
-    if (!def_use_netgroups && !def_group_plugin) {
+    /* No need to query netgroups if using netgroup_base. */
+    if (!STAILQ_EMPTY(&ldap_conf.netgroup_base))
+       query_netgroups = false;
+
+    /* Short circuit if no netgroups and no non-Unix groups. */
+    if (!query_netgroups && !def_group_plugin)
        debug_return_str(NULL);
-    }
 
     if (ldap_conf.timed)
        sudo_ldap_timefilter(timebuffer, sizeof(timebuffer));
 
     /*
      * Match all sudoUsers beginning with '+' or '%:'.
-     * If a search filter or time restriction is specified, 
+     * If a search filter or time restriction is specified,
      * those get ANDed in to the expression.
      */
-    if (def_group_plugin) {
-       sudo_easprintf(&filt, "%s%s(|(sudoUser=%s*)(sudoUser=%%:*))%s%s",
+    if (query_netgroups && def_group_plugin) {
+       sudo_easprintf(&filt, "%s%s(|(sudoUser=+*)(sudoUser=%%:*))%s%s",
            (ldap_conf.timed || ldap_conf.search_filter) ? "(&" : "",
            ldap_conf.search_filter ? ldap_conf.search_filter : "",
-           def_use_netgroups ? "+" : "",
            ldap_conf.timed ? timebuffer : "",
            (ldap_conf.timed || ldap_conf.search_filter) ? ")" : "");
     } else {
-       sudo_easprintf(&filt, "%s%s(sudoUser=*)(sudoUser=+*)%s%s",
+       sudo_easprintf(&filt, "%s%s(sudoUser=*)(sudoUser=%s*)%s%s",
            (ldap_conf.timed || ldap_conf.search_filter) ? "(&" : "",
            ldap_conf.search_filter ? ldap_conf.search_filter : "",
+           query_netgroups ? "+" : "%:",
            ldap_conf.timed ? timebuffer : "",
            (ldap_conf.timed || ldap_conf.search_filter) ? ")" : "");
     }
@@ -1559,9 +1783,10 @@ sudo_check_krb5_ccname(const char *ccname)
 static bool
 sudo_ldap_read_config(void)
 {
-    FILE *fp;
     char *cp, *keyword, *value, *line = NULL;
+    struct ldap_config_str *conf_str;
     size_t linesize = 0;
+    FILE *fp;
     debug_decl(sudo_ldap_read_config, SUDOERS_DEBUG_LDAP, sudoers_debug_instance)
 
     /* defaults */
@@ -1575,8 +1800,10 @@ sudo_ldap_read_config(void)
     ldap_conf.rootuse_sasl = -1;
     ldap_conf.deref = -1;
     ldap_conf.search_filter = sudo_estrdup(DEFAULT_SEARCH_FILTER);
+    ldap_conf.netgroup_search_filter = sudo_estrdup(DEFAULT_NETGROUP_SEARCH_FILTER);
     STAILQ_INIT(&ldap_conf.uri);
     STAILQ_INIT(&ldap_conf.base);
+    STAILQ_INIT(&ldap_conf.netgroup_base);
 
     if ((fp = fopen(path_ldap_conf, "r")) == NULL)
        debug_return_bool(false);
@@ -1610,10 +1837,8 @@ sudo_ldap_read_config(void)
     DPRINTF1("LDAP Config Summary");
     DPRINTF1("===================");
     if (!STAILQ_EMPTY(&ldap_conf.uri)) {
-       struct ldap_config_str *uri;
-
-       STAILQ_FOREACH(uri, &ldap_conf.uri, entries) {
-           DPRINTF1("uri              %s", uri->val);
+       STAILQ_FOREACH(conf_str, &ldap_conf.uri, entries) {
+           DPRINTF1("uri              %s", conf_str->val);
        }
     } else {
        DPRINTF1("host             %s",
@@ -1623,9 +1848,8 @@ sudo_ldap_read_config(void)
     DPRINTF1("ldap_version     %d", ldap_conf.version);
 
     if (!STAILQ_EMPTY(&ldap_conf.base)) {
-       struct ldap_config_str *base;
-       STAILQ_FOREACH(base, &ldap_conf.base, entries) {
-           DPRINTF1("sudoers_base     %s", base->val);
+       STAILQ_FOREACH(conf_str, &ldap_conf.base, entries) {
+           DPRINTF1("sudoers_base     %s", conf_str->val);
        }
     } else {
        DPRINTF1("sudoers_base     %s", "(NONE: LDAP disabled)");
@@ -1633,6 +1857,16 @@ sudo_ldap_read_config(void)
     if (ldap_conf.search_filter) {
        DPRINTF1("search_filter    %s", ldap_conf.search_filter);
     }
+    if (!STAILQ_EMPTY(&ldap_conf.netgroup_base)) {
+       STAILQ_FOREACH(conf_str, &ldap_conf.netgroup_base, entries) {
+           DPRINTF1("netgroup_base    %s", conf_str->val);
+       }
+    } else {
+       DPRINTF1("netgroup_base %s", "(NONE: will use nsswitch)");
+    }
+    if (ldap_conf.netgroup_search_filter) {
+        DPRINTF1("netgroup_search_filter %s", ldap_conf.netgroup_search_filter);
+    }
     DPRINTF1("binddn           %s",
        ldap_conf.binddn ? ldap_conf.binddn : "(anonymous)");
     DPRINTF1("bindpw           %s",
@@ -2706,7 +2940,7 @@ sudo_ldap_lookup(struct sudo_nss *nss, int ret, int pwflag)
     if (pwflag) {
        int doauth = UNSPEC;
        int matched = UNSPEC;
-       enum def_tuple pwcheck = 
+       enum def_tuple pwcheck =
            (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple;
 
        DPRINTF1("perform search for pwflag %d", pwflag);
@@ -2949,7 +3183,7 @@ sudo_ldap_result_get(struct sudo_nss *nss, struct passwd *pw)
      */
     lres = sudo_ldap_result_alloc();
     for (pass = 0; pass < 2; pass++) {
-       filt = pass ? sudo_ldap_build_pass2() : sudo_ldap_build_pass1(pw);
+       filt = pass ? sudo_ldap_build_pass2() : sudo_ldap_build_pass1(ld, pw);
        if (filt != NULL) {
            DPRINTF1("ldap search '%s'", filt);
            STAILQ_FOREACH(base, &ldap_conf.base, entries) {