Stand up TACACS+ device administration
When you need this
Section titled “When you need this”- “Who logged into that switch last night, and what did they type?”
- “Give the network team a shell on the fabric from their AD credentials, and take it away when someone leaves — in one place.”
- “Show me every
configure-level command run this week, as of the moment it ran.”
TACACS+ is the other half of AAA from 802.1X: RADIUS decides whether a device joins the network; TACACS+ decides whether an engineer may log into a switch and run a command. l2trace runs both.
Step 1 — the local break-glass account (not optional)
Section titled “Step 1 — the local break-glass account (not optional)”Before pointing any switch at l2trace, on each device:
username breakglass privilege 15 secret <a-real-password>aaa authentication login default group tacacs+ localaaa authorization exec default group tacacs+ localThen log in with breakglass and confirm it works. A NAS only falls through to the
local account when the TACACS+ server is unreachable or returns ERROR — which, by the
doctrine, is what it does on any fault. The local account is your way back in. Test it now,
while nothing depends on it.
Step 2 — an identity source and a service account
Section titled “Step 2 — an identity source and a service account”l2trace authenticates engineers by binding to Active Directory as the user (so it needs
LDAPS), and reads their group membership with a service account (authorization is a
separate TACACS+ exchange that carries no password, so it cannot bind as the user to read
their own groups). In .env:
TACACS_ENABLED=trueTACACS_SHARED_SECRET=env://TACACS_SECRET # per-device secrets are better; see Step 3
# Authenticate the engineer (LDAPS is required — a simple bind sends the real password):LDAP_URI=ldaps://dc.hospital.exampleLDAP_USER_TEMPLATE={username}@hospital.example
# Read their groups (least privilege: read memberOf on user objects, nothing else):LDAP_BIND_DN=CN=svc-tacacs,OU=Service,DC=hospital,DC=exampleLDAP_BIND_PASSWORD=env://TACACS_SVC_PW # a secrets URI, so it stays out of the DBLDAP_SEARCH_BASE=DC=hospital,DC=examplel2trace refuses to start without an identity source, on purpose: a login server that
can authenticate nobody is worse than one that is plainly absent, because absence lets
every NAS fall through to local.
Step 3 — register each switch
Section titled “Step 3 — register each switch”A switch is a device with an enabled device_collector row naming source tacacs —
the same registration path you already use for SNMP. Its shared secret is a secrets URI,
so it never lives in the database:
docker compose exec reconciler l2trace device register \ --hostname sw-lab-1 --mgmt-ip 192.0.2.10docker compose exec reconciler l2trace device add-collector \ --hostname sw-lab-1 --source tacacs \ --auth tacacs_secret=env://SW_LAB_1_SECRET \ --extra tacacs_dialect=ios # ios | nxos | arista | genericThe tacacs_dialect matters because it decides how a privilege is phrased on the wire:
IOS understands priv-lvl, while NX-OS and EOS want shell:roles. Send the wrong one as
a mandatory argument and the switch fails the authorization and denies the engineer, so
l2trace only ever sends a role to a dialect known to understand it. Unknown or unset →
generic, which sends only priv-lvl (every Cisco-family device understands it).
Step 4 — deploy (host 49 → container 4949)
Section titled “Step 4 — deploy (host 49 → container 4949)”TACACS+‘s well-known port is 49, which is privileged, and the container runs as a non-root user. So it listens high and the compose overlay publishes host 49 to it — Docker does the privileged bind:
docker compose -f docker-compose.yml -f docker-compose.tacacs.yml up -d tacacsdocker compose logs -f tacacsPoint a lab switch at it (tacacs server ... address ipv4 <l2trace-host> / key),
and log in with an AD credential. It works because monitor mode changes nothing — see the
next step.
Not shown but worth bringing up a lab switch on first:
l2trace tacacs-lab is a standalone, database-free runner
that proves the wire protocol against a real Catalyst before you wire in AD and Postgres.
Step 5 — map AD groups to privileges and commands
Section titled “Step 5 — map AD groups to privileges and commands”Authorization is driven by AD group, first-match wins, from .env. A shell profile sets
the privilege level; a command set filters what may be run:
# priv-lvl / roles per group (first match wins — put admins first):TACACS_GROUP_PROFILES='[ {"group":"NetAdmins","priv_lvl":15,"roles":["network-admin"]}, {"group":"NetOps","priv_lvl":7,"idletime":10}]'
# ordered permit/deny command rules per group (default-deny once a set applies):TACACS_GROUP_COMMAND_SETS='[ {"group":"NetAdmins","default":"permit"}, {"group":"NetOps","default":"deny", "rules":[{"permit":"show .*"},{"deny":"configure .*"}]}]'Both share one directory lookup per login. Command patterns are full-match (show
permits only show; show .* permits every show command), so a permit conf cannot
quietly authorize configure terminal.
Those patterns are matched with RE2 (google-re2), not Python’s re, on purpose: RE2
matches in guaranteed linear time, so a sloppy operator pattern can never catastrophically
backtrack (a ReDoS) and stall the authorization path for the whole fabric. The trade-off is
that RE2 rejects backreferences and lookaround — a pattern that uses them fails to
compile, and a rule l2trace can’t compile is skipped and logged as a non-match rather
than silently matching. A single bad rule can therefore never, by itself, permit or deny
every command; the set’s later rules and its default decide.
Step 6 — watch, in monitor mode, before enforcing
Section titled “Step 6 — watch, in monitor mode, before enforcing”Everything above runs with TACACS_MONITOR_MODE=true (the default). Logins are answered
truthfully — an AD password works or it does not — but authorization changes nothing:
the server computes the profile and the command verdict, logs what it would apply, and
returns a no-op. Every login and every command lands in the audit trail. Open the
Commands view in the web UI to see per-command accounting, newest first, under the
DeLorean as-of control — “what commands had run by time T” is exactly what an incident
review asks.
Run like this for as long as it takes to trust the would-denies in the log.
Step 7 — enforce, on lab switches only
Section titled “Step 7 — enforce, on lab switches only”Enforcement is triple-gated, so turning it on takes three deliberate acts:
TACACS_MONITOR_MODE=false # global: stop flattening decisions to no-opsTACACS_ENFORCEMENT_PERMITTED=true # global arm# and per lab device:l2trace device add-collector --hostname sw-lab-1 --source tacacs --extra tacacs_enforce=trueA per-device tacacs_enforce flag can never enforce by itself; all three must agree.
Prove the full path on the operator’s spare lab switches — real login, correct
privilege, a denied command, and a recovered login after killing the server (it should
fall through to breakglass) — and only then consider a production port.
See also
Section titled “See also”- The TACACS+ lockout doctrine — why ERROR and never FAIL, and the three fail directions. Required reading before enforcement.
- Stand up 802.1X in monitor mode — the RADIUS half of AAA.