Skip to content

Diagnostic bundles

./taranac diagnose is one command with four collectors behind it. Each answers a different question, and each hands you one thing you can attach to a ticket whole, without gathering anything else by hand.

RunWhenWhat you get
--protocol tacacs|radius|nac --device <IP>Device administration or 802.1X fails for one deviceOne .tar.gz: the traffic decoded, the daemon log for those seconds, the records Taranac wrote, the generated config — and, on an 802.1X run, the CoA exchange too
--protocol ncm --config "<name>"A configuration collection fails, or never gets as far as failingOne text report: what was resolved before any network, whether the device answered, what the SSH handshake agreed on, and the session itself
--protocol snmp --device "<name>"SNMP polling fails, or a device’s inventory, ports or forwarding table come back emptyOne text report: whether the device is polled and by whom, which profile and address apply, the SNMPv3 engine-ID exchange, every request with its timing, and what the device did and did not answer
--haAnything about a cluster: a node that will not start, a failover that should not have happened, replication that stoppedOne .tar.gz per node: role and timeline, the etcd ring, Patroni’s view, replication, reachability, certificates, clocks and logs

The first is a capture — it records a window while you reproduce a fault. The others are not: an NCM collection and an SNMP poll are sessions Taranac itself opens, and HA faults are state rather than events. That difference is why they are separate collectors and not flags on one.

For host-level problems — high CPU, memory pressure, crash-looping services — start at Troubleshooting & diagnostics instead.

Everything from here to Reading decoded.txt is the traffic collector — --protocol tacacs, radius or nac. The other collectors have their own sections below.

When device administration or 802.1X stops working for a device, the explanation is almost never in one place. It is spread across four: the packets on the wire, the daemon’s own log for those few seconds, the records Taranac wrote at the time, and how the device is configured. Nobody gathers four things by hand while the problem is happening. This collector gathers all four for one device and one protocol, decodes the traffic so the archive is readable, prints in plain words what it noticed, and packs everything into a single .tar.gz. For 802.1X there is a fifth place — the backend’s own log, the only record of a CoA — and it is collected as well.

Four things must be true. The collector checks each of them and stops with a specific message rather than producing an empty archive:

RequirementWhyIf it is missing
tcpdump installed on the hostIt is what captures the traffic. The daemon images ship without it, and installing it into a container does not survive the next recreate. The virtual appliance has it installed.The collector prints the exact install command for your package manager and exits.
root, or passwordless sudoCapturing packets needs it.It tells you to re-run with sudo.
The protocol’s daemon container runningNothing would be captured, because nothing is listening.It names the container and tells you to start the stack.
The device’s IP address — not a hostnameTaranac matches clients by IP. A name that resolves elsewhere would quietly capture the wrong traffic.The address is validated (including octet ranges) before tcpdump is ever invoked.

One thing the tool cannot check for you: the address must be the one the device sends from. That is its source IP — often a loopback or management interface, not the address you SSH into. Getting this wrong is the single commonest cause of an empty capture.

Terminal window
./taranac diagnose --protocol tacacs --device 10.0.0.5

Start it, reproduce the problem on the device, press Enter. That is the whole workflow.

OptionMeaning
--protocol Ptacacs (device administration), radius (device administration over RADIUS) or nac (802.1X port access). Required. ncm selects a different collector entirely.
--device IPThe network device’s IP address, as Taranac sees it. Required.
--duration SECCapture for a fixed number of seconds instead of waiting for Enter.
--out DIRWhere to write the archive. Default: ./diagnostics.
--max-size MBStop the capture past this size. Default: 100.
-h, --helpUsage — for every collector.

Use --duration when reproducing the fault and watching the terminal are not the same person’s job — start the collector, tell the other end to try, walk back.

The archive is named for what it contains: taranac-diag-<protocol>-<device>-<UTC timestamp>.tar.gz.

The order matters: the two checks come before the capture, because the answer is frequently already there.

1. How is this address configured in Taranac? Every network object covering the address is listed — one IP can sit inside a host entry and two subnets at once, each with its own key or inheriting a different one — most specific first, since that is the one the daemon applies. For each: whether the device record is enabled, whether the protocol is switched on for it, where its shared secret comes from and that secret’s fingerprint.

Three conditions are reported as findings here, because each one makes the configuration generator skip the device entirely — so the daemon has no client entry for it and drops its requests before any policy is evaluated:

  • the device record is disabled;
  • the protocol is off for that device, even though the device exists;
  • no shared secret resolves — device, groups and the global setting are all empty.

If nothing in Taranac covers the address at all, that is said outright. For RADIUS and 802.1X the dynamic-client networks are consulted too, so a device accepted with the global secret and no device record of its own is reported as such rather than as “not found”.

2. Which ports is the daemon actually listening on? Read from the running process, never from the documented defaults, and combined with the published host ports — with a remapped port the packet carries one number on the physical interface and another on the Docker bridge after NAT, and filtering on only one set silently loses half the story. Loopback-only helper sockets are dropped from the filter.

On a nac run the filter is widened once more, with the CoA ports. Change of Authorization (RFC 5176) is the one direction the daemon never sees: a disconnect, a VLAN change, a port bounce and the CoA test all leave the api container for a port on the device, and nothing in the daemon container listens on it. Filtering on the daemon’s ports alone therefore hides every one of them — and hides the switch’s answer, which is usually the whole question. The ports come from the configuration check above, which resolves them per matching device; when they are not already in the daemon’s own set, the run says → Capturing CoA as well, on port(s): … on its own line.

This check exists because a device whose protocol flag is off and a daemon on a custom port produce identical symptoms and identical empty captures.

3. Capture. tcpdump runs on the host, on all interfaces, filtered to that device and those ports, with full-length packets. Capturing on any sees both the physical interface and the Docker bridge — so a packet appearing twice means it was forwarded, and a packet appearing only on the physical interface means it never reached the container, which is a firewall or NAT problem rather than a Taranac one.

The filter also takes ICMP from that address. A port-unreachable is how a switch says nothing was listening where Taranac sent — and for a CoA that is the difference between the switch ignored our request and the request never had a listener, two opposite diagnoses that a ports-only filter cannot tell apart. ICMP is not decoded; it sits in capture.pcap, where Wireshark names it in one line.

4. Collect. The daemon’s log is sliced to the capture window by byte offset taken before and after — not by timestamp, because the daemons log in three different time formats and a date parser that misreads one returns nothing silently. Then: the records Taranac itself wrote during the window, the generated daemon configuration with secrets masked, short context tails from the neighbouring services, and the backend’s own log sliced to the same window — the only place a CoA outcome is written down.

5. Decode. The capture is decoded inside the api container — capture in, readable text out, the key never leaves.

6. Report. A summary is printed to your terminal and written into the archive as summary.txt.

This is a real collection from the demo platform — a TACACS+ login and a few commands on a Cisco switch, reproduced while the collector was running:

Terminal window
$ sudo ./taranac diagnose --protocol tacacs --device 10.30.0.14
Taranac diagnostic collector
protocol : tacacs
device : 10.30.0.14
daemon : taranac-tacacs
→ Checking how 10.30.0.14 is configured in Taranac…
device: SW6-DEMO [10.30.0.14/32] - enabled, protocol on
secret from device, fingerprint 709ad291ecf8
groups: Cisco Devices, Demo Devices
device: demo-network [10.30.0.0/24] - enabled, protocol on
secret from device, fingerprint 6aabedfde070
groups: Demo Devices
! 2 network objects contain 10.30.0.14; the daemon uses the most specific one (10.30.0.14/32). Differing keys between them is a known misconfiguration class — compare the fingerprints above.
→ taranac-tacacs is listening on: 49,6049
→ Capturing on all interfaces, filter: host 10.30.0.14 and ((port 49 or port 6049) or icmp)
════════════════════════════════════════════════════════════════
Capturing. Reproduce the problem on 10.30.0.14 now.
Press Enter when you are done.
════════════════════════════════════════════════════════════════
→ Capture stopped.
Taranac diagnostic bundle
=========================
protocol : tacacs
device : 10.30.0.14
window : 2026-08-13T21:16:51Z .. 2026-08-13T21:17:45Z
packets : 387
How this address is configured
------------------------------
2 object(s) in Taranac cover this address:
SW6-DEMO [10.30.0.14/32] host <- most specific, used by the daemon
enabled=True protocol=on key from device (fp 709ad291ecf8)
groups: Cisco Devices, Demo Devices
demo-network [10.30.0.0/24] subnet
enabled=True protocol=on key from device (fp 6aabedfde070)
groups: Demo Devices
Findings
--------
[note] 2 network objects contain 10.30.0.14; the daemon uses the most specific one (10.30.0.14/32). Differing keys between them is a known misconfiguration class — compare the fingerprints above.
[traffic] 387 packets captured.
[ok] Taranac reacted: 13 database record(s), 12769 bytes of daemon log.
[decode] Traffic decoded into readable text — see decoded.txt.
Contents
--------
capture.pcap — raw traffic (387 packets; open in Wireshark)
config/daemon-config.masked.txt — generated daemon config, secrets masked
db-logs.json — what Taranac recorded during the window (13 rows)
decoded.txt — the exchange decoded into readable text (no secret included)
environment.txt — versions, containers, capture settings
logs/accounting_202608_2026-08-13.log — daemon log output during the window
logs/authentication_202608_2026-08-13.log — daemon log output during the window
logs/authorization_202608_2026-08-13.log — daemon log output during the window
logs/context/taranac-nac_radius_nac.log — recent tail from a neighbouring service (context, not the window)
logs/mavis_stderr.log — daemon log output during the window
logs/syslog.log — daemon log output during the window
preflight.json — how the device is configured in Taranac
tcpdump.stderr — capture tool output (empty unless something went wrong)
Note on secrets: no shared secret is included. Secrets appear only as a
12-hex fingerprint. To decode TACACS+ payloads in Wireshark yourself, enter
the device's key locally — it is not, and must not be, in this archive.
Archive: /home/taranac-demo/taranac/diagnostics/taranac-diag-tacacs-10_30_0_14-20260813-211649.tar.gz (24K)
Send this file to support.

Two things in that output are worth pausing on, because they are the tool doing its job before any packet was read.

The address is covered by two network objects with different keys. SW6-DEMO as a /32 host entry and demo-network as the surrounding /24, each carrying its own shared secret — fingerprints 709ad291ecf8 and 6aabedfde070. The daemon applies the most specific one, and the collector says which that is. If the switch had been configured with the other key, every request would have gone unanswered with nothing in any log to explain it; here the two fingerprints sitting side by side are the whole diagnosis.

The daemon is listening on 49,6049, not just the documented 49. That second port is read from the running process — which is the point of the check. Ports differ per protocol and per installation; the same three protocols on this platform report:

Terminal window
→ taranac-tacacs is listening on: 49,6049
→ taranac-radius is listening on: 1812,1813
→ taranac-nac is listening on: 1814,1815,18140

A capture filtered on a port the daemon is not actually using comes back empty, and an empty capture is indistinguishable from “the device sent nothing”. (18140 is the NAC daemon’s internal status listener. The NAC daemon listens for no CoA: CoA leaves the api, which is why a nac run adds the device’s CoA ports separately, as described above.)

Capturing packets needs root. The collector checks before it does any work and tells you how to re-run rather than failing later inside tcpdump:

Terminal window
$ ./taranac diagnose --protocol tacacs --device 10.30.0.14
…
→ taranac-tacacs is listening on: 49,6049
✗ Capturing packets needs root, and this account cannot sudo without a password.
Re-run as root: sudo /home/taranac-demo/taranac/diagnose.sh --protocol tacacs --device 10.30.0.14

Note that the configuration check and the port check still ran — so even a refused collection tells you how the device is set up and where the daemon is listening. Simplest practice: run the collector with sudo from the start.

The summary opens with the capture parameters, then how the address is configured, then Findings. Each finding carries a label, and the vocabulary is small enough to learn once:

LabelMeaning
[config]A configuration fact that excludes this device from the daemon’s config — disabled record, protocol off, no secret resolves, or no matching object at all.
[note]Context rather than a fault: several objects cover the address, a dynamic-client network explains a device with no record, or — on a nac run — a CoA fact worth naming: no CoA secret resolves, the vendor entry declares the liveness probe unsupported, or the device is not pinned to a Vendor Dictionary entry.
[traffic]How many packets were captured — or, at the end, that the capture hit its size cap and wrapped.
[EMPTY]No traffic at all. The bundle cannot show anything; do not send it.
[ok]Taranac reacted: n database records and n bytes of daemon log.
[!]Packets arrived but nothing was recorded. Two distinct variants — see below.
[?]Packets arrived and the daemon logged, but no database record was written for this device, and the bundle cannot tell which of the usual causes it is — see below.
[KEY]The captured bodies do not decode with the key Taranac holds. The two sides disagree about the shared secret. Reached only for a session in which no packet has ever decoded — see below.
[decode]The traffic was decoded into readable text; read decoded.txt.
[logs]A log rotated or was trimmed mid-capture, so the bundle carries a bounded tail of that file instead of an exact slice.
[IPv6]The host cannot give IPv6 devices their own address — Docker older than 28, Docker’s ip6tables off, no IPv6 in the kernel, a daemon port with no IPv6 listener or DNAT — or inventory entries cover the Docker gateway while IPv6 devices still arrive from it. See IPv6.

The three findings worth knowing before you need them

Section titled “The three findings worth knowing before you need them”

[EMPTY] — nothing arrived. The collector refuses to end an empty run with “send this to support”. Instead it lists what to check: that the problem really was reproduced while the collector was running, that the address is the one the device sends from, that the device points at this server on those ports, and that nothing on the path drops it. An archive with no traffic in it cannot show anything, and mailing one costs a round-trip.

[!] — packets arrived, Taranac recorded nothing. This is a different problem from “policy said no”, and the two look identical to whoever reports that it does not work. The collector separates two cases:

  • No reply was sent either. The requests are being discarded before any policy runs. Usual causes: the device is not a configured client, the shared secret does not match, or it is sending a different protocol to that port.
  • The daemon did answer — its replies are in the decode — but there is no log line and no database row. The exchange started and never completed: the device stopped mid-conversation, or gave up before sending credentials.

When the daemon log is not empty but no record was written, the verdict depends on what the log can prove. Only on a radius run whose daemon log names the device does the summary say the request was processed and not recorded ([!]): then a Log Exclusion matching this user or device, or a database write that failed and went to the outage spool, is the explanation. Otherwise — a radius log that does not name the device, or any tacacs run, where tac_plus-ng names the peer even when it refuses it — the summary prints [?] and lists the usual causes without picking one: the device is not a configured client, the shared secret or key does not match, or a Log Exclusion.

[KEY] — the secret does not match. A key mismatch is invisible from the outside, because the service does not raise an error; it simply stops answering. The decode is the only place it can be stated outright, and it is stated by trying every key that could apply to the address and reporting that none of them opens the body.

It is also the verdict that must not be reached carelessly, because it sends someone off to re-check keys that are fine. Two guards keep it honest. A header that cannot be TACACS+ — the version nibble, the packet type and the sequence number are each constrained by the standard — stops the stream splitter instead of being handed to the decoder and diagnosed: a capture that started mid-connection, or a segment boundary landing inside a packet, is misframing, and misframing is reported as the end of the readable stream. And once any packet of a session has decoded with a key, a later failure in that same session is reported as a truncated or misassembled capture, not as a different secret — the key is demonstrably right, and saying otherwise would contradict evidence printed a few lines above.

The Contents section of the summary is generated from what is actually on disk, not from a fixed list — a bundle that promises a file it does not contain sends the reader looking for something that was never written.

FileWhat it holds
summary.txtThe report you also saw in the terminal. Start here.
capture.pcapThe raw traffic. Open in Wireshark if you want to go deeper.
decoded.txtThe exchange decoded into readable text, no secret included.
preflight.jsonHow the device is configured in Taranac — every matching object, with key fingerprints.
db-logs.jsonWhat Taranac recorded during the window. A protocol maps to several tables, not one.
logs/…The daemon’s log output for the window. Paths are flattened into filenames, since several subdirectories can hold a file of the same name.
logs/context/…Recent tails from neighbouring services — clearly marked as context, not the window. The backend’s own log is here too, sliced to the window (or its last 200 lines, if it said nothing during it).
config/daemon-config.masked.txtThe generated daemon configuration, secrets masked.
environment.txtVersions, containers, capture settings, and both clocks (see the timezone note below).
ipv6.txtWhat this host and the daemon do with an IPv6 device: Docker’s version and whether its ip6tables is on, kernel IPv6, IPv6 forwarding, accept_ra per interface, whether the stack network carries IPv6, the daemon’s IPv6 listener and Docker’s IPv6 DNAT per published port (the DNAT part needs root or sudo), and the inventory entries that cover the Docker gateway.
supervisor/…radius and nac only: the radiusd supervisor’s trace of every restart it caused (trace-<time>-<reason>.json: sockets, generations, last output) and incidents.json, every restart with a 24-hour count. All of them, not only the window’s — the restart that matters may be the one before.
config/eap.masked.txtnac only: the rendered EAP module — check_crl, the per-CA ca_policy lines, the reload interval and cache.
crl/…nac only: EAP-TLS revocation on this node. report.txt compares, in words, the CRL directory as radiusd sees it (inventory.tsv, with copies of each CRL and CA certificate under files/ — public data; a file carrying a private key is skipped) with what the database holds (crl-facts.json: every CRL source, the per-CA CRL and OCSP policy with the chain the daemon walks from each CA, open OCSP alerts, the last day’s OCSP refusals by class, and a live probe of each responder URL, 15 s in all). Its findings go into the summary. See Certificate revocation → Diagnosing.
tcpdump.stderrCapture tool output. Empty unless something went wrong.
decode.stderrPresent only when the decode failed — decoded.txt is then absent, and this says why. A bundle quietly missing its most useful file is worse than one that explains itself.

Empty log slices are deleted rather than shipped, so a directory of zero-byte files never masquerades as collected data.

Three of them, for three specific reasons.

winbind is what verifies an Active Directory password: MS-CHAPv2 and PEAP for domain accounts go out through ntlm_auth. Taranac’s own log does name the failure class — each NT_STATUS maps to a reason and a hint — but the machine account, the trust and which controller answered are winbind’s own business, and only its log holds them. It is in every bundle because it is the log nobody thinks to ask for. A directory that publishes the NT hash instead — FreeIPA — takes a branch that never touches winbind, and there the evidence is in the daemon’s own log.

The other two daemons are included because the wrong protocol is easy to collect. A device sending RADIUS while --protocol tacacs is running produces an empty capture and a bundle that says nothing arrived — when the truth, traffic on the other port, is one file away.

The backend is included because half of NAC never passes through a daemon at all. A disconnect, a VLAN change, a port bounce and the CoA test are all sent from the api container, and their outcome — CoA-ACK received, timeout waiting for a CoA response, no CoA secret — is recorded only there. The daemon’s log cannot mention a packet it never saw. This tail is sliced to the capture window; if nothing was logged in it, the last 200 lines are kept instead, so “was the worker even running?” still has an answer.

Decoding is not a convenience; for TACACS+ it is the difference between a usable bundle and an opaque one. A TACACS+ body is obfuscated on the wire, so a raw capture of your own traffic is unreadable to you. Taranac holds both the traffic and the key, so the decode happens inside the api container and the key stays there.

RADIUS and 802.1X are decoded for a different reason. Three things exist only in the capture and in no log anywhere:

  • the attributes Taranac sent back;
  • everything the device sent beyond the handful of values that get stored;
  • requests discarded before they were processed — which is exactly the “traffic is clearly flowing and Taranac says nothing” case.

Attributes are named using the same dictionary the service itself generates, so entries from your own attribute dictionary are named correctly here too, rather than appearing as numbers in the one document where you most need to recognise them.

decoded.txt opens with a header naming the key actually in use, then prints the traffic grouped by session — not in stream order. Stream order interleaves the two directions, every request first and then every reply, which is unreadable for the one question being asked: what did the server answer to this request.

These four sessions are an excerpt from a longer capture — a successful login on a Cisco switch, the shell authorization that follows it, one command, and the accounting record:

TACACS+ decode for 10.30.0.14
packets: 35 seen, 14 session(s)
key in use: SW6-DEMO [10.30.0.14/32], key from device
----------------------------------------------------------------------
═══ session 0x4a87cff5 ═══
[key: SW6-DEMO [10.30.0.14/32], key from device]
21:17:23.399 seq 1 device -> Taranac AUTHEN
AUTHEN START action=LOGIN priv_lvl=1 type=ASCII service=LOGIN
user='taranac' port='tty2' rem_addr='10.6.0.110'
21:17:23.400 seq 2 Taranac -> device AUTHEN
AUTHEN REPLY status=GETPASS flags=0x01
server_msg='User Access Verification\n\nPassword: '
21:17:23.406 seq 3 device -> Taranac AUTHEN (+6ms)
AUTHEN CONTINUE user_msg=<REDACTED 21 bytes>
21:17:23.482 seq 4 Taranac -> device AUTHEN (+77ms)
AUTHEN REPLY status=PASS flags=0x00
═══ session 0xa6113343 ═══
21:17:23.520 seq 1 device -> Taranac AUTHOR
AUTHOR REQUEST method=TACACSPLUS priv_lvl=1 type=ASCII service=LOGIN
user='taranac' port='tty2' rem_addr='10.6.0.110'
arg: 'service=shell'
arg: 'cmd*'
21:17:23.521 seq 2 Taranac -> device AUTHOR (+2ms)
AUTHOR RESPONSE status=PASS_ADD
arg: 'priv-lvl=15'
═══ session 0x748eea97 ═══
21:17:26.893 seq 1 device -> Taranac AUTHOR
AUTHOR REQUEST method=NONE priv_lvl=1 type=ASCII service=NONE
user='taranac' port='tty2' rem_addr='10.6.0.110'
arg: 'service=shell'
arg: 'cmd=show'
arg: 'cmd-arg=version'
arg: 'cmd-arg=<cr>'
21:17:27.003 seq 2 Taranac -> device AUTHOR (+110ms)
AUTHOR RESPONSE status=PASS_ADD
═══ session 0xbadc07f2 ═══
21:17:27.106 seq 1 device -> Taranac ACCT
ACCT REQUEST flags=STOP method=TACACSPLUS priv_lvl=15 service=LOGIN
user='taranac' port='tty2' rem_addr='10.6.0.110'
arg: 'task_id=100443'
arg: 'timezone=UTC'
arg: 'service=shell'
arg: 'priv-lvl=1'
arg: 'cmd=show version <cr>'
21:17:27.108 seq 2 Taranac -> device ACCT (+2ms)
ACCT REPLY status=SUCCESS

Four things in that output are worth knowing how to read:

  • The password is never printed — user_msg=<REDACTED 21 bytes>. You get its presence and its length, which is all a diagnosis needs.
  • [key: …] names which candidate opened the session. It is printed only when there was more than one candidate — that is, only when there was a choice to get wrong. One session is decoded with one key throughout, so a coincidence cannot split a conversation across two of them.
  • Sides are named by role, not by address. DNAT means a request is addressed to the host while the reply comes from the container, so printing raw addresses showed one exchange with two different “server” addresses and made replies read backwards. The addresses are still in capture.pcap if you need them.
  • The gap annotations are diagnostic. (+77ms) on the reply is the whole authentication round trip. A reply that takes seconds is the signature of a backend timing out — a directory behind MAVIS, most often — and that wait is invisible in the daemon log, which records the outcome rather than the time spent waiting for it.

Also visible here: the trailing cmd-arg=<cr> that the standard requires to carry a separator. A device that sends a bare <cr> instead is what the Vendor Compatibility setting on the TACACS+ settings page exists for — and this decode is where you can see which of the two your device does.

A NAC capture opens with the list of attempts before printing any of them, so you can find the interesting one without reading the whole file. This is an excerpt from a capture that contained thirteen:

NAC decode for 10.30.0.12
packets: 40 seen, 40 decoded
key in use: SW2 [10.30.0.12/32], key from device
----------------------------------------------------------------------
Attempts in this capture:
…
7. '500000080000' · PAP · Access-Accept · 1 round(s), 0.0s
8. 'lab\\anonymous' · EAP-TTLS · Access-Accept · 8 round(s), 0.4s
9. 'lab\\anonymous' · accounting Start · Accounting-Response
10. 'lab\\anonymous' · accounting Interim-Update · Accounting-Response

Attempt 7 is MAB — a MAC address as the user name, accepted into VLAN 30. Attempt 8 is a real 802.1X supplicant doing EAP-TTLS in eight rounds.

An attempt in which no method was ever answered is not named after the server’s offer. Its headline says what was on the table and what the client wanted instead — server offered PEAP, client asked for LEAP (17), or client named no alternative for a Nak that lists none — and the packet itself prints as Response/Legacy Nak — wants: LEAP (17). The method names and numbers are the same ones the NAC Auth Log uses for eap_method_unsupported (see EAP rejects with a reason).

Attempt 8, printed in full, with the rounds in the middle elided:

═══ attempt 8 — 'lab\\anonymous' · EAP-TTLS · Access-Accept · 8 round(s), 0.4s ═══
21:26:33.434 id 85 device -> Taranac Access-Request
User-Name = 'lab\\anonymous'
Service-Type = Framed-User (2)
Cisco-AVPair = 'service-type=Framed'
Framed-MTU = 1500
Called-Station-Id = '50-00-00-03-00-05'
Calling-Station-Id = '50-00-00-08-00-00'
Message-Authenticator = present (0x1449a1a3937532e4…)
Cisco-AVPair = 'audit-session-id=0A1E000C000000EB3AE33919'
Cisco-AVPair = 'method=dot1x'
NAS-IP-Address = 10.30.0.12
NAS-Port-Id = 'GigabitEthernet1/1'
NAS-Port-Type = Ethernet (15)
NAS-Port = 50101
EAP-Message = Response/Identity 'lab\\anonymous'
21:26:33.437 id 85 Taranac -> device Access-Challenge (+3ms)
[key verified: SW2 [10.30.0.12/32], key from device]
Message-Authenticator = present (0x31d4ed2aabe70fc5…)
State = 0x1f5691e31f549c1c99b4938d922146c6
EAP-Message = Request/EAP-TLS
… six more challenge/response rounds …
21:26:33.866 id 92 Taranac -> device Access-Accept (+215ms)
[key verified: SW2 [10.30.0.12/32], key from device]
Message-Authenticator = present (0x6dd57406341b8d22…)
User-Name = 'lab\\anonymous'
Tunnel-Type = 13
Tunnel-Medium-Type = 6
Tunnel-Private-Group-Id = '10'
Class = '3bbb68c676524464ab5c319ad252d559'
EAP-Message = Success

This is the part that exists nowhere else. The three Tunnel-* attributes in the Access-Accept are what actually put the endpoint into VLAN 10 — they are what Taranac sent, and no log records them. If a supplicant authenticates successfully and still lands in the wrong VLAN, this is the only place the answer is written down. The same applies to everything the switch sent beyond the few values that get stored: the port, the audit-session-id, the method, the MTU.

[key verified: …] on each reply is the RADIUS twin of the TACACS+ key check — the Message-Authenticator proves which shared secret was actually in use.

An 802.1X capture is not printed as one flat stream. It opens with a list of the attempts it found, then prints each one separately with a headline saying who, by what method, how it ended, how many rounds it took and how long.

Two of those endings cannot appear in any log, which is the whole argument for reading the wire:

  • An attempt abandoned by the client. This is the shape of a supplicant refusing an untrusted server certificate: the user declines the warning, the supplicant goes quiet, and the server never learns the exchange ended — so there is nothing to log, because from the server’s side it is still waiting.
  • A TLS alert, spelled out rather than left in hex — for example unknown_ca — the client does not trust the server certificate's CA, or certificate_expired.

Both are a certificate problem being reported to the operator as an authentication problem, which is where the hours go. See 802.1X with PEAP and PKI for the fix once the decode has named it.

Change of Authorization reverses the roles. Everywhere else the switch asks and Taranac answers; in a CoA Taranac is the client and the switch is the server, listening on its own CoA port. The decoder singles the six RFC 5176 codes out — Disconnect-Request, CoA-Request and their ACK/NAK replies — and takes the direction from the code itself, because both halves travel on the same port pair and no port heuristic can tell them apart. That is not cosmetic: the device’s address is what the key candidates and the pre/post-NAT deduplication are keyed on, so a CoA read the wrong way round came out labelled backwards, listed twice, and with the switch’s answer unverifiable.

The reply is verified against the CoA secret, not the authentication one. The two are frequently different — CoA resolves the device’s own CoA secret, then the oldest group carrying one, then the global nac.coa_secret, independently of the shared secret the same device authenticates with. Both are handed to the decoder, so a CoA-ACK verifies as [key verified: … CoA secret] instead of being reported as a key mismatch that does not exist.

Where the port comes from. The collector does not restate the inheritance chain: it asks the CoA sender itself to resolve port and secret, so the port in the bundle is the port a real CoA would have gone to — the same answer the device’s Details card shows. preflight.json records which level decided it (port_source: device, group with the group’s name, vendor, global, or none for the RFC 5176 default 3799), because a CoA sent to a closed port times out exactly like a switch that is switched off, and the number alone does not say whether someone chose it. The order is the one on Network devices: device → device group → vendor dictionary → global nac.coa_port → 3799.

And when nothing answers at all, the ICMP in capture.pcap is the answer. A port-unreachable from the switch means nothing was listening where Taranac sent, which is a different fault from a switch that received the request and ignored it — and neither leaves a trace anywhere else. [note] findings cover the cases that never reached the wire: no CoA secret resolves for the device, its vendor entry declares the liveness probe unsupported, or it is not pinned to a Vendor Dictionary entry at all.

On a nac run two CoA findings are raised as problems — printed as [config] lines at the top of the findings — rather than left in preflight.json, because from every other angle they look like success:

  • The CoA port disagrees with the vendor. The device is pinned to a vendor entry that declares one CoA port, and the port that would actually be used — set on the device, on a group or globally — is a different one. The finding names the port, the level it came from and the vendor’s port. A CoA to the wrong port simply times out.
  • A teardown that nothing followed. The bundle reads the NAC Auth Log for this NAS over the last 24 hours and counts every session stop that was not followed by an authentication from the same station within 60 seconds; up to twenty of them are listed in preflight.json, newest first, with the station and the port. It is the trace of a CoA that led nowhere: the switch acknowledged the change, tore the session down, and never sent another Access-Request, so the client never received its new authorization while every screen showed the grant as done. On hardware that drops the link while a port is unauthorized this is a deadlock — the switch removes the very stimulus that would restart authentication. A healthy re-authentication follows a CoA within a second or two. An endpoint that simply left the network also ends in a stop with nothing after it, so read the listed stops against the times a CoA was sent (the session’s Close reason and the backend log in the bundle say which ones were).
Terminal window
./taranac diagnose --protocol ncm --config "SW1 - show runn"

Config Tracker is the mirror image of AAA. There Taranac is the server and the packets are the evidence; here Taranac is the client, the session is encrypted, and a capture would prove nothing. The evidence lives inside the session: which driver was resolved, whether the port answered, what the SSH handshake agreed on, which prompt the device showed, and where exactly the attempt died.

So this collector is not a capture but a session recorder. It runs one real collection for one tracked configuration through the same engine a scheduled run uses, and writes a single readable report — one file, meant to be attached to a ticket whole.

OptionMeaning
--protocol ncmSelects this collector.
--config NAMEThe tracked configuration: its exact name as shown in the UI, or its id. Required.
--out DIRWhere to write the report. Default: ./diagnostics.
--full-transcriptKeep recording past the first command. The report then contains the device’s configuration — see below.

Run it on the core, where the api container is: that container is the only place holding both the database and the master key, and the collector says so and stops if it is not running. The connection to the device is made from the core, by the same embedded collector Collection Preview uses — not from the standalone collector that may normally serve that source. Plan, driver and credential resolution are identical either way; reachability and the handshake describe the path from the core.

It changes nothing, and it logs in exactly once

Section titled “It changes nothing, and it logs in exactly once”

Nothing is persisted: no run, no result, no snapshot, no version, no report row. The file it hands you is the only artefact.

Exactly one device login is attempted — never the retry loop a scheduled run would use, because retrying a rejected credential is how an account gets locked out. The reachability and handshake layers are credential-free for the same reason: they open their own connection, offer nothing, and cannot contribute to a lockout.

The one thing it does change is outside Taranac: on a source using a just-in-time credential, the mint rotates the managed account’s password exactly as a real collection would, because a live login needs a committed password. If the mint does not succeed, the report says so, prints what it was about to do, and no device is contacted — a real run would have failed in the same place.

The order is the point rather than a formality. Each layer stands on its own and is reported even when the next one cannot run — the failure that prompted this tool, a driver name the library does not have, happens before a socket is ever opened, so a tool that only recorded sessions would have handed back an empty file and a shrug.

LayerWhat it answersHow
0 — what we were about to doTransport, address, port, whether a credential resolved and which fields it carries, the recipe and its steps, the scrub rules, and the resolved CLI driver together with which of the four priorities produced itNo network at all
1 — reachabilityDoes the port answer, and in how many millisecondsA plain TCP connect, no credential
2 — the SSH handshakeThe device’s version banner, the negotiated cipher, MAC and host-key type with its SHA256: fingerprint — plus what the device offered against what the two ends agreedA real handshake with no authentication attempted. SSH, SCP and SFTP sources only — a telnet source has no handshake, and the layer is skipped
3 — the collectionThe run itself, stage by stage with timings, and the dialogue that produced itOne real attempt through the production engine; nothing ingested

Layer 0 is where the unglamorous failures are. The driver is resolved the same way the collection resolves it — a device_type override on the source wins, then the recipe’s CLI driver, then the default, with _telnet appended for a telnet transport — and the result is checked against the drivers actually installed. When it is not one of them the report says so outright and lists the drivers that exist for that vendor, including the common case of a platform that ships an SSH driver and no telnet one.

Layer 2 earns its place on old equipment. When a modern client and an old switch share no key exchange or host-key algorithm, the failure text plus the list of what Taranac offered is the entire diagnosis — it names the algorithm to enable rather than leaving you with “the handshake failed”.

What the run prints, and what lands in the file

Section titled “What the run prints, and what lands in the file”

The terminal gets the verdict and the standing warnings; the report goes to a file, because a transcript scrolled past is a transcript nobody sends:

Terminal window
$ ./taranac diagnose --protocol ncm --config "SW1 - show runn"
Taranac diagnostic collector
protocol : ncm
config : SW1 - show runn
api : taranac-api-dev
Connecting to the device once. Nothing is written to the configuration history.
verdict : collection succeeded
- credential values (passwords, keys, passphrases, tokens) — masked at capture
- the device configuration itself — the transcript stops before the first command
Report: /opt/taranac/diagnostics/ncm-diagnostics-20260814-193529.txt (214 lines)

The verdict is the first line of the report as well, and there are only a few of them: collection succeeded; failed at stage '<stage>' (<class>), naming the first stage that failed and how the engine classified it; failed (no stage reported it — see the transcript); this tracked config has no recipe — it can never collect, which is answered before any network; and the just-in-time mint failure above.

The file itself is fixed in shape, so two reports from different devices compare line by line: the header with the verdict and the standing “what this file does not contain” block, then the four layers in order, then the session transcript, then — when the run failed — the raw failure text from the transport library, which the engine’s own trace deliberately withholds. It ends with a machine-readable JSON summary of the same facts, for support to read without parsing prose.

In the transcript, < is what the device sent, > is what Taranac wrote, and # is the library’s own commentary — In disable_paging, Pattern found and friends, which are often the whole answer.

The transcript stops where the configuration would start

Section titled “The transcript stops where the configuration would start”

By default the recording is cut at the moment the session issues the first recipe command. That cut is exactly where the diagnostic value ends and the running-config — full of password hashes and community strings — would begin. The report says the cut happened and how to opt out.

--full-transcript opts out, and then the report contains the device’s configuration. The flag says so, the header of the report says so, and the finished run says so again on the terminal. Use it when the fault is in what the device answered to a command, and treat the resulting file like the configuration it contains.

Credential values never survive either way: every secret the plan resolved is masked before the transcript is buffered — the login password genuinely passes through this recording, and is genuinely removed, including the escaped spelling a password with punctuation produces. The username is deliberately kept: seeing which account was offered is half of reading a login dialogue, and it is not a secret. A very long session is truncated at 256 KB, and the truncation is announced in the file rather than left to look like the session simply ended.

Terminal window
./taranac diagnose --protocol snmp --device "core-sw-01"

--device is the device’s name in Taranac (or its id), not an address: which address gets polled is one of the facts the report works out and explains. The run makes one real poll through the same code the scheduled sweep uses, writes nothing to the database, and produces one text report in ./diagnostics/ — resolution before any packet (whether polling is on, which profile applied and from which step of the inheritance, which address and why), the USM algorithms this image can perform, the SNMPv3 engine-ID exchange on its own, every request numbered and timed, and finally what the device did and did not answer, including why a forwarding table is empty. There is no packet capture: the interesting half of an SNMP fault is which object the device declined to have, and that is in the answer. The report never contains a community or passphrase.

What each layer means and how to act on it is on SNMP.

Terminal window
./taranac diagnose --ha

One command, no roles, no flags to get wrong, run on every machine in the cluster — each database node and the witness — and send all the archives together.

On a witness this is one of the few things the wrapper will do at all. ./taranac refuses most commands there — a witness runs etcd and its etcd-agent and nothing else, so anything that reads the cluster’s records or drives its containers has nothing to work with — and diagnose --ha is on the short list it offers instead. The witness is a third of the cluster’s evidence.

That is the design rather than a formality. Almost every HA fault is a disagreement between hosts: a timeline that forked, a secret that differs, a ring one node knows and another does not, a version upgraded on one machine only. A disagreement cannot be seen from one side of it. Part of the picture is cluster-wide from any single host — etcd and Patroni’s REST are cluster APIs, and both are collected — but the rest of it (this node’s .env, its containers, its clock, its disk, what it can reach) exists only locally, which is exactly why the command runs everywhere.

This is a snapshot, not a capture. There is no window and nothing to reproduce: HA faults are state. It reads that state and changes nothing.

Its summary opens with a verdict and one next command — the same machine-level judgement ./taranac cluster status makes (“can any one machine be lost and writes continue?”), computed here from the etcd ring and Patroni so that it also works on the witness, which has no api to run cluster status on, and on a database node whose api is down. Below it, every machine of the ring is listed as it answered about itself, with the version its own updater recorded.

OptionMeaning
--haSelects this collector. Takes no --protocol and no --device.
--out DIRWhere to write the archive. Default: ./diagnostics.
--log-lines NLines of each component log to include. Default: 400.

High availability is a licensed feature — see Licensing. The collector itself is not gated and is safe on any node at any time, including one that is not clustered at all, which it will say.

Every bundle prints one cluster-fingerprint line — a hash over the fingerprints of every value that must be identical cluster-wide: the cluster name, the database and replication passwords, the application secret key, the master key, the MFA API key and the Patroni REST password. Identical on every node means every cluster-wide secret agrees, and that question is closed at a glance instead of by diffing seven values. If it differs, the per-key block in config.txt names the one that drifted. Compare it across the database nodes: a witness holds none of the cluster’s secrets, so its bundle prints cluster-fingerprint : not comparable — a witness carries no cluster secrets instead.

The bundle deliberately splits configuration into two classes: values that must be identical everywhere, and values that must differ (node name, node address, etcd name). Topology — addresses, member lists, node names — is not secret and is recorded verbatim; it is the entire point of the bundle.

That is when it gets run, so no probe is allowed to take the others down with it. Every check fails softly and records why it could not answer; one dead subsystem never costs the bundle the other eight. Specifically:

  • .env is read from the host, never through a container;
  • the database timeline is read straight off the data volume with a throwaway container, so a node whose Postgres will not start still reports the number that explains why;
  • Patroni is asked locally first and then at every declared member, so a node with a dead stack still gets the cluster’s real state — reads need no credential, which is what makes this possible without handling the REST password;
  • a witness has no .env in some layouts, so its identity is read from the etcd container instead. The same command works on the third machine, which is a third of the cluster.

The role is decided by construction before any daemon is asked anything: a name in the etcd ring with no Postgres behind it is a witness, and every database probe being empty there is correct rather than a fault.

Three database nodes and a witness; taranac-node-1’s stack is stopped. This is the terminal output from taranac-node-3:

Terminal window
$ ./taranac diagnose --ha
Taranac diagnostic collector
mode : ha (cluster-state snapshot)
node : taranac-node-3 (10.20.0.43)
cluster : taranac
looks like: database node
→ reading this node's containers
→ reading the declared configuration
→ reading config/cluster.yml
→ asking Patroni for the cluster's real state
→ asking etcd about the ring
→ reading every ring member's own health and version record
→ reading this node's database state
→ comparing every node's write routing with the roster
→ probing reachability to every declared member (from this host, and from the Docker network)
→ checking the etcd certificates
→ collecting component logs (last 400 lines each)
Taranac HA diagnostic bundle
============================
node : taranac-node-3 (10.20.0.43)
hostname : taranac-replica2
cluster : taranac
role : database node
version : 1.3.3
collected : 2026-10-11 19:33:44 UTC
Verdict (from this host)
------------------------
Healthy — any one machine (taranac-node-1, witness, taranac-node-2, taranac-node-3) can be lost and writes continue.
Next: nothing to fix from this host's view — compare the bundles from the other machines.
machines of the ring (each asked about itself; the witness is one of them):
taranac-node-1 10.20.0.40 db healthy 1.3.3
witness 10.20.0.41 witness healthy 1.3.3
taranac-node-2 10.20.0.42 db healthy 1.3.3
taranac-node-3 10.20.0.43 db healthy 1.3.3 ← this host
cluster-fingerprint : 639906aca2bf
^ Compare this ONE string across every DATABASE node's bundle. Identical on
all of them means every cluster-wide secret agrees. If it differs, the
per-key block in config.txt names the value that drifted.
declared members : 4
taranac-node-1 10.20.0.40 db
witness 10.20.0.41 witness
taranac-node-2 10.20.0.42 db
taranac-node-3 10.20.0.43 db
taranac-db : running=true image=ghcr.io/taranaclabs/taranac/postgres-ha:1.3.3
compose=/opt/taranac/docker-compose.yml,/opt/taranac/docker-compose.ha.yml
taranac-etcd: running=true
patroni view: taken from 127.0.0.1
control file: timeline 6, last checkpoint 0/18ECD230
(a streaming replica's checkpoint timeline legitimately lags
the cluster's until its next restartpoint — see patroni-cluster.json
for the timeline Patroni actually has this node on)
Findings
--------
[LAG] Patroni reports member(s) that are not keeping up:
taranac-node-1 is not streaming (state: stopped)
A replica that is behind is not a spare: a failover to it loses whatever
it has not replayed, and past 45s it also freezes daemon-config reloads.
[SLOT] An INACTIVE replication slot is present (see postgres.txt, active = f).
An inactive physical slot pins WAL on the primary indefinitely — the disk
fills and the cluster stops. If the node it belonged to is gone for good,
drop the slot; the eviction commands do this for you.
[UNREACHABLE] From this node, these member ports do not answer:
taranac-node-1:5432 (refused) taranac-node-1:8008 (refused) taranac-node-1:443 (refused)
Collect this bundle on the OTHER nodes too: if the reverse direction
works, this is asymmetric filtering on the path (a firewall rule or a
route), which is a different fault from a host being down. Remember that
BOTH etcd ports matter — the ring forms over 2380, and a ring that cannot
form makes every 2379 check look perfectly healthy.
refused = the host answered and nothing listens (or a REJECT): start it there.
timeout = nothing came back (a DROP, or no host): a firewall or route.
Notes
-----
[note] 4 etcd voters: 3 database nodes and the witness. A majority of
4 survives one loss, as the database nodes alone do — the witness adds
nothing at this size. To take it out, on any database node:
./taranac cluster remove-witness
Contents
--------
certs.txt — etcd CA and this node's leaf: fingerprint, dates, SANs
clock-skew.txt — measured clock difference against each reachable peer
config.txt — declared topology verbatim; secrets as fingerprints only
etcd-members.json — the ring as data: what every etcd verdict is computed from
etcd.txt — ring membership, health, raft term/index
logs/api.log — last 400 lines
logs/db.log — last 400 lines
logs/etcd.log — last 400 lines
manifest.txt — config/cluster.yml: what this node declares the cluster to be
node.txt — this host: containers, images, compose files, clock, disk
patroni-cluster.json — the ring as Patroni sees it (roles, timelines, lag)
patroni-config.json — the cluster's Patroni configuration
patroni-history.json — timeline history: where each timeline forked
patroni-local.json — what THIS node thinks it is
patronictl-list.txt — the same view from patronictl inside the container
postgres.txt — timeline, checkpoint, replication, slots, WAL size
reachability.txt — this node → every member, per port (run everywhere = the full matrix)
ring.txt — every ring member (witness included): own health, version record; departed records
signatures.txt — lines in this node's container logs that name a known cause (read up to T0)
write-routing.txt — each node's write list (heartbeat + this host's daemons) against the roster
Note on secrets: no password, key or token is in this archive. Values that
must match across nodes appear only as 12-hex fingerprints, which prove
agreement or disagreement without carrying the secret.
This bundle describes ONE node. Send one from every node — each database
node and the witness — or the comparison that finds the fault cannot be made.
Archive: /opt/taranac/diagnostics/taranac-diag-ha-taranac-node-3-20261011-193231.tar.gz (28K)
Run the SAME command on every other node — including the witness — and send
all the archives together. Most HA faults are disagreements between nodes,
and a disagreement cannot be seen from one side of it.

The verdict comes first and is about the next loss, not this one: taranac-node-1’s etcd still answers and taranac-node-3 can take over from the leader, so losing any one more machine keeps writes. The findings say what is already wrong — three of them, and reading them together is the skill worth having.

They are one fact seen from three angles. taranac-node-1 is stopped, so Patroni reports it not streaming ([LAG]), its replication slot on the primary is therefore inactive and pinning WAL ([SLOT]), and from this node its database, Patroni and api ports are refused ([UNREACHABLE]) — refused, not timed out, so the host answered and nothing listens there. Note which ports are missing from that list: 2379 and 2380 answered, so node-1’s etcd is alive and voting while everything else on that host is down. That is a machine that is up with a stopped stack, not a machine that is gone — a distinction that decides whether you restart a service or re-clone a node.

The note is not about the incident at all. Three database nodes plus a witness is four voting etcd members, the shape the build makes. It is not a fault, so it is a note rather than a finding: the witness adds nothing at this size, and the note names the command that takes it out if you want to.

And the [SLOT] finding is the one that becomes an outage if node-1 stays down and nobody looks: an inactive physical slot pins WAL on the primary indefinitely, the disk fills, and the cluster stops for a reason that looks nothing like the original fault.

Findings that come from the logs are listed first. The collector reads the etcd, database and api logs up to the moment it started, so its own probes can never come back as a finding, and matches lines that name a known cause. Each is reported with its cure, and marked live (recent, or from the last run of a container that has stopped) or past. A line whose fault is gone — the role logs in at the time of the run, the replica streams, the api became healthy afterwards — is reported as past and never acted on. The first live one becomes the verdict’s Next step, unless a firewall cutting this node’s containers from its own etcd or Patroni port comes first. The matched lines are in signatures.txt.

Log labelThe line it matched
[ETCD-ID]etcd’s cluster ID mismatch: a member from another (or an earlier) ring. The cure names the clears and the re-run.
[NO-ROLE]The primary’s Role "replicator" does not exist.
[REPL-PASSWORD]A replica presented the wrong replication password (password authentication failed for user "replicator").
[CLONE-AUTH]This node’s own clone (pg_basebackup) was refused that password.
[NOLOGIN] / [NO-REPLICATION]The replicator role may not log in, or may not start a WAL sender.
[API-DB-TIMEOUT]The api could not reach its database at start-up.
[BIND]Not a log line: Docker could not start a container because a port it publishes — 443 included — is taken on this host. The finding prints the ss command that names the holder.

Patroni’s start-up pg_hba messages are not reported as faults. The state findings follow:

LabelWhat it means
[WILL NOT START]TARANAC_HA=1 with no Patroni REST password — the database container refuses to start without it, and the value is cluster-wide.
[SPLIT-BRAIN RISK]The node is marked HA but its database was created without the Patroni overlay, or runs the plain image. That is an ordinary writable Postgres on Patroni’s data directory.
[SPLIT-BRAIN]Patroni reports more than one leader. Two nodes are accepting writes; stop writing and contact support before restarting anything.
[DIVERGED]This node’s data is on an older timeline than the leader’s, with the fork point and how much WAL it holds that the leader never had.
[LAG]A member is not keeping up, in bytes — or is not streaming at all.
[QUORUM]Fewer than three members declared, an unstarted member, a non-voting learner, or an even number of voters that is not the build’s own shape. Three or more database nodes and their witness is a note naming ./taranac cluster remove-witness; a ring still being built (declared nodes not joined yet) is a note to add them. Voters are counted from the live etcd ring, so a witness removed from another node, or a node declared and not yet joined, is neither a voter nor [UNREACHABLE].
[RING]A member of the etcd ring — a database node or the witness — does not answer its own health check, with the reason and how many voters still answer; or a member was added and never started, with the remove-node that takes it out.
[DEPARTED]A witness was removed with remove-witness while its etcd ran and has not left yet. The finding names the ./witness-join.sh --leave to run there, or remove-witness --forget for a machine gone for good.
[UNREGISTERED]A node streams but has no active roster row: its join stopped after the clone and before its api registered it. The finding prints how to finish it — ./taranac up -d then ./taranac update --version <v> on that node before the join token expires, or later a new cluster join-token for the same name and address.
[ROUTING]A daemon on this node has a write list that does not name every roster member, so it cannot write while that member leads.
[UPDATE]An update finisher is running on this node. The finding says where the rollout stands and what, if anything, to run next and on which machine — usually the leader.
[RENAMED]A container runs under a temporary name such as <12 hex>_taranac-db, left by an interrupted docker compose recreate. The bundle reads it under its actual name; any ./taranac command renames it back without restarting it.
[SLOT]An inactive replication slot is pinning WAL.
[UNREACHABLE]Declared member ports that do not answer from this node — direction matters. Each carries its class: refused (the host answered, nothing listens or a firewall REJECTs — start it there) or timeout / filtered (a firewall DROP, or no route). This node’s own other ports are not reported.
[DOCKER PATH]Ports that the host reaches but the Docker network does not — where Patroni and the api dial from. For this node’s own 2379 and 8008 it is the host firewall dropping traffic from 10.255.255.240/28 to the node’s own address; the finding prints the rule to add. See The node’s own Docker network.
[LOGIN]replicator could not log in to a database node from this node’s Docker network — a wrong password, a PG_ALLOW_CIDR that does not cover the source, or a role that cannot replicate.
[CLOCK]This node’s clock differs from a peer’s by more than 5 seconds. Patroni’s leader lease is time-based, so skew shows up as failovers that should not have happened.
[CERT]The etcd certificate expires within 30 days, or does not cover the address this node advertises. For an expiring certificate, run ./taranac cluster renew-cert on that host (the finding’s own text still says to re-issue it from the seed); for the CA, ./taranac cluster rotate-ca. See etcd certificates and the etcd CA.
[CA]config/etcd-ca holds a CA this member does not trust — usually restored from an archive older than a CA rotation. The member cannot renew its certificate or admit a node; re-join it.
[CA ROTATION]An etcd CA rotation has not finished, or this member is part-way through one the cluster has no mark for. Run ./taranac cluster rotate-ca on a database node to finish it or put every member back on one CA.
[CRL]A hash link in this node’s EAP-TLS CRL directory points at a missing file (see crl.txt).
[FLAPPING]More than four leadership events in the collected log window. Leadership should be rare.
[NOT CONVERTED]A cluster topology is declared but the HA marker is absent, so the tooling would start this node without the overlay.
[note]A state worth naming rather than a fault: a standalone install, a witness, a value taken from a fallback.
[ok]Nothing wrong was detected from this node — which is not the same as a healthy cluster.

That last distinction is repeated by the collector itself, and it is the reason the command is run everywhere: a clean bundle from one node proves only that this node sees nothing wrong.

Each bundle probes this node → every declared member, on five ports: 2379 etcd client, 2380 etcd peer, 5432 Postgres, 8008 Patroni REST and 443 the api. Run on every node, N bundles are the full N×N matrix, asymmetry included — and asymmetric reachability (A sees B, B does not see A) is a different fault from a host being down, with a different cure.

reachability.txt probes twice: from the host, and from the Docker network, where Patroni and the api actually dial from. Toward this node’s own address the Docker column is the one that matters, asked on 2379 and 8008: a host firewall that drops traffic from the containers to the host’s own LAN address breaks this node’s Patroni and api while every host-side check passes. Each cell is a class rather than yes/no — ok, refused (an RST: nothing listens, or a REJECT), timeout (nothing came back: a DROP, or no host) or filtered (host-unreachable) — because a refused port means start the service there and a dropped one means fix the firewall. The file also carries the round-trip time to each member and the api’s own replicator-login matrix.

2380 is probed because it is the port the join tooling never checked: a ring whose members cannot reach each other’s peer port forms no quorum while every client-port check looks perfectly healthy. A witness legitimately has only the two etcd ports open, and the collector expects that rather than reporting it.

Beyond the summary, each archive carries the node’s containers and which compose files created them, the declared configuration, the cluster manifest, Patroni’s cluster view / history / config / local opinion, the etcd ring as a table and as data, every ring member’s own health and the version record its updater wrote, the departed-witness records, each node’s write routing against the roster, the database’s timeline and replication and slots, the reachability matrix, the etcd certificates, the measured clock skew against each reachable peer, and the last 400 lines of the database, etcd and api container logs. Where an update ran, it adds the update finisher’s state and log and the tail of the updater’s own trail, .taranac-update.log — with anything shaped like a password, key or token masked.

Those log tails are the services’ own words, included verbatim because a node that will not start usually explains itself only there. Everything Taranac itself collects is fingerprinted, but the logs are not ours to rewrite — give them a glance before sending the archive on if your policy requires it.

Reading a log by hand works the same on every kind of host, the witness included: ./taranac logs <service>. When that service has no container on this host, it says no <service> container on this host. and lists the containers that do exist (or tells you to start the stack), instead of printing nothing — so an empty answer always means the container itself logged nothing.

The rule is the same in every collector: a value that must only be compared is carried as a fingerprint, and a value that must not travel at all does not travel.

  • No shared secret, ever — only a 12-character fingerprint of it, the same one Taranac uses for key-anomaly reports. Enough to prove a mismatch, useless to an interceptor. The CoA secret is treated identically: a NAC preflight prints the CoA port and the fingerprint of the secret that would be used, never the secret. In an HA bundle the same applies to every cluster-wide value: database and replication passwords, the application secret key, the master key, the MFA API key and the Patroni REST password.
  • No passwords. The decoder reports that a password was present and how long it was, and redacts the value. In an NCM report every credential the plan resolved is masked before the transcript is buffered; the username is kept on purpose.
  • The generated daemon config is masked before it is stored. It is the one place a plaintext key is legitimately written, so it is also the one file that must never be handed over as-is.
  • The device’s configuration is not collected by an NCM run — the transcript stops before the first command. --full-transcript is the one deliberate exception, and it announces itself three times over.

Topology is not secret and is deliberately recorded verbatim in an HA bundle: addresses, member lists and node names are the entire point of it. Component log tails are the one place worth a glance before forwarding if your policy requires it — the etcd, database and api tails in an HA bundle, and the winbind, neighbouring-daemon and backend tails in a traffic bundle. They are the services’ own output rather than ours, and are included verbatim for exactly that reason.

If you want to decode TACACS+ payloads in Wireshark yourself, enter the device’s key in your own Wireshark — it is not, and must not be, in the archive.

  • The capture is capped at --max-size (100 MB by default) and wraps at the cap: the tail survives, the beginning does not. The summary says so when it happens, because a silently half-missing trace is worse than a short one — the gap looks like the device went quiet. Re-run with a shorter window or a larger cap if the start of the exchange matters.
  • A log may rotate mid-window. If the daemon trims or rotates a file during the capture, the byte offset no longer points at the right content; the bundle takes a bounded tail instead and the summary says which file it happened to.
  • Two clocks, two zones. The capture window is UTC and tac_plus logs in UTC, while the FreeRADIUS log is in the container’s local time. Both clocks are recorded in environment.txt — a three-hour “gap” between the capture and the log is usually just this.
  • Packets can appear twice. That is deliberate: capturing on any covers the physical interface and the Docker bridge, and the duplication is what proves the packet was forwarded into the container.
  • One device, one protocol, per run. If you are not sure which protocol the device is using, the context logs in the first bundle will tell you where the traffic actually went.
  • CoA is collected on a nac run only. The CoA port and secret are resolved by the 802.1X configuration check, so a tacacs or radius capture is filtered to the daemon’s own ports plus ICMP. Taranac’s CoA sender belongs to NAC sessions, so there is nothing on the other two runs for it to widen the filter for.

And for the others:

  • An NCM report describes one tracked configuration, collected from the core. A second tracked config on the same device is a second run — cheap, since each one is a single login.
  • An SNMP report describes one poll of one device, made from the core. When the device’s profile polls from a site collector the report says so at the top, and its conversation serves as a comparison rather than as the collector’s own poll.
  • An HA bundle describes one node. Nothing in it can prove the cluster is healthy; that conclusion needs a bundle from every node, the witness included. The one cross-node measurement a single bundle makes honestly is the clock skew against each reachable peer.