Configure TACACS+ and LDAP authentication and authorization using users from a directory service. This is a GNS3 netlab the router operating system in this netlab is Cisco's IOS-XE. It will work with every other router too. I have written a lot of simple GNS3 TACACS+NG netlabs, that one is more complex but I plan to make it as simple as possible so you can rebuild it in your own network testing area. The components used in the netlab are following:

  • TACACS+ appliance (TACACS+NG)
  • LDAP appliance (LLDAP)
  • IOS-XE router

Build the both appliances using alpine-linux, the resulting size for both appliances is 500MB. You need to build them by yourself, use following build instructions Building 64bit alpine linux GNS3 FRRouting appliance and add the software from above.

This netlab runs without enabling encryption protocol like LDAPS or TLS or HTTPS. It is not in the scope of the netlab to enable encryption at all. You SHOULD be able to add security protocols on top once everything is setup and working. The scope of this netlab is to build a simple environment where you can play with the programs and protocols. This is a sandbox solution.

Regarding LDAP the official TACACS+NG documentation states in section 5.1.LDAP Backends

mavis_tacplus-ng_ldap.pl is an authentication/authorization back-end for the external module. It interfaces to various kinds of LDAP servers, e.g. OpenLDAP, Fedora DS and Active Directory. The server type is detected automatically. Its behaviour is controlled by a list of environmental variables: ...

TACACS+NG has a very long list of environmental LDAP variables that might be worth reading about and tinkering with once everything is working as configured, to name a few:

  • LDAP_FILTER
  • LDAP_FILTER_GROUP
  • LDAP_MEMBEROF_REGEX

Overview of the used IP addressing in this netlab

Hostname Term IP address Function
R101 NAS 10.100.100.101 Testrouter
LLDAP-10 LDAP 198.51.100.10 directory service
AAA-49 TACACS+ 10.100.200.49 TACACS+ daemon

Overview of the data that needs to be configured in the directory service LDAP in the order of priority:

Name Type Password Group
ldapadmin person ldapadmin lldap_admin
ro person readonly lldap_strict_readonly
NOC group
    -
-
cisco person cisco123 NOC

LDAP

I wanted to find and use a really simple and lightweight LDAP implementation for this particular netlab. A netlab with only few users in the directory and a service with provisioned groups and easy configuration packaged and shipped with a GUI, since configuring LDAP from CLI is something most of use would not use in first place. LLDAP is such lightweight LDAP implementation that is packaged and with a HTTP GUI so it is easy to use out-of-the-box.

This is what the LLDAP project about its own implementation of LDAP:

  • simple to setup (no messing around with slapd)
  • simple to manage (friendly web UI)
  • low resources
  • opinionated with basic defaults so you don't have to understand the subtleties of LDAP

The right software for running in a small netlab environment.

LLDAP configuration

Before start copy and pasting the example configuration below, 2 variables need to be generated manually according to the alpinelinux wiki entry:

  • LLDAP_JWT_SECRET
  • LLDAP_KEY_SEED

When using the 0.6.x LLDAP version, setting the LLDAP_KEY_SEED is not needed anymore. The initial start of the daemon or the software generates the key_seed automatically, looks there is already a routine for it. If not the software it will generate a failure message that something is missing.

Full configuration for running LLDAP in the netlab, the configuration file is called lldap_config.toml.

#verbose = true
ldap_port = 389
ldap_host = "0.0.0.0"
http_port = 17170
http_url = "http://lldap.example.org"
jwt_secret = "QPK6m5l8iUSiHWNCZFXt"
ldap_base_dn = "dc=example,dc=org"
ldap_user_dn = "ldapadmin"
ldap_user_email = "ldapadmin@example.org"
ldap_user_pass = "ldapadmin"
database_url = "sqlite:///opt/lldap/users.db?mode=rwc"

Few variables need to be explained before hitting the go button.

Best practice is to create a LDAP read-only user or a query user, this user is called ro with password readonly. Everything runs using IPv4 only, check the ldap_host setting. LLDAP has already the right LDAP group for that particular rouser , is is called lldap_strict_readonly group. Add the ro user in this particular LDAP group. The LLDAP is configured over here to run using the official port 389 the LLDAP default is set to use the 3890 TCP/IP port.

Check using the grep command on /etc/services on the linux box:

user % grep ldap /etc/services

ldap 389/tcp # Lightweight Directory Access Protocol ldap 389/udp ldaps 636/tcp # LDAP over SSL ldaps 636/udp

Finally Start the LLDAP service:

root # rc-service lldap start

Verify the directory service it runs using the ss -tulpn command, the LDAP application will have a open TCP socket, verify in the output below:

ss -tulpn
Netid   State    Recv-Q   Send-Q     Local Address:Port      Peer Address:Port  Process
tcp     LISTEN   0        128              0.0.0.0:22             0.0.0.0:*      users:(("sshd",pid=2113,fd=6))
tcp     LISTEN   0        2048             0.0.0.0:17170          0.0.0.0:*      users:(("lldap",pid=2337,fd=11))
tcp     LISTEN   0        2048             0.0.0.0:389            0.0.0.0:*      users:(("lldap",pid=2337,fd=10))
tcp     LISTEN   0        128                 [::]:22                [::]:*      users:(("sshd",pid=2113,fd=7))

At this point login to the LLDAP WebUI and provision the directory with example users and groups. In my netlab the LDAP server is reachable at 198.51.100.10 IP:

http://198.51.100.10:17170

Login using the ldapadmin user and the password ldapadmin and create the directory with users and groups as in the table above in the heading.

LDAP memberOf attribute

LDAP memberOf is a another option on how to assign LDAP users to certain TACACS+ profiles without defining everything in the tac_plus-ng configuration file.

Something from the official mailing list Marc Huber explains on a particular configuration issue:

the backend script didn't return a MEMBEROF attribute, causing the condition to fail.

1061346: 16:59:32.350 52/14b8d526: 192.168.2.28 USER (len: 8): admin-rw
1061346: 16:59:32.350 52/14b8d526: 192.168.2.28 DN (len: 36): uid=admin-rw,ou=Users,dc=sample,dc=net
1061346: 16:59:32.350 52/14b8d526: 192.168.2.28 SERVERIP (len: 14): 192.168.2.28
1061346: 16:59:32.350 52/14b8d526: 192.168.2.28 REALM (len: 8): port1611
1061346: 16:59:32.350 52/14b8d526: 192.168.2.28 IDENTITY_SOURCE (len: 1): 0

Please double-check that your LDAP server is returning the memberOf attribute and (if set to a custom value) LDAP_MEMBEROF_REGEX matches your memberOf values.

The memberOf is supported by LLDAP and it might be interesting to compare the times using different configurations options of TACACS+NG with LDAP. It might be related for your network when thousands of routers and many applications requests credentials to login to routers gathering information monitoring the IP network, the hardware and the state of the network. Compare the different configuration options regarding LDAP requests and choose one that applies best to your productive environment.

Review the LLDAP debug and verbose running to see how many events in LDAP a single authentication/authorization reqest does.

TACACS+NG

This is the most simple mavis external configuration. The lines are taken out of a context but show what is really needed to configure a productive TACACS+ with LDAP:

Marc writes

... Auto-detecting the server type should work with the 389 Project LDAP server, so setting

setenv LDAP_HOSTS = "ldap://10.10.1.70:389" setenv LDAP_BASE = "cn=accounts,dc=mgmt,dc=removed,dc=com" setenv LDAP_USER = "uid=tacacs-server,cn=sysaccounts,cn=etc,dc=mgmt,dc=removed,dc=com" setenv LDAP_PASSWD = "removed"

should be sufficient. ...

4 configuration lines and it should be working. The LDAP configuration COULD contain users password in the tac_plus-ng.cfg configuration file AND MAVIS at same time. MAVIS takes precedence over the locally configured passwords. See section 4.22. Configuring User Authentication.

...

login backend = mavis

(again, in the global section of the configuration file) will cause PAP and/or Login authentication to be performed by the MAVIS back-end (e.g. by performing an LDAP bind), ignoring any corresponding password definitions in the users' profile.

If you just want the users defined in your configuration file to authenticate using the MAVIS back-end, simply set the corresponding PAP or Login password field to mavis (there's no need to add the user backend = mavis directive in this case):

user mary { login = mavis } ...

This all is not used here and does not need configuration but at your disposition.

Configuration

This is a example LDAP authentication and authorization example, where the users and groups are from LDAP directory. The group NOC here below is named the same in LDAP. No credentials or anything in tacacs+ng configuration. Everything is managed in LDAP, the profile is added as the user authentifiction succeeds.

The user cisco is member of the LDAP group NOC, the user profile is set by TACACS+ below with the correct priv-lvl.

Using this TACACS+ section the daemon sends the received user data to a MAVIS backend, to LDAP:

...
login backend = mavis
user backend = mavis
pap backend = mavis
...

Fully working TAC_PLUS-NG configuration using user data form LDAP:

#!/usr/local/sbin/tac_plus-ng

id = spawnd {
    listen = { port = 49 }
}

id = tac_plus-ng {
    # debug = ALL
    mavis module = external {
        setenv LDAP_HOSTS = "ldap://198.51.100.10:389"
        setenv LDAP_BASE = "dc=example,dc=org"
        setenv LDAP_USER = "cn=ro,ou=people,dc=example,dc=org"
        setenv LDAP_PASSWD = "readonly"
        exec = /usr/lib/mavis/mavis_tacplus-ng_ldap.pl
    }

    login backend = mavis
    user backend = mavis
    pap backend = mavis

    host IPv4 {
        address = 0.0.0.0/0
        welcome banner = "\n Welcome to TACACS+NG LDAP MAVIS\n"
        key = "123-my_tacacs_key"
    }

    profile netadmin {
        script {
            if (service == shell) {
                if (cmd == "") {
                    set priv-lvl = 15
                    permit
                }
            }
        }
    }

    group NOC

    ruleset {
        rule {
            script {
                if (group == NOC) {
                    profile = netadmin
                    permit
                }
            }
        }
    }
}

Ready.

From that point on if everything is working it is possible to extend the configuration and apply your own rules and add more privilege levels or even use RBAC instead.

In my opinion more than 3 different profiles per networking operating system or per networking vendor do not make much sense. It is to expensive to configure and operate this kind of configuration, but every IP network is a snowflake and this is a rule of thumb and meant only as a suggestion.

Testing TACACS+NG and LDAP

To test the full setup run the following command:

mavistest /etc/tac_plus-ng/tac_plus-ng.cfg tac_plus-ng TACPLUS cisco cisco123

This tests if the communiction with the LDAP server is working. The username is cisco its password is cisco123. When mavistest runs successfully following lines will be shown at the end of the output, showing the various AV pairs from the directory service configure on the testing user:

mavistest /etc/tac_plus-ng/tac_plus-ng.cfg tac_plus-ng TACPLUS cisco cisco123
...
Input attribute-value-pairs:
TYPE                TACPLUS
TIMESTAMP           mavistest-2092-1768544684-0
USER                cisco
PASSWORD            cisco123
TACTYPE             AUTH

Output attribute-value-pairs:
TYPE                TACPLUS
MEMBEROF            "cn=NOC,ou=groups,dc=example,dc=org"
TIMESTAMP           mavistest-2092-1768544684-0
USER                cisco
DN                  uid=cisco,ou=people,dc=example,dc=org
RESULT              ACK
PASSWORD            cisco123
SERIAL              HA248If+wwH/FFnFZd1Vqw=
IDENTITY_SOURCE     0
TACMEMBER           "NOC"
TACTYPE             AUTH
PASSWORD_ONESHOT    1

This is the result of a successfully ran mavistest command. Everything is working as expected.

IOS-XE configuration

Not using the default authentication/authorization list name in the AAA IOS configuration. Instead the VTY list name is defined to have a explicit overview how everything is connected in the IOS AAA configuration. The result is the same, but it is not relying on the defaults and much easier to spot in the configuration.

Routers R101 AAA configuration used in the netlab:

configure terminal
!
aaa new-model
!
tacacs server AAA-49
 address ipv4 10.100.200.49
 key 123-my_tacacs_key 
!
aaa group server tacacs+ AAA-group
 server name AAA-49
!
aaa authentication login VTY group AAA-group local
aaa authorization exec VTY group AAA-group
!
line vty 0 4
 authorization exec VTY
 login authentication VTY
 transport input telnet ssh
!
end
wr

Configuration issues

TACACS+NG does not send requests to the LDAP server at all.

That only is a problem if TACACS+NG is build from the git tree and missing modules are not installed automatically, happened to me on my linux gentoo workstation. I was not aware about it. The error will looks like below:

 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 session id: 6f2e97fe, data length: 14
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 AUTHEN/CONT user_msg_len=9, user_data_len=0
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 ---<end packet>---
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 authen: hdr->seq_no: 5
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 looking for user cisco realm default
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 user lookup succeded
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 evaluating ACL __internal__username_acl__
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101  regex: '[]<>/()|=[*"':$]+' <=> 'cisco' = 0
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101  line 513: [user] regex '[]<>/()|=[*"':$]+' => false
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101  line 513: [permit]
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 ACL __internal__username_acl__: match
 13437: 07:02:43.596 0/6f2e97fe: 10.100.100.101 looking for user cisco in MAVIS backend

 [...  runs into a timeout ,spawns a new TACACS+ session]

 13437: 07:02:52.599 1/5154f119: 10.100.100.101 New tacacs session

Prevention

To prevent this error from happening use

1) Simply run the following perl script that is shipped with TACACS+NG:

/usr/local/lib/mavis/mavis_tacplus-ng_ldap.pl

If everything is all right executing this script and the output will be left empty, if something is missing the module will spit out following message about missing Perl Module.

2) Use mavistest to test the connection to the LDAP server

mavistest /etc/tac_plus-ng/tac_plus-ng.cfg tac_plus-ng TACPLUS cisco cisco123

Solution

Install missing dev-perl/perl-ldap module:

root # emerge dev-perl/perl-ldap

This all is also described in the Mini-HowTo: Integrating TACACS+ (NG) with ActiveDirectory

Debugging

In the case of things not working out of the box in your environment it is good to enable verbose mode or debugging to find out where things are stuck. Often it is a typo, sometimes certain products do not have certain assumed defaults, for example if your environment runs different implementation of LDAP protocol like AD/openLDAP/etc. Various things can go wrong. Enable verbose debugging mode.

TACACS+NG

The available debugging options for this particular netlab are tac_plus-ng are:

debug = AUTHOR AUTHEN AV MAVIS

Or if you want to see even more because you have the feel you are missing a crucial part of information in the given debug just set:

debug = ALL

This is has been added to the upper configuration file, remove the comment to use it. And here a example for authenticating and authorize a user session while only certain debug options are enabled in the configuration file.

The debug can be enabled either in the config file, where several debug options at a time are cherry picked or stacked, or interactive in the command like when the daemon is ran in foreground. Notice the debug below displays user lookup failed which refers to its local configuration file no user found. This is not a failure but relates to that ALL user credentials are stored in LDAP directory.

tac_plus-ng -1 -f /etc/tac_plus-ng/tac_plus-ng.cfg
2057: 16:54:31.486 0/b194dd14: 10.100.100.101 authen: hdr->seq_no: 1
2057: 16:54:33.485 0/b194dd14: 10.100.100.101 authen: hdr->seq_no: 3
2057: 16:54:33.485 0/b194dd14: 10.100.100.101 looking for user cisco realm default
2057: 16:54:33.485 0/b194dd14: 10.100.100.101 user lookup failed
2057: 16:54:36.110 0/b194dd14: 10.100.100.101 authen: hdr->seq_no: 5
2057: 16:54:36.110 0/b194dd14: 10.100.100.101 looking for user cisco realm default
2057: 16:54:36.110 0/b194dd14: 10.100.100.101 user lookup failed
2057: 16:54:36.110 0/b194dd14: 10.100.100.101 looking for user cisco in MAVIS backend
2057: 16:54:36.550 0/b194dd14: 10.100.100.101 result for user cisco is ACK [440 ms]
2057: 16:54:36.550 0/b194dd14: 10.100.100.101 looking for user cisco realm default
2057: 16:54:36.550 0/b194dd14: 10.100.100.101 user lookup succeded
2057: 16:54:36.550 0/b194dd14: 10.100.100.101 shell login for 'cisco' from 203.0.113.100 on tty2 succeeded (profile=netadmin)
2057: 16:54:36.552 1/24807529: 10.100.100.101 Start authorization request
2057: 16:54:36.552 1/24807529: 10.100.100.101 user 'cisco' found
2057: 16:54:36.552 1/24807529: 10.100.100.101 nas:service=shell (passed thru)
2057: 16:54:36.552 1/24807529: 10.100.100.101 nas:cmd* (passed thru)
2057: 16:54:36.552 1/24807529: 10.100.100.101 nas:absent srv:priv-lvl=15 -> add priv-lvl=15 (k)
2057: 16:54:36.552 1/24807529: 10.100.100.101 added 1 args

LLDAP

To run the LLDAP in debug mode comment out the verbose = true line in the lldap_config.toml configuration, then run the LLDAP daemon like following, stop the daemon:

rc-service lldap stop

Then run lldap manually:

cd /opt/lldap/
./lldap run

This will produce verbose LDAP logging while the daemon runs. All debug messages for only one successful user authentication and authorization run. Unrelated debug messages have been removed using the LLDAP ignore_* option:

2026-01-15T14:33:25.267618867+00:00  INFO     LDAP request [ 94.5ms | 0.13% / 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.267647550+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 24, op: BindRequest(LdapBindRequest { dn: "uid=ro,ou=people,dc=example,dc=org", cred: LdapBindCred::Simple }), ctrl: [] }
2026-01-15T14:33:25.267658822+00:00  DEBUG    โ”โ” do_bind [ 94.4ms | 0.00% / 99.87% ] dn: uid=ro,ou=people,dc=example,dc=org
2026-01-15T14:33:25.267682346+00:00  DEBUG    โ”‚  โ”โ” bind [ 93.9ms | 0.13% / 99.34% ]
2026-01-15T14:33:25.267690401+00:00  DEBUG    โ”‚  โ”‚  โ”โ” get_password_file_for_user [ 478ยตs | 0.51% ] user_id: "ro"
2026-01-15T14:33:25.268279756+00:00  INFO     โ”‚  โ”‚  โ”โ” ๏ฝ‰ [info]: Login attempt for "ro"
2026-01-15T14:33:25.268284925+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” passwords_match [ 93.3ms | 98.70% ] username: ro
2026-01-15T14:33:25.361583157+00:00  DEBUG    โ”‚  โ”โ” get_user_groups [ 498ยตs | 0.53% ] user_id: "ro"
2026-01-15T14:33:25.362096630+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” ๐Ÿ› [debug]:  | return: {GroupDetails { group_id: 3, display_name: "lldap_strict_readonly", creation_date: 2026-01-15T14:12:28.925844452, uuid: "63adbc1c-c803-352b-9685-d515a199a0c2", attributes: [] }}
2026-01-15T14:33:25.362106919+00:00  DEBUG    โ”‚  โ”•โ” ๐Ÿ› [debug]: Success!
2026-01-15T14:33:25.362114393+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: BindResponse(LdapBindResponse { res: LdapResult { code: Success, matcheddn: "", message: "", referral: [] }, saslcreds: None })
2026-01-15T14:33:25.381032831+00:00  INFO     LDAP request [ 298ยตs | 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.381062968+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 25, op: SearchRequest(LdapSearchRequest { base: "", scope: Base, aliases: FindingBaseObj, sizelimit: 0, timelimit: 0, typesonly: false, filter: Present("objectClass"), attrs: ["subschemaSubentry", "namingContexts", "altServer", "supportedExtension", "supportedControl", "supportedFeatures", "supportedSASLMechanisms", "supportedLDAPVersion", "vendorName", "vendorVersion"] }), ctrl: [] }
2026-01-15T14:33:25.381068588+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]: rootDSE request
2026-01-15T14:33:25.381089467+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | response: SearchResultEntry(LdapSearchResultEntry { dn: "", attributes: [LdapPartialAttribute { atype: "objectClass", vals: ["top"] }, LdapPartialAttribute { atype: "vendorName", vals: ["LLDAP"] }, LdapPartialAttribute { atype: "vendorVersion", vals: ["lldap_0.1.0"] }, LdapPartialAttribute { atype: "supportedLDAPVersion", vals: ["3"] }, LdapPartialAttribute { atype: "supportedExtension", vals: ["1.3.6.1.4.1.4203.1.11.1", "1.3.6.1.4.1.4203.1.11.3"] }, LdapPartialAttribute { atype: "supportedControl", vals: [] }, LdapPartialAttribute { atype: "supportedFeatures", vals: ["1.3.6.1.4.1.4203.1.5.1"] }, LdapPartialAttribute { atype: "defaultNamingContext", vals: ["dc=example,dc=org"] }, LdapPartialAttribute { atype: "namingContexts", vals: ["dc=example,dc=org"] }, LdapPartialAttribute { atype: "isGlobalCatalogReady", vals: ["false"] }, LdapPartialAttribute { atype: "subschemaSubentry", vals: ["cn=Subschema"] }] })
2026-01-15T14:33:25.381315842+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: SearchResultDone(LdapResult { code: Success, matcheddn: "", message: "", referral: [] })
2026-01-15T14:33:25.429792013+00:00  INFO     LDAP request [ 80.1ms | 0.25% / 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.429804386+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 26, op: BindRequest(LdapBindRequest { dn: "uid=ro,ou=people,dc=example,dc=org", cred: LdapBindCred::Simple }), ctrl: [] }
2026-01-15T14:33:25.429807902+00:00  DEBUG    โ”โ” do_bind [ 79.9ms | 0.00% / 99.75% ] dn: uid=ro,ou=people,dc=example,dc=org
2026-01-15T14:33:25.429817110+00:00  DEBUG    โ”‚  โ”โ” bind [ 79.6ms | 0.09% / 99.35% ]
2026-01-15T14:33:25.429820747+00:00  DEBUG    โ”‚  โ”‚  โ”โ” get_password_file_for_user [ 116ยตs | 0.14% ] user_id: "ro"
2026-01-15T14:33:25.429996286+00:00  INFO     โ”‚  โ”‚  โ”โ” ๏ฝ‰ [info]: Login attempt for "ro"
2026-01-15T14:33:25.429998190+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” passwords_match [ 79.4ms | 99.12% ] username: ro
2026-01-15T14:33:25.509388978+00:00  DEBUG    โ”‚  โ”โ” get_user_groups [ 325ยตs | 0.41% ] user_id: "ro"
2026-01-15T14:33:25.509724808+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” ๐Ÿ› [debug]:  | return: {GroupDetails { group_id: 3, display_name: "lldap_strict_readonly", creation_date: 2026-01-15T14:12:28.925844452, uuid: "63adbc1c-c803-352b-9685-d515a199a0c2", attributes: [] }}
2026-01-15T14:33:25.509731039+00:00  DEBUG    โ”‚  โ”•โ” ๐Ÿ› [debug]: Success!
2026-01-15T14:33:25.509739566+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: BindResponse(LdapBindResponse { res: LdapResult { code: Success, matcheddn: "", message: "", referral: [] }, saslcreds: None })
2026-01-15T14:33:25.542624637+00:00  INFO     LDAP request [ 2.20ms | 0.38% / 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.542641018+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 27, op: SearchRequest(LdapSearchRequest { base: "dc=example,dc=org", scope: Subtree, aliases: FindingBaseObj, sizelimit: 0, timelimit: 0, typesonly: false, filter: And([Equality("objectclass", "posixAccount"), Equality("uid", "cisco")]), attrs: ["shadowExpire", "memberOf", "dn", "uidNumber", "gidNumber", "loginShell", "homeDirectory", "sshPublicKey", "krbPasswordExpiration", "tacMember"] }), ctrl: [] }
2026-01-15T14:33:25.542653601+00:00  DEBUG    โ”โ” do_search [ 2.20ms | 0.00% / 99.62% ]
2026-01-15T14:33:25.542655705+00:00  DEBUG    โ”‚  โ”•โ” do_search [ 2.20ms | 0.00% / 99.62% ]
2026-01-15T14:33:25.542953223+00:00  DEBUG    โ”‚     โ”โ” ๐Ÿ› [debug]:  | request.base: "dc=example,dc=org" | scope: Global
2026-01-15T14:33:25.542955247+00:00  DEBUG    โ”‚     โ”โ” get_user_list [ 1.01ms | 0.00% / 45.83% ]
2026-01-15T14:33:25.542962661+00:00  DEBUG    โ”‚     โ”‚  โ”โ” ๐Ÿ› [debug]:  | filters: And([True, UserId("cisco")])
2026-01-15T14:33:25.542968271+00:00  DEBUG    โ”‚     โ”‚  โ”•โ” list_users [ 1.01ms | 45.83% ] filters: Some(And([True, UserId("cisco")])) | _get_groups: true
2026-01-15T14:33:25.543914456+00:00  DEBUG    โ”‚     โ”‚     โ”•โ” ๐Ÿ› [debug]:  | return: [UserAndGroups { user: User { user_id: "cisco", email: "cisco@example.org", display_name: Some("Cisco"), creation_date: 2026-01-15T14:14:13.808237542, uuid: "8cefc3b8-c728-321e-beca-25212bbd8726", attributes: [Attribute { name: AttributeName(CaseInsensitiveString("first_name")), value: String(Singleton("San")) }, Attribute { name: AttributeName(CaseInsensitiveString("last_name")), value: String(Singleton("Francisco")) }] }, groups: Some([GroupDetails { group_id: 4, display_name: "NOC", creation_date: 2026-01-15T14:15:12.223338125, uuid: "4d12dab9-1422-3828-aa55-ac6805dea25d", attributes: [] }]) }]
2026-01-15T14:33:25.543919766+00:00  DEBUG    โ”‚     โ”โ” get_groups_list [ 1.18ms | 0.00% / 53.34% ]
2026-01-15T14:33:25.543926117+00:00  DEBUG    โ”‚     โ”‚  โ”โ” ๐Ÿ› [debug]:  | filters: And([False, DisplayName("cisco")])
2026-01-15T14:33:25.543929173+00:00  DEBUG    โ”‚     โ”‚  โ”•โ” list_groups [ 1.18ms | 53.34% ] filters: Some(And([False, DisplayName("cisco")]))
2026-01-15T14:33:25.544832657+00:00  DEBUG    โ”‚     โ”‚     โ”•โ” ๐Ÿ› [debug]:  | return: []
2026-01-15T14:33:25.544842465+00:00  DEBUG    โ”‚     โ”•โ” expand_attribute_wildcards [ 10.1ยตs | 0.46% ] ldap_attributes: ["shadowExpire", "memberOf", "dn", "uidNumber", "gidNumber", "loginShell", "homeDirectory", "sshPublicKey", "krbPasswordExpiration", "tacMember"]
2026-01-15T14:33:25.544852704+00:00  DEBUG    โ”‚        โ”•โ” ๐Ÿ› [debug]:  | attributes_out: {AttributeName(CaseInsensitiveString("dn")): "dn", AttributeName(CaseInsensitiveString("gidnumber")): "gidNumber", AttributeName(CaseInsensitiveString("homedirectory")): "homeDirectory", AttributeName(CaseInsensitiveString("krbpasswordexpiration")): "krbPasswordExpiration", AttributeName(CaseInsensitiveString("loginshell")): "loginShell", AttributeName(CaseInsensitiveString("memberof")): "memberOf", AttributeName(CaseInsensitiveString("shadowexpire")): "shadowExpire", AttributeName(CaseInsensitiveString("sshpublickey")): "sshPublicKey", AttributeName(CaseInsensitiveString("tacmember")): "tacMember", AttributeName(CaseInsensitiveString("uidnumber")): "uidNumber"}
2026-01-15T14:33:25.544882540+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | response: SearchResultEntry(LdapSearchResultEntry { dn: "uid=cisco,ou=people,dc=example,dc=org", attributes: [LdapPartialAttribute { atype: "memberOf", vals: ["cn=NOC,ou=groups,dc=example,dc=org"] }] })
2026-01-15T14:33:25.545126598+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: SearchResultDone(LdapResult { code: Success, matcheddn: "", message: "", referral: [] })
2026-01-15T14:33:25.630682124+00:00  INFO     LDAP request [ 1.18ms | 20.96% / 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.630696090+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 28, op: SearchRequest(LdapSearchRequest { base: "cn=NOC,ou=groups,dc=example,dc=org", scope: Base, aliases: FindingBaseObj, sizelimit: 0, timelimit: 0, typesonly: false, filter: Present("objectclass"), attrs: ["memberOf"] }), ctrl: [] }
2026-01-15T14:33:25.630698885+00:00  DEBUG    โ”โ” do_search [ 936ยตs | 3.28% / 79.04% ]
2026-01-15T14:33:25.630700488+00:00  DEBUG    โ”‚  โ”•โ” do_search [ 897ยตs | 19.52% / 75.75% ]
2026-01-15T14:33:25.631011943+00:00  DEBUG    โ”‚     โ”โ” ๐Ÿ› [debug]:  | request.base: "cn=NOC,ou=groups,dc=example,dc=org" | scope: Group(Equality("cn", "noc"))
2026-01-15T14:33:25.631014517+00:00  DEBUG    โ”‚     โ”โ” get_groups_list [ 662ยตs | 5.65% / 55.92% ]
2026-01-15T14:33:25.631020709+00:00  DEBUG    โ”‚     โ”‚  โ”โ” ๐Ÿ› [debug]:  | filters: And([True, DisplayName("noc")])
2026-01-15T14:33:25.631025187+00:00  DEBUG    โ”‚     โ”‚  โ”•โ” list_groups [ 595ยตs | 50.27% ] filters: Some(And([True, DisplayName("noc")]))
2026-01-15T14:33:25.631718387+00:00  DEBUG    โ”‚     โ”‚     โ”•โ” ๐Ÿ› [debug]:  | return: [Group { id: 4, display_name: "NOC", creation_date: 2026-01-15T14:15:12.223338125, uuid: "4d12dab9-1422-3828-aa55-ac6805dea25d", users: ["cisco", "huawei"], attributes: [] }]
2026-01-15T14:33:25.631727945+00:00  DEBUG    โ”‚     โ”โ” expand_attribute_wildcards [ 3.76ยตs | 0.32% ] ldap_attributes: ["memberOf"]
2026-01-15T14:33:25.631731752+00:00  DEBUG    โ”‚     โ”‚  โ”•โ” ๐Ÿ› [debug]:  | attributes_out: {AttributeName(CaseInsensitiveString("memberof")): "memberOf"}
2026-01-15T14:33:25.631738314+00:00  WARN     โ”‚     โ”•โ” ๐Ÿšง [warn]: Ignoring unrecognized group attribute: memberof. To disable this warning, add it to "ignored_group_attributes" in the config.
2026-01-15T14:33:25.631748613+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | response: SearchResultEntry(LdapSearchResultEntry { dn: "cn=NOC,ou=groups,dc=example,dc=org", attributes: [] })
2026-01-15T14:33:25.631960350+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: SearchResultDone(LdapResult { code: Success, matcheddn: "", message: "", referral: [] })
2026-01-15T14:33:25.684060195+00:00  INFO     LDAP request [ 69.0ms | 0.27% / 100.00% ] session_id: 87870adb-1967-41a1-8401-c6f3ec34ca31
2026-01-15T14:33:25.684072588+00:00  DEBUG    โ”โ” ๐Ÿ› [debug]:  | msg: LdapMsg { msgid: 29, op: BindRequest(LdapBindRequest { dn: "uid=cisco,ou=people,dc=example,dc=org", cred: LdapBindCred::Simple }), ctrl: [] }
2026-01-15T14:33:25.684075714+00:00  DEBUG    โ”โ” do_bind [ 68.8ms | 0.00% / 99.73% ] dn: uid=cisco,ou=people,dc=example,dc=org
2026-01-15T14:33:25.684084851+00:00  DEBUG    โ”‚  โ”โ” bind [ 68.5ms | 0.04% / 99.34% ]
2026-01-15T14:33:25.684088117+00:00  DEBUG    โ”‚  โ”‚  โ”โ” get_password_file_for_user [ 131ยตs | 0.19% ] user_id: "cisco"
2026-01-15T14:33:25.684237638+00:00  INFO     โ”‚  โ”‚  โ”โ” ๏ฝ‰ [info]: Login attempt for "cisco"
2026-01-15T14:33:25.684239962+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” passwords_match [ 68.4ms | 99.12% ] username: cisco
2026-01-15T14:33:25.752612043+00:00  DEBUG    โ”‚  โ”โ” get_user_groups [ 264ยตs | 0.38% ] user_id: "cisco"
2026-01-15T14:33:25.752883192+00:00  DEBUG    โ”‚  โ”‚  โ”•โ” ๐Ÿ› [debug]:  | return: {GroupDetails { group_id: 4, display_name: "NOC", creation_date: 2026-01-15T14:15:12.223338125, uuid: "4d12dab9-1422-3828-aa55-ac6805dea25d", attributes: [] }}
2026-01-15T14:33:25.752887780+00:00  DEBUG    โ”‚  โ”•โ” ๐Ÿ› [debug]: Success!
2026-01-15T14:33:25.752894964+00:00  DEBUG    โ”•โ” ๐Ÿ› [debug]:  | response: BindResponse(LdapBindResponse { res: LdapResult { code: Success, matcheddn: "", message: "", referral: [] }, saslcreds: None })

The careful reader will spot the LLDAP debug messages show default LDAP attributes like:

  • tacmember
  • shadowexpire

I am curious how to configure user password changes requests using TACACS+NG and LDAP. Another netlab would be interesting on how to setup password expiration dates in that particular setup with LLDAP and TACACS+NG.

See also

Continue with the follow up GNS netlab about the LDAPS configuration setup:

This is a how to build a tiny alpinelinux GNS3 appliance for your virtual network laboratory. The appliance is not bigger than 500MB and fully functional.

Read many other blog entries about TACACS+NG and several other network operating systems, when you want to netlab and do not have a commercial network operating system at hand. Try out using freeRtr or EXOS which are free to download and work in GNS3.

References