Dev Blog
Dev Blog
Short notes on building Taranac — new features, demos and behind-the-scenes.
1.2.8 — MS-CHAPv2 without a domain, and the rest of what you asked for
Last time nearly everything came from somebody else: four public issues, a customer’s Calix switch, the first HA cluster built outside our lab. That did not turn out to be a one-off week. It is now simply how the product finds out what is wrong with it.
Two more public issues are closed here, and they have the same shape: in both, the product held a belief about itself that had never been examined. It believed only Active Directory could verify an MS-CHAPv2 password. It believed a synchronized directory user was a person who should be able to log in. Neither was true, and nobody inside the project noticed, because you do not test a belief you have not articulated.
The third piece — the login banner — is the same shape again, and the second instance in two releases: a field that saved, rendered in a preview, and reached no device.
MS-CHAPv2 without a domain, and without winbind
This is public issue #13, and answering it meant separating two facts the product had been treating as one.
What Active Directory alone can do is hold a machine account: one winbindd, one
join, one domain, and everything that follows from being a member of it. What
MS-CHAPv2 actually needs is much smaller — the account’s NT hash. FreeIPA
publishes one. ipaNTHash is created for every user, byte for byte
MD4(UTF-16LE(password)), even on a realm where ipa-adtrust-install was never run.
So a directory that hands out the hash now gets a mechanism of its own. Device administration over RADIUS and 802.1X PEAP-MSCHAPv2 both take that branch, and which branch a login takes is decided by the user’s own directory — a node may hold a domain join and any number of hash-readable directories at the same time, and neither borrows the other’s verifier. Unlike the domain, this second mechanism is not exclusive: there can be as many of them as you configure.
The hash is read on each login and stored nowhere. Caching it would put a password-equivalent in a daemon’s memory and go stale the moment the person changes their password.
One operator action is always required, and the product hands it over rather than
describing it. ipaNTHash sits in an explicit exclusion list even in the
administrator’s own ACI, so an ACI granting your bind account read access has to be
added. The MS-CHAPv2 tab generates that LDIF with your bind DN and search base
already filled in, and Check directory reports whether the hash is readable —
asked with a filter that establishes the fact without pulling a single hash out of
the directory.
The part worth dwelling on is what happens when the ACI is missing. FreeIPA does not refuse the request; it answers without the attribute. A product that counted that as a wrong password would feed the brute-force guard and send you hunting a credential problem that does not exist — so a missing ACI is reported as exactly that, and never as a bad password.
The console is a decision you make once, for the whole installation
Public issue #14, and the reporter was describing a hole rather than an inconvenience.
A directory synchronized for the sake of NAC and AAA policy brings in policy
subjects, not console operators. A Domain Users mirror is thousands of accounts,
and until this release every one of them was a valid credential pair against the
management interface. The only lever that existed was is_active = false, which is
not a console switch at all: the TACACS+ user cache reads the same column, so
switching it off removes the person from AAA — from precisely the thing the directory
was synchronized for.
A group now carries a three-state console mode — inherit, allow, deny —
resolved against a new installation-wide default in Settings → Authentication.
inherit is the state that makes the default retroactive, and that is the whole
design: a group nobody has touched is already sitting in it, so one setting closes
the hundred groups already synced and every group the directory adds tomorrow. A
per-group flag could never have done that. Sync assigns policy only to groups it
creates, so a default set today would never have reached the ninety already there.
Administrators are never shut out — is_admins wins over everything — and the shipped
default is allow, so upgrading changes nothing about who can log in today. What
the control closes is the console, not the account: password reset, the certificate
download link and MFA enrolment keep working, and the BYOD captive portal is
deliberately untouched, because taking somebody’s console away must not take their
network with it.
Two details that only matter once you are living with it. The gate is re-checked at the second factor, not only at the password — a deny that arrives between the two steps has to land, or the window between them is the hole. And the group table reports the resolved answer rather than the stored one, marking which groups inherited it rather than chose it; assigning an RBAC role to a group that cannot reach the console is meaningless, so the picker goes read-only there while keeping the existing assignment visible. “We did not delete it” is something you have to be able to see.
A banner, at the scope a banner can actually live at
TACACS+ now carries a pre-login banner and a post-login message of the day, set at the realm, at a device group, or at a single device. A banner resolves device → oldest device group → global, which is the same chain and the same ordering the TACACS+ key already uses, and the two banners inherit independently of each other.
They are two settings rather than one because they ride in different packets and therefore reach different people: the pre-login banner is seen by anyone who can open a session, the MOTD only by somebody who authenticated. Fold them together and you have promised one audience something you are showing the other.
This replaces the MOTD field that used to sit on a TACACS+ profile, which could never have worked. A profile is chosen during authorization; a login banner is carried in the authentication reply, and the daemon accepts a banner at realm and device scope only. The field promised a per-role banner the protocol cannot express — and promised it convincingly, because the profile’s config preview rendered it while the deployed configuration carried it nowhere.
That is the second field of exactly this kind in two releases. 1.2.7 fixed the TACACS+ password prompt that was saved, rendered in the preview and never once put on the wire. Neither was found by a test, because both behaved correctly everywhere a test looks — the value was stored, read back, and displayed. What was missing was the last step, and the only instrument that sees it is somebody’s device.
The probe that changed what it was meant to observe
The stale-session worker used to ask a NAS whether a silent session still existed. First with a Disconnect-Request, which disconnected the users it was checking. Then with an identification-only CoA-Request, on the theory that a CoA carrying no changing attribute is a question.
It is not a question. Measured on a customer’s Huawei CloudEngine S5735, with
debugging on the switch and captures at both ends: that packet, sent to a session that
is definitely alive, is answered CoA-ACK in 30–90 ms and the RADIUS-assigned VLAN
is removed from the session. The user stays authenticated and drops to the port
PVID. RFC 5176 defines no read-only packet — a CoA-Request carries the session’s
authorization, and a NAS with replace semantics applies an empty one literally. The
probe was changing the state it existed to observe, on exactly the users who were
still online.
So it is gone, and nothing in Taranac now closes a session on the strength of a NAS’s answer. Liveness comes from accounting, in two windows, the way ISE, ClearPass and PacketFence read it. A session that sends interim updates ends after silence past the stale threshold. A session where only the Start ever arrived cannot be judged by silence at all — there was never anything to fall silent — so it is held by age, and that default moves from 72 to 120 hours, with a warning that names the cure rather than the symptom: periodic interim accounting on the switch.
Disabling that job also persists now. It used to be a pause held in the scheduler’s memory and lost on the next container restart, which is a trap laid specifically for the person who had paused it to stop a switch from being probed.
Two VLANs, and which one you are looking at
Public issue #11 started with a MAB’d phone that tags its own voice VLAN — nothing
is assigned to it, and the VLAN appears only inside NAS-Port-Id. Pulling that thread
found that sessions and authentication records carried a single VLAN field that meant
one thing or the other by turns, because accounting overwrote the assignment. Nobody
could tell which of the two a row was showing, and the one case genuinely worth
seeing — the two disagreeing, from a CoA that never landed, a hybrid port, or a
port somebody moved by hand — was invisible by construction.
They are two columns with two meanings now. On a session, a disagreement is flagged, because a session is where the port settled after authorization. On an authentication record it is not, because that is the VLAN the frame arrived in before any assignment was applied, and flagging it there would light an icon on every ordinary dynamic-VLAN authentication in the log. Filtering and sorting cover either half, so searching for a VLAN finds the sessions in it whichever way the number got there.
AAA, in the places your devices found
Most of this section exists because somebody’s configuration did not survive contact with ours.
- A TACACS+ AV-pair name may contain
#again. The directive-name whitelist shipped in 1.2.2 banned it outright, which locks an operator out of Cisco’s numbered families —inacl#1,outacl#1,route#1,interface-config#1. Templates saved before the ban could still be read but no longer re-saved: a 422 on a profile nobody had edited.#is the daemon’s comment character, but only at the front of a token, so that is where it stays banned. - A raw AV-pair sharing a service with the template now reaches the device. Each
raw pair was given a block of its own, and the daemon stops evaluating at the first
permitit reaches — so whenever the profile’s template already defined that service, the raw pair sat behind a block that always terminated. Login worked, authorization succeeded, and the attribute simply was not on the wire, with nothing logged, because nothing had failed. - A RADIUS template created after the daemon started never reached it. The handler read templates once at process start into a cache nothing refreshed. The Access-Accept went out carrying nothing at all while the UI and the AAA log both showed the profile as applied. Raw attributes survived that path, which is why they looked like the only part that worked.
- A seeded TACACS+ profile spells out its command default.
NULLwas read as permit by the renderer and as deny by the form, and a form submits what it shows — so a seeded profile authorized every command until somebody opened it to change something unrelated and pressed Save, from which moment the device refused every command while login still succeeded. - A new profile permits commands by default, which is a behaviour change and a deliberate one. A profile that works the moment it is attached and is narrowed afterwards beats one that authenticates and then refuses everything until you find the field — that failure reads as “TACACS+ is broken”, not as “this profile is strict”.
- A cleared attribute stays cleared, and a mandatory one may not be left empty. Clearing a field is how a defaulted attribute is switched off, so the form no longer discards the empty value on the way to the server; and an attribute the template calls mandatory is refused on save instead of quietly shipping a profile that does not do what its template says.
- A single-valued RADIUS attribute can no longer be saved with every alternative at once. A Palo Alto role could go out as superuser, superreader and deviceadmin in one Access-Accept, with nothing to tell the device which to believe. The template had already declared the answer; nothing read it.
- The capture decoder stops blaming your shared secret for its own misframing. A capture that starts mid-connection, or a segment boundary inside a packet, left the stream splitter misaligned, and the bogus packet it then produced was reported as the device is using a different shared secret — the most common real fault, asserted to somebody whose keys are fine. On a lab capture, 18 of 19 packets decoded with the device’s key and the 19th produced that verdict.
LDAP, where a sync reported nothing you could use
- Sync filters are visible without opening Advanced. A customer reported that AD user filters “do not work in any configuration”. They did — both sync filters were rendered inside the Advanced branch, so in the ordinary view they were off screen entirely while the login-time filter was not, and the preview answered about something other than what the form was showing.
- A group that brings in nobody now says why. “Members: 0” meant three different things — an empty group, a membership attribute we do not read, or an entry that could not be read at all — and looked like one. A customer spent an exchange hunting a fault in their own tree over the second case.
- The directory sync runs on the leader, not on every node at once. On an N-node cluster it was N concurrent binds against the customer’s directory every interval, N audit rows, racing upserts on the same users and groups, and N copies of any failure email.
High availability: a rehearsal instead of a description
cluster status and cluster connectivity describe how a cluster is built —
connectivity says as much itself, proving the legs that would matter after a
failover. Nobody had ever made one happen.
cluster verify does. Without a flag it is a read: the survivability verdict, roles,
lag, the connectivity matrix, and a non-zero exit if anything is not ready. With
--rehearse it hands leadership to a healthy replica, writes a real audited row on the
new leader and times it, waits for the old leader to come back as a streaming replica,
and hands leadership back — then says in words what happened and where it left the
cluster. It is a planned handover, never a kill: Patroni waits for the target to catch
up, so nothing is lost. On a standalone installation it exits 0 rather than complaining
about a cluster that was never meant to exist.
The verdict it opens with had to be rebuilt first. cluster status used to say
“losing any one database node keeps writes” on the strength of replay lag in
seconds, against a threshold of ours, while Patroni decides promotion on WAL
bytes, against its own. On an idle cluster both are zero, so the two never
disagreed on any stand we had. On a 1.9 GB stand under real writing they disagree
constantly: across 150 samples the line claimed survivability every time while 46 of
them had no replica inside Patroni’s bar — and live, within one second, status
promised survival while a switchover answered “2.5 GB of WAL behind”. The threshold is
read from Patroni’s live configuration now rather than copied into ours, so raising it
is respected.
The rest of the cluster work is a list of things a real deployment does and a lab does not:
- Every database node now runs with a budgeted connection ceiling. The product set
max_connectionsnowhere, so Postgres’ own hundred was in force — and under HA every node’s writes route to one leader, so the primary carries the whole cluster’s write side on top of its own reads. Three API engines alone ask for 90 of those 100 before a single daemon connects. New clusters get the budget on their own; existing ones cannot, because the value lives in a section Patroni applies only when a cluster is first built. Soverifyreports the ceiling each node is actually running with — asked over its own connection, not read out of a file — and exits non-zero while a node is short, naming the procedure. - etcd compacts its history. Patroni rewrites the leader key every ten seconds or so, and etcd’s default keeps every revision of it: about 13 MB a day on an otherwise idle cluster, holding history nothing ever reads, until fsync latency starts costing leader elections. Measured on a customer’s cluster at 127 MB, and on our own lab at 27 MB two days after a clean rebuild — universal rather than a symptom of scale, which is exactly why the default was not good enough to ship.
- The primary refuses to update while any member is behind, before anything is written. The old guard printed “update the replicas first” and carried on. One cluster met every hole in that at once on 15 August: a replica upgrade had died silently, so the operator read the warning and concluded, from everything he could see, that he had already done it. A warning about order is powerless the moment an earlier step lies about having succeeded.
- An upgrade that stops half-way says so, names the phase it stopped in — replacing files, writing the version pin, or pulling images — and reports the bundle’s version rather than a guess. Two defects that hid each other, and together they are why a customer was told to upgrade, watched it appear to work, and stayed where they were.
- A replica waiting for the leader’s schema says the node is fine. It used to restate two revision hashes, which is the one fact the operator was already looking at. It now says which way the two differ, that only the leader can create that schema, and that the order is replicas first and the primary last.
- The backup archive carried less than it claimed and said nothing about it.
config/etcd-cais the only protection against losing a machine and never being able to admit or replace a node again — and it has been missing from every archive since the feature shipped. The keys are written readable only by the operator’s own account, and the component that collects them is opportunistic, so there was neither a missing entry nor an alert. Both halves are fixed: the files are made readable, and an incomplete archive now reports itself. - A scheduled-backup alert is per node. It fails because this node ran out of space or lost a mount, so a shared identity let a failover launder a fault: a node with a full disk had its alert cleared by a successful backup on the node that took leadership, and nothing on it had changed.
- A daemon that loses the database retries, and stops being silent. Under load on
the stand, TACACS+ refused 163 of 180 logins while the container stayed healthy, port
49 answered and
cluster statusprinted Healthy — with 17 rows in the AAA journal for 180 attempts. A child that failed to get a pool within ten seconds latched for the life of the process and answered ERROR from then on, and the node stayed poisoned twenty minutes after the load stopped. - An ordinary daemon restart no longer pages anyone. A daemon that has just come up refuses while its caches load — 7 to 20 seconds on a loaded node — so a routine recreate landed a critical alert and an email, then resolved a minute later. A real outage lasts longer than the grace and still alerts at once.
Smaller things
- The dashboard’s backup widget says whether the archive is usable, and what is inside it. It used to say only that a backup exists. What it reports now is read out of the manifest that lives inside the tar, beside the payload, in plaintext — so the answer travels with the archive, survives being copied, and can be read without the master key.
- A device names its groups from its own form. Membership could only be written from the group side: linking a device meant leaving the device, opening the group and adding it there. Devices were the last entity to work that way, and the first customer migrating from TACACSGUI hit it. An absent field leaves membership alone, so editing an unrelated setting cannot silently unlink a device.
- The diagnostic bundle can explain a CoA exchange. Everything that reaches a switch outside the request/response flow is a CoA — an admin disconnect, a VLAN change, a port bounce, a guest revoke — and the bundle could explain none of it. It now captures on the CoA port resolved the same way the sender resolves it, captures the ICMP that distinguishes “the switch ignored us” from “nothing was listening there”, decodes replies against the right secret, and collects the backend log where a CoA outcome is recorded. See Diagnostics.
- Ubuntu 26.04 LTS is a verified installation target (issue #15). Installed on
a clean 26.04 by the documented route —
bootstrap.sh→install.sh— without a single workaround, and verified by more than containers starting: a live TACACS+ authentication with the right verdict for both a correct and a wrong password, and all eleven containers surviving a host reboot. - A sorted column header no longer rides over the resize line it shares with the next column.
1.2.8 ran 5067 passing, 0 failing on the dedicated runner: 4820 unit, service and integration at 81% backend line coverage, 233 end-to-end across six shards, and 14 load. Eight end-to-end cases are expected-fail — the documented known gaps — and nothing was skipped.
Every headline in this release has a name attached to it that is not ours: an issue, a switch, a directory, a cluster in somebody else’s rack. That is a much faster way to find out what a product actually does than any amount of testing our own assumptions, and it only works because people take the trouble to report things. Thank you — please keep doing it.
The bundle and the appliance images are on the download page, and what is coming next is on the roadmap.
1.2.7 — four issues, and the first cluster in the field
Last week was three releases in three days, and every one of them came from the product telling on itself — we shipped a tool whose job is to find out what is really happening, and then sat with what it said.
This one is the opposite direction. Nearly everything below was found by somebody else: four public issues, a customer’s Calix switch, and the first high-availability cluster built outside our lab, which found five separate defects in its own lifecycle simply by being built by someone who had not watched us build one.
That last one is worth saying plainly. HA has been tested here for months. It took one cluster in somebody else’s rack, assembled by somebody following the runbook rather than remembering it, to surface a command we document and never implemented, an upgrade path that leaves the database unable to start, and a way to reattach a machine to a cluster it should have been a stranger to.
An endpoint, classified by where its machine sits in the directory
Public issue #5. A machine bound to a directory computer object already carried that
object’s DN, and no rule could reach it — so “every machine under OU=Workstations
gets this profile” had to be maintained by hand, one endpoint at a time.
Two new classification match types close that: LDAP DN and LDAP group.
A DN is matched component-wise, never as a substring, so a rule for OU=Computers
does not also capture OU=Computers-Test. One semantic covers both uses: a container
matches its whole subtree, a full object DN matches that one object.
The rule also carries the directory it speaks about. With several forests joined, the same DN — and certainly the same group name — exists in more than one, and an unscoped rule is exactly how a machine from one domain lands in a group written for another with nobody finding out.
Matching on group membership needed data nobody was importing: a computer’s memberOf
was never read, for any directory. Collecting it is a new switch on the LDAP
configuration, off by default — an upgrade must not quietly start recording which
groups a customer’s machines are in without being asked. Turning it back off stops the
collection and keeps what was already collected. The endpoint’s card shows every group
the directory reports, with the time each was last seen, which is what answers why did
my rule not fire.
And machines are imported from directories that are not Active Directory
Computer sync now runs against FreeIPA, OpenLDAP and generic LDAP as well. The
difference between directories turns out to be one line of filter —
(objectClass=computer) on AD, (objectClass=ipaHost) on FreeIPA,
(objectClass=ieee802Device) on an OpenLDAP keeping machines the RFC 2307 way — and
every input was already a field on the form.
On FreeIPA and OpenLDAP it is in fact simpler than on AD, because both publish a MAC address: a machine becomes an endpoint on the first sync, before it has ever authenticated. The staging list of computers awaiting an address exists because Active Directory has no standard attribute for one; there it stays the normal path.
Google Workspace still declines, and says so: Secure LDAP serves users and groups, and devices live behind an API no filter reaches.
The password prompt that was never sent
Public issue #8, and its author was right.
The TACACS+ Password prompt setting was seeded, saved, and rendered as a field on
the settings page. No code ever put it into the generated daemon configuration. The
compiled-in Password: went out no matter what you typed, on every device, in every
release.
It reaches the device now. Leaving it empty restores the daemon’s own default rather than blanking the prompt.
Two things worth knowing about where it applies. The value is used only while MFA is on — announcing an OTP is what the prompt is for, and a value left behind by a since-disabled MFA setup must not keep taking effect. And only an interactive login can display it at all: a device that logs in over PAP carries the password inside the first packet and asks nothing, which Extreme SLX-OS does exactly. The hint beside the field now says both.
An attribute declared optional was sent as mandatory
RFC 8907 separates attr=value from attr*value. The first is mandatory — a device
that does not implement the attribute must refuse the whole authorization. The
second may be ignored.
Templates have always carried the distinction, and the profile form has always shown
it. The configuration renderer wrote = in both cases, so the declaration never
reached the wire.
The shipped example is our own built-in Cisco IOS-XR template, whose task
attribute is declared optional and has been going out mandatory in every deployment
there has ever been. A device that rejects it fails the exec authorization outright —
and on the device the symptom is indistinguishable from a policy problem, which is
where the time goes. Templates that declare nothing are unaffected, and the Config
Preview renders exactly what is deployed.
The interface, in the places you kept hitting
Two more public issues, and a set of things they dragged out with them.
#9 — list columns can be resized, and they remember their widths. On wide screens the columns did not fill the space and there was no way to give one more of it. Every list now measures its columns once real rows are on screen, freezes them, and puts a divider on the right of each header that drags that border. Narrowing a column can pull an overflowing table back inside its container and take the horizontal scrollbar with it; the last column is elastic, so the table always reaches the right edge. Double-click a divider to drop the layout and start over; arrow keys nudge a focused border.
Widths are stored per table in the browser, in pixels — a share of the container would follow the window, and a table that overflowed by 30px would keep overflowing on a wider monitor.
#10 — a parent breadcrumb led to “404 — Route not found”. Clicking NAC, AAA or Settings in the trail landed on the error page, because those crumbs are sidebar groupings rather than pages and were being linked anyway. A crumb is a link only when its path is a real page you have permission to open; otherwise it is plain text, which is what a section is. The same rule turns the last crumb of a detail page into a working link back to its list.
And the trail now names the page you have open. /nac/endpoints/<id> read “NAC /
Endpoints” — naming an ancestor and going silent about the object on screen. It closes
with the object’s own name now, the same label the page header already shows, so no
page had to opt in.
Three things that came out with them:
- Tables reserved width for text they would never show. Every list cut its cell text at a width written into that page’s own file — 200px here, 300 there — and the browser has to reserve those pixels, so the cap acted as the column’s floor. TACACS+ Profiles demanded 975px inside a 941px container and scrolled horizontally with nothing on screen explaining why. The caps are gone from all sixteen shared tables.
- Whatever a column clips is readable on hover — in every list, rather than in the ones whose author remembered to add a tooltip.
- The LDAP browser draws a tree. Five defects in the one screen you open to pick a container or a group: levels had no indentation at all, the row highlight was dead, the group-members preview answered “50 of 1000” forever, and a level holding four thousand machines drew four thousand rows. Leaves are capped at 200 now with the remainder counted and said out loud — while containers are never cut, because cutting one makes everything below it unreachable while looking exactly like a container that is empty.
The first cluster in the field found five things
All five are lifecycle defects: not “HA does not work”, but “building, rebuilding, upgrading or repairing a cluster does not work the way the runbook says”. They are the kind that only appear when somebody else drives.
Upgrading a cluster converted before 1.2.3 left its database unable to start.
Patroni’s control API became authenticated unconditionally in 1.2.3, so the database
image refuses to start without PATRONI_REST_PASSWORD. Every path that creates a node
has provisioned that key since — a fresh install, a conversion, a join. The path every
existing cluster takes did not. So ./taranac update handed such a cluster a database
image that would not run, and the failure surfaced three steps later as a container in a
restart loop rather than at the moment it could have been fixed calmly.
The upgrade provisions it now, and provisions it identically on every node — which
is the whole difficulty. This is a cluster-wide credential, and two nodes that each
generated their own would authenticate nothing to each other: every switchover, failover
and restart the cluster sent that node would come back 401, quietly, right up to the
failover that needed it. So the value is derived from the master key the cluster
already shares, which gives the same answer on every node with no coordination and
nothing new on the wire. A cluster that already has a password keeps it untouched.
--reinstall was printed as the cure by three different refusals, and existed in no
parser. Re-adding a machine that is already a member — it died, it was rebuilt, its
join never finished — is ./taranac cluster add-node --address <addr> --reinstall. The
refusals that send you there printed that exact command. The runbook documented it. The
wizard’s own --help listed it. Typing it produced unknown option '--reinstall'.
The behaviour behind the flag had been implemented in full; only the two lines that
accept it on the command line were missing, which is why reading the code did not reveal
it either. Both add-node and add-witness take it now.
docker compose -f docker-compose.yml up -d on an HA node started a second writable
database. On a cluster node the database belongs to Patroni, through the HA overlay.
The base compose file on its own put an ordinary, writable PostgreSQL on Patroni’s data
directory — two write histories on one data set, which no failover can reconcile.
./taranac and install.sh both refused to do that, and both are bypassed the moment
someone runs docker compose directly, which is a reasonable thing to do and is how it
happened. The refusal now lives in the compose file itself, so the guard is where the
damage would be.
A stale etcd member list was called harmless, and it stopped joins and rebuilds. Each
node keeps a declared list of the cluster’s etcd members; adding a node updates it on the
node you ran the command from and deliberately not on the others, so peers fall behind —
and the preflight reported that as drift that “breaks nothing right now”. It breaks two
things. etcd compares the list against the live ring whenever a member starts on an empty
data directory — a rebuilt disk, a --reinstall, a restored node — and refuses to start
on any difference. And cluster add-node hands its list to the node being added, so
running it from a node that has fallen behind produces a join that cannot succeed. The
preflight now compares every member’s declared list against the ring that is actually
running and stops the add, naming the members that differ — and ./taranac cluster sync,
the command the preflight prints as the cure, actually reconciles that list now. It
previously left the one key it was prescribed for untouched and reported “already matches
what the cluster reports”.
A cluster rebuilt under the same name was, to etcd, the same cluster. etcd derives a ring’s identity from a token plus its first members, and the token was the cluster’s name — so tearing a cluster down and building it again with the same name and the same nodes produced a byte-identical cluster identity. A machine still holding the old cluster’s etcd data, most often one whose join had failed and which nobody thought to wipe, was not a stranger to the new ring. It reattached in silence. A cluster draws its own identity when it is created now and discards it when it is dismantled, so a rebuild is genuinely a different cluster. Existing clusters keep the identity they have; there is nothing to migrate.
The witness that tried to run the whole product
A witness unpacks the same bundle as a Taranac node — that is how it gets
witness-join.sh — so ./taranac update has always been reachable on it, and it ran the
node’s update: pulled the product images, brought docker-compose.yml up on the
arbiter, and edited a .env belonging to a different stack.
What an operator saw was a wall of “variable is not set” warnings ending in unable to get image '/radius:'. That visible failure was the lucky outcome. A witness whose
.env happened to carry an image prefix and version would instead have started
PostgreSQL, the API and the AAA daemons on the host whose entire purpose is to be cheap
to lose.
The updater recognises what kind of host it is on now. On a witness it refreshes the
bundle’s files and stops there: it neither reads nor edits the witness .env, and it
starts nothing. It deliberately does not restart etcd either, because recreating the
arbiter is a quorum event and not housekeeping — if a release does change the witness
compose file, the update says so and prints the single command that applies it, for you
to run while the cluster is healthy.
The same rule now covers a host running a standalone NCM collector, and ./taranac
itself: on a witness or collector host it drives that host’s stack, and the commands
needing the database or API explain that they belong on a Taranac node instead of failing
with the same wall of warnings.
A collector was told it was up to date while nothing was being collected
When a collector’s snapshot contract does not match its core, the core fail-closes: it
dispatches nothing, and the collector logs that it must be upgraded. The way out is
./collector-join.sh --update, which moves the collector to the version its core runs.
It answered “already in step with the core — nothing to do”. For hours. While nothing was being collected.
The core reports its version on every authenticated answer and the collector records it — but that header was attached only when the request succeeded. So the one answer that mattered, the refusal, carried no version, and after the core was upgraded the collector kept reporting the version it had heard before the upgrade, which of course matched the version it was still running. The single door out of the fail-closed state was held shut by the failure it exists for.
The core reports its version on refusals too now. Unauthenticated callers still learn
nothing: the version is attached only after a caller’s signature has verified. And the
collector’s own tool no longer claims to be in step while its log shows the core refusing
it — it explains the contradiction and what to do — with
./collector-join.sh --update --version <ver> available to move it to a version you name
without consulting the recorded one.
Smaller things
- Calix AXOS ships as a built-in template. AXOS authorizes an exec session under the
service name
Loginrather than the RFC 8907shell, and takes the role from aROLESattribute — it does not implement priv-lvl at all. A stock shell profile is therefore refused, and what the operator sees is a successful SSH login into a CLI that accepts onlyexit, which reads like a policy problem and is not one. Verified against a customer’s E7 running AXOS-R24.1.0. Command authorization stays off deliberately: AXOS documents accounting for commands but no per-command authorization, and offering command sets the device never asks for would only invite a profile that silently does nothing. - A sync mode nobody knows ran a user sync and reported success.
POST /ldap/{config_id}/synctakes an optional per-run mode, and anything that was not one of the two it knows fell through to a user sync answering{"status": "completed"}—computer_sync, which is a real operation on another route entirely, and every typo alike. An unknown mode is refused now, naming the modes that exist. The interface was never affected; this cost scripts and integrations calling the API directly. - A setting with a fixed set of values could be changed to nonsense through the API. Four LDAP settings accepted any string on update. The tolerance was deliberate and its reason was sound — every edit form posts the whole object, so a stored value this release does not know has to come back unchanged on saves that have nothing to do with it — but it was implemented as “accept anything”. The rule asks the right question now: re-sending what is stored always passes, changing it must land inside the known set.
- A classification pattern without a wildcard matches any part of the name, and now
says so. A hostname, certificate-identity or vendor rule written as
PC-01also catchesPC-011,OLD-PC-01andPC-01-TEST. That behaviour is deliberate and stays — changing it would rewrite rules customers have already written, and for vendor names the substring is the required behaviour, since an operator typesCiscowhere the database holds “Cisco Systems, Inc”. What was missing was the sentence. The field hints and match-type descriptions carry it now, along with the way out: a character class turns the value into a glob, soPC-0[1]matchesPC-01and notPC-011.
1.2.7 ran 4706 passing, 0 failing on the dedicated runner — unit through sustained-rate load under HA failover — at 81% backend line coverage, dated 2026-08-18 and commit-pinned. One number in there went down and is worth a sentence: the end-to-end tier reads 220 against 1.2.6’s 229, and none of those nine went missing. They are time-window cases that build a cron window around the current hour, and this run crossed the single 23:00–24:00 UTC hour in which the shape they need cannot be expressed at all — so they skip, with that reason attached, rather than assert a window that cannot hold. Eight further end-to-end cases are tracked openly as expected-fail, as they have been every release.
Four of the things above have issue numbers, which means four people took the time to write them down instead of working around them. That is worth more to this product than it probably feels like from the outside: the password prompt had been inert since the setting existed, the IOS-XR template had been sending a mandatory attribute in every deployment there has ever been, and neither would have been found from in here, because from in here they both look like they work.
If something is behaving strangely, the issue tracker
is the fastest route, and ./taranac diagnose will
usually tell you which half of the problem is ours. Existing installations update in
place with ./taranac update; new ones start from the appliance. Where the
platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.6 — the third one this week
1.2.4 was Wednesday. 1.2.5 was Thursday. This is Friday.
That was not a plan. It is what a week looks like when the previous release ships a tool whose entire job is to find out what is really happening — and then it does. Nearly everything below was found by looking, and two of the faults had been sitting in the product for as long as the features they broke.
So this release does both halves of that loop: the diagnostics grew two more modes, and the things worth diagnosing got fixed.
One section is not a fix and not a tool, and it is the one to read before you upgrade: mail. The rest can be read afterwards.
The collector learned two more questions
1.2.5’s ./taranac diagnose answered one
question: what happened between this device and Taranac. It answers two more now.
An HA cluster, from every side of it
./taranac diagnose --haOne archive describing this node’s view of the cluster: its role and timeline, the etcd ring, Patroni’s view, replication and slots, reachability to every declared member on every port — the etcd peer port included — certificates, clocks and logs. It reads state and changes nothing.
Run it on every machine, the witness included, and send all the archives. That is
the point 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 — and a disagreement cannot be seen from one side of it. Each bundle
prints a single cluster-fingerprint line, so checking whether every cluster-wide
secret agrees is one glance rather than a diff.
It is deliberately built for the states you actually need it in, which are the states where nothing works:
- a node whose database will not start — it takes the timeline straight off the data volume when the container is dead;
- a node that was never converted;
- the witness, which has no database and no api at all.
It reads .env from the host, and asks a peer’s Patroni when the local one is gone.
A node that cannot start still explains itself, which is the only condition under
which a cluster diagnostic is worth anything.
Every value Taranac collects appears as a short fingerprint — enough to prove that nodes agree or disagree, never enough to carry the secret. The last lines of the database, etcd and API container logs are included verbatim, because a node that will not start usually explains itself only there. Those are the services’ own words rather than ours, so give them a glance before forwarding the archive if your policy requires it.
One configuration collection, in four layers
./taranac diagnose --protocol ncm --config "<name>"Configuration collection can now explain itself. This runs one real collection
through the same engine a scheduled run uses and writes a single readable report to
./diagnostics. It records nothing in Taranac: no run, no result, no snapshot, no
version. Exactly one login is attempted, never a retry loop — though on a source
using a just-in-time credential, that login rotates the managed account’s password,
exactly as a real collection would.
The report answers in four layers, each standing on its own even when the next cannot run:
- What we were about to do, with no network at all — transport, address, port, credential mode, recipe, and the driver we resolved together with why.
- Whether the port answers.
- What the SSH handshake agreed on — attempted without credentials, so it cannot contribute to a lockout.
- The collection itself, with the dialogue that produced it.
The first layer is the point rather than a preamble. The failure that prompted this tool — a driver name the library does not have — happens before a socket is ever opened, so a session recorder would have handed back an empty file and a shrug.
Passwords are removed from the transcript before it is written; the account name is
kept, because which account was used is usually the question. By default the
transcript stops once the session turns to fetching the configuration. Add
--full-transcript and it keeps going — which means the report then contains the
device’s configuration. The command says so, and the finished run says so again.
Three faults on Huawei, one of them total
Every 802.1X authentication failed as soon as the client had IPv6.
Huawei numbers its RADIUS attributes two incompatible ways — one for carrier equipment, one for campus switches — and the 802.1X service was reading campus switches with the carrier list. The two disagree about the type of eleven attribute numbers, and one of those is the attribute a switch fills with an IPv6 address. Read as text, sixteen bytes of address are not text, and the authentication was refused before any rule of yours was consulted. Seen on an S5735 running VRP V200R021, where it rejected every EAP-TLS login.
The 802.1X service now reads campus switches with the campus list. Device administration over RADIUS is deliberately left on the carrier list: 802.1X is the path these switches take, and changing both at once would have widened the blast radius of a fix that needed to ship. If you see the same symptom on device administration from a Huawei campus switch, tell us.
If you added a Huawei attribute to the vendor dictionary by hand, check it after upgrading. The two lists also disagree about the name of 28 further attribute numbers, so an entry written against a name that exists only in the carrier list will no longer be sent — silently, because a name Taranac cannot resolve is simply absent from the reply rather than an error. Nothing Taranac ships is affected; this is only about entries you added yourself. A related error in the same definition is corrected too: downloadable ACLs for Huawei were built under an attribute name that exists in neither list, so a Huawei dACL could never have reached a switch in any release.
And a Huawei port name could stop the authentication journal recording anything at all.
Configured with the newer NAS-Port-Id format, a Huawei switch sends a port description longer than the column that stored it. The write was refused, the batch containing it was retried unchanged, it failed again — so every authentication that followed queued behind a record that could never land, until the queue filled and began discarding real entries.
What made this expensive is that authentication itself kept working. The log is written after the fact, so devices authenticated normally while the operator watched a journal that had gone silent — which is the worst possible arrangement of symptoms, because the thing that looks broken is the thing that is fine. The column now holds the full length the protocol allows, and — the more important half — a record that genuinely cannot be written is dropped on its own, loudly and in its own words, instead of taking every record behind it down with it. The same protection went into device administration over RADIUS, which had the identical flaw waiting in it.
Entries lost while the log was blocked cannot be recovered; they never reached the database.
Windows machine certificates were logged as PEAP
Certificate templates on a Windows authority commonly issue machine certificates with an empty subject, putting the identity in an extension instead. Taranac decided the authentication method from the subject’s common name alone — so for those certificates it found nothing and fell back to its last-resort guess, PEAP, for a session that was EAP-TLS from beginning to end.
The consequences went well past a wrong label. A policy rule matching on EAP-TLS could not match these sessions at all, so the profile, VLAN and ACL you intended were never applied. And everything Taranac derives from a certificate — the identity, the issuer, whether it belongs to a machine or a person — was skipped in silence.
A certificate the peer presents is now recognised by any of the fields a certificate always carries, rather than by the one it may leave empty. A session that genuinely is PEAP or TTLS carrying a client certificate is still reported as PEAP or TTLS.
Two things to know. Earlier records are not rewritten — only authentications after the upgrade are classified correctly. And if you worked around this by writing a rule that matches PEAP to catch your Windows machines, that rule will stop matching them. They now present as EAP-TLS, which is what they were all along.
The interface a device authenticated on
Switches report the port in whatever shape their vendor chose. Cisco says
GigabitEthernet0/3, MikroTik says ether2, and those pass through unchanged. Huawei
and H3C encode it — as a key/value string, or as sixteen digits with the slot,
sub-slot, port and VLAN packed inside. Both are decoded now, and the raw value is
kept beside the readable one, so nothing is lost and which port was that? stops
requiring a lookup table.
The readable name is what the NAS Port column shows in the NAC authentication log and in sessions, with the raw value on hover, and the log sorts on it. It appears as Interface in the detail panel of both, in endpoint discovery, in CSV and XLSX exports, and in both NAC report templates, where it is shown by default. Free-text search matches it.
It is filled in as authentications arrive. Records written before the upgrade keep an empty Interface: the raw value they stored is the only evidence there is, and reconstructing the rest would be inventing it. The digit-packed form is decoded only when every field falls inside the range the vendor documents — otherwise the raw value stands alone rather than becoming a confident wrong answer.
Masking became a setting instead of a law
Taranac has always masked passwords, keys and community strings in collected configurations even when no scrub set was assigned — a safety net running underneath your rules rather than alongside them. That made it impossible to store a configuration exactly as the device returned it, and it produced a result that read backwards: removing a scrub set masked more, not less, because the exceptions that rescue benign lines live in the set.
A scrub set now carries a Mask secrets automatically switch. Leave it on and nothing changes. Turn it off and the set’s own rules become the entire policy — with no rules at all, the configuration is stored exactly as collected. Rules are optional; a set may consist of nothing but this switch.
In that mode configurations keep their secrets in storage, and they appear that way in diffs, notifications and exports — so the form says so where you flip it, the set’s view repeats it, and the scrub-set list flags every set that has it off. Recipes with no scrub set stay fully masked: an absent set cannot express the intent, and a recipe can lose its set without anyone asking it to.
Update every standalone collector in the same maintenance window as the core. A collector on the previous version stops collecting entirely — every tracked configuration it serves, not only the ones this switch touches — rather than storing a snapshot masked differently from what the core expects. It idles rather than guesses, and resumes on its own once updated.
And a narrower way to reach a real password
Where the switch above stores the configuration in the clear for everyone who can read NCM, a recipe can instead keep an unmasked second copy: the normal configuration stays masked exactly as before, and an encrypted copy of the device output as returned — before any trimming or masking — is kept alongside it.
Reading that copy, on screen or as a download, requires the separate Reveal raw config permission, which is not implied by permission to view configurations. Every access is written to the activity log, and it is written before the copy is decrypted — so a read that gets as far as the stored ciphertext and then fails is recorded just as a successful one is.
It is off by default and set per recipe. Standalone collectors do collect it, and hold it in memory only: it is never written to the collector’s local queue, so it cannot sit on disk at a remote site. If the collector restarts, or the core is unreachable long enough for the result to be queued, that one capture arrives without its unmasked copy — the configuration snapshot itself is never lost.
Mail, and a default that loosens security
Read this one even if you never touch mail settings.
SMTP settings gain a Verify the server’s TLS certificate switch and, when it is on, an optional CA certificate field. They appear only for STARTTLS and SSL, since with encryption off there is no certificate to judge. The same trust settings are used by the test and by real delivery, so a test that passes can no longer sit beside mail that quietly fails on trust.
A pasted certificate is added on top of the system trust store rather than replacing it — the difference between fixing the common case and breaking every other one. A server whose certificate comes from a public authority but which forgets to send the intermediate is fixed by pasting just the intermediate, while everything else keeps working with the field empty. A self-signed certificate is equally valid here, and a bundle of several is accepted. Text that will not parse is refused when you save it, on the field, rather than at the next send.
Here is the part that changes your installation. Until now Taranac always verified the mail server’s certificate and gave you no way to say otherwise — which is why an internal server with a self-signed certificate could not be used at all. The new switch is off by default, and on upgrade that includes installations which were verifying successfully all along.
Mail keeps flowing everywhere it flowed before, and starts flowing where an untrusted certificate was blocking it. What changes quietly is that the connection is still encrypted but no longer authenticated — so something sitting in the path could present any certificate it liked and collect the SMTP password.
If your mail server presents a certificate worth checking, and anything signed by a public authority certainly is: turn the switch on after upgrading, and paste the missing intermediate if the server sends an incomplete chain. The default was chosen so that no installation breaks on upgrade. It was not chosen because off is the better setting.
The SMTP test says what actually went wrong
It used to answer Request failed with status code 502. Every failure — a name that
does not resolve, a port nothing listens on, a rejected password, a TLS handshake that
collapses — arrived as the same line of framework text, naming no cause and no setting.
The reason existed, but only in the server log, which is the one place an operator
configuring mail through a web interface will not be looking.
The test now answers with the cause, in your own language, and names the setting to go
and change. A timeout points at the port and the encryption mode, and spells out that
587 normally means STARTTLS while 465 means implicit TLS. A refused login says the
credentials were rejected. Where the mail server explained itself, its own reply is
quoted after the sentence — 535 5.7.8 Username and Password not accepted settles a
question no amount of guessing will. The password you typed cannot appear there: the
quoted text is built from what the server sent back, never from what was sent to it.
And an untrusted certificate is called by its name rather than blamed on the port. It arrives disguised as an ordinary connection failure, so the diagnosis now follows the chain of causes down to the certificate error underneath instead of judging by the outermost one. Telling an operator whose address and port are both correct to go and check the address and port is the kind of wrong answer that costs more time than no answer at all.
Updating stopped hoarding images
./taranac update fetched the new images, started them, and never removed what they
replaced. On a demo host thirteen releases had piled up 30 GB of superseded images
— enough, on an appliance’s disk, to eventually stop the stack a year and a dozen
updates after the change that caused it, with nothing at the point of failure pointing
back at the cause.
Cleanup now runs at the end of an update, and the first update onto this version sweeps up what earlier ones left behind. It is deliberately narrow:
- Only images belonging to this installation are ever considered. Anything else on the host, including other people’s software, is never touched — and if the installation does not record which images are its own, it removes nothing and says so.
- Two versions are kept: the one just installed and the one it replaced. The rollback this same script offers is worthless if the previous images are gone and the registry is unreachable, which is exactly the situation a rollback happens in.
- An image another container still uses is reported, not forced, so a standalone collector, captive portal or cluster peer sitting on a different version survives untouched.
- Every failure here is non-fatal. An unreclaimed gigabyte must never turn a good update into a failed one.
./taranac update --keep-images skips it. One wrinkle worth knowing: the flag is read
by the updater already on disk, so on the update that brings you to this version it is
not understood yet and will be rejected before the update starts. Leave it off there.
The cleanup itself does run on that first update; the opt-out becomes usable from the
next one onward.
Smaller things
- An update could be offered to you while you were already running it. The version
pinned in
.envwas compared exactly as written, so a file with Windows line endings — or a value someone had put in quotes — never matched the published version, and./taranac versionreported an update available for the release already installed. Every.envvalue is now read the same forgiving way. As above, this takes effect from the next update onward. - A TACACS+ template for Extreme SLX-OS. On SLX-OS the privilege level is not the
role: set it and nothing happens, the switch reports the user’s role as unavailable,
and an administrator who authenticated perfectly well lands in the default role able
to do nothing. The role travels in a
brcd-roleattribute, and the template builds that reply, withadminanduseras ready choices and any role you have defined accepted as free text. Bind a profile made from this template only to SLX devices — the attribute is sent as mandatory, which SLX-OS requires, and a switch from another vendor answers an unknown mandatory attribute by failing the whole authorization rather than ignoring it. - A form opened over another form now submits only itself. Creating a scrub set from inside a recipe saved and closed the recipe underneath as a side effect; when the new set was invalid, the validation message was raised and then destroyed along with the component showing it, so the operator watched the form vanish with no explanation and no set. Fixed everywhere in the product, not only there.
1.2.6 ran 4561 passing, 0 failing on the dedicated runner — unit through
sustained-rate load under HA failover — at 80% backend line coverage, dated
2026-08-14 and commit-pinned. That is 88 more tests than yesterday’s release at the same
coverage; app/scripts/ still sits outside the paths the gate walks, so the diagnostic
collector and the new NCM session recorder still do not count toward the denominator,
and their own tests still pass without being wired in. End-to-end ran in the same six
shards CI uses rather than one session, and all six were green on the first attempt.
Three releases in three days is not a cadence and it is not a plan. It is what happens
when you build something that tells you the truth and then have to sit with what it
says. Existing installations update in place with ./taranac update — read the mail
section first; new ones start from the appliance. The diagnostic
collector has a page of its own, and where the platform
goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.5 — what actually happened on the wire
The support ticket says “TACACS stopped working”. It is a completely fair description and it is almost never enough, because the explanation is not in one place. It is spread across four: the packets on the wire, the service’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. So this release collects them with one command — and, because we hold both the traffic and the key, decodes them, so the archive is readable by the person who collected it rather than only by us.
The other half of 1.2.5 is two faults with the same shape as each other: Taranac misreading what a device actually said, and then confidently reporting something that sent the operator in the wrong direction. One of them was also quietly skipping multi-factor authentication.
One command, one archive
./taranac diagnose --protocol tacacs --device 10.0.0.5Start it, reproduce the problem on the device, press Enter. It writes one
.tar.gz under ./diagnostics and prints, in plain words, what it noticed —
which is often enough to settle the question without sending the archive
anywhere.
--protocol takes tacacs, radius (device administration over RADIUS) or nac
(802.1X port access). --duration SEC captures for a fixed time instead of
waiting for Enter, for the case where reproducing the problem and watching the
terminal are not the same person’s job.
It checks two things before it captures anything, because the answer is frequently already there:
- Is this device configured in Taranac for this protocol at all? A device record that exists but is disabled, a protocol flag that is off, a device whose shared secret resolves to nothing — each of these is excluded by the configuration generator, so the daemon has no client entry and drops the device’s requests before any policy is evaluated. Each is now named as a finding, in those words.
- Which ports are the services actually listening on? Not the documented defaults — the running system.
Those two matter more than they sound, because a device whose protocol flag is off and a daemon on a non-default port produce identical symptoms and identical empty captures. Anyone who has collected a diagnostic and got nothing has met one of them.
If several network objects contain the address — a host entry and the subnets around it, each with its own key or inheriting a different one — every candidate is carried, most specific first, and the report says which one the daemon will actually use.
Decoded, because otherwise you cannot read your own capture
A TACACS+ body is obfuscated on the wire. Hand someone a raw pcap of their own TACACS+ traffic and they have a file they cannot read — not because of a secret they lack, but because the format hides it from everyone. Taranac is the one party holding both the traffic and the key, so the decoding happens inside the api container: capture in, text out, the key never leaves.
RADIUS and 802.1X are decoded too, 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 precisely the “traffic is clearly flowing and Taranac says nothing” case people collect diagnostics for in the first place.
Attributes are named using the same dictionary the service itself generates, so the custom entries you added in 1.2.4 are named correctly here as well, rather than showing up as numbers in the one document where you most need to recognise them.
The traffic is captured on the host, not inside the container. The daemon
images ship no tcpdump, and installing it into a container does not survive the
next recreate. Capturing on the host also sees both the physical interface and the
Docker bridge — so “it arrived at the host and never reached the container”, a
firewall or NAT problem, is visible rather than merely absent.
What it will not write down
No shared secret is ever written into the archive. This archive is made to be emailed to support, and a key in a mailbox is a key on the internet.
A secret appears only as a 12-character fingerprint — the same one Taranac already uses for key-anomaly reports. That is enough for the single most useful finding available: the decoder reports which of a device’s possible keys was actually used, and a body that decodes under none of them is near-proof that the two sides disagree about the secret. That is both the commonest fault in this area and otherwise invisible, because a mismatched key does not produce an error — the service simply stops answering.
Passwords are never printed either: only whether one was present and how long it was. The generated daemon configuration is included with its secrets masked.
And an empty capture is reported as empty, with the advice not to send it. An archive with no traffic in it cannot show anything, and the round trip to discover that costs a day.
802.1X, split into attempts
An 802.1X capture is not printed as one flat stream. It is split into separate attempts, each with a headline: who, by what method, how it ended, how many rounds it took, and how long.
Two of those endings are outcomes no log can contain, which is the entire 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. There is nothing to log, because from the server’s side nothing happened — it is still waiting.
- A TLS alert, spelled out rather than left in hex:
unknown_ca — the client does not trust the server certificate's CA,certificate_expired, and the rest.
Both of those are, in practice, a certificate problem being reported to the operator as an authentication problem — which is where the hours go.
The logs around the one you asked for
Alongside the chosen service’s log, the bundle carries short tails of the other two and of winbind, clearly marked as context.
Winbind is there for a specific reason: it is what actually verifies a domain password. When a domain login fails, the reason is in winbind’s log and nowhere in Taranac’s own. And an operator who collected the wrong protocol — easy to do, and usually only discovered afterwards — still gets a bundle that can say where the traffic really went.
An enable request is no longer a user who does not exist
A switch asking for enable over RADIUS sends no account name. It sends the
synthetic $enab15$, which no directory has ever contained.
Taranac looked that up as an ordinary user, failed to find it, and answered user not found — which reads exactly like a wrong password and sends an operator hunting for an account nobody ever created.
There is now a global enable password on Settings → RADIUS, sitting beside the shared secret and behaving the same way: revealed on request with the reveal recorded in the audit log, copied, regenerated, saved only when changed. It is generated at first boot, as the shared secret is, so the feature is never live with a blank password — and it is meant to be changed before it is handed out.
One password serves every privilege level, because the request identifies the device and nobody else. For that same reason multi-factor authentication cannot apply to an escalation, and the interface says so plainly rather than leaving it to be discovered later. Granting the privilege level at login — where the user is known — remains the stronger arrangement, and that has not changed.
Escalations are also visible for what they are in the authentication log now: a
badge reading enable → 15 beside the synthetic name, with the request type and
requested level in the detail panel. Without it an escalation looks precisely like
a failed login by a non-existent account, because every column that would be
filled from a user’s record is empty.
This is recognised for Cisco IOS / IOS-XE and Cisco Business, H3C Comware 7 and Eltex ESR, which share the convention. Equipment that sends the real user name for enable — Aruba AOS-S, ASA, AOS-CX 10.15 and later — already worked and is unchanged, as is equipment with no separate escalation step at all.
A PAP login was reported as a failed enable — and skipped MFA
This one is the reason to update.
TACACS+ lets a device send a login either as an interactive dialogue (ASCII) or in a single packet (PAP). Both are ordinary logins, and which one a device uses is its own choice — some send PAP for SSH, Extreme SLX-OS among them.
Taranac told logins and enable requests apart by a marker that, it turns out, means something else entirely: whether the session can hold an interactive dialogue. A PAP login cannot. So every PAP login was taken for an enable request, and two things followed from that.
The visible one: a user typed a correct password at a normal login prompt and was told “Enable authentication failed”.
The one that matters: enable deliberately does not ask for a second factor — correctly, since the request identifies a device rather than a person. So an account whose policy requires MFA was let in on the password alone.
Login and enable are now separated by the request’s own service field — the thing the device sets, and the very thing that makes the daemon treat a request as an enable in the first place. It is independent of how the password was transported, so ASCII, PAP, CHAP and MS-CHAP are all classified correctly.
Who is affected: any installation with devices that authenticate over PAP. Those logins now work, MFA is enforced on them as the policy always said it was, and the authentication journal stops filing them under enable. Enable itself is unchanged, as is anything using ASCII login.
What to do: nothing. The corrected daemon configuration is written out on update. Until it is, logins are treated as logins — the safe direction — and the service log carries a one-time note.
Two smaller things
The authentication log says why a login was rejected in your own language. The
reason column printed the service’s internal token: user_not_found,
mschap_no_nt_hash, chap_unsupported, domain_time_skew and thirty-odd more,
shown to operators exactly as they are written in the code. All 36 now have text
in every bundled language. A token from a service newer than the interface still
falls through to the raw string rather than leaving the cell blank — an
untranslated reason is more use than none.
The updater stopped asking for a sudo password on every single update. It
tells you sudo is needed once, to give the api service read access to .env,
and then asked again every time — which is the kind of small dishonesty that
teaches people to stop reading what a tool says. Two separate causes, both fixed:
pinning the new version rewrote .env as a brand-new file, dropping the group
that access depends on; and the check meant to skip the work when the file was
already correct reached for the privileged command before looking at the file. The
promise the message makes is now true.
1.2.5 ran 4473 passing, 0 failing on the dedicated runner — unit through
sustained-rate load under HA failover — at 80% backend line coverage, dated
2026-08-13 and commit-pinned. Coverage is down two points from 1.2.4 and the cause
is the denominator rather than a thinner suite: the diagnostic collector adds
about a thousand statements that the coverage gate’s paths do not reach. Its own
tests exist and pass — 31 of them, 22 belonging to the decoder — they are simply
not wired into that gate yet, and widening it is its own change rather than
something to slip in beside a release. The breakdown ships in the bundle as
TESTING.md, including the eight end-to-end cases tracked openly as expected-fail.
If you have ever been asked for “the logs” after an authentication failure and had
to decide which of four places to look in, that is now one command. And if you have
devices that speak PAP, this release is the one where their logins stop being
reported as something they never were. Existing installations update in place with
./taranac update; new ones start from the appliance. Where the
platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.4 — tightening the bolts
1.2.3 was about what it costs to deploy Taranac. 1.2.4 is about what it costs to trust it.
Most of this release is fixes, and one kind keeps coming back: something reported success and did nothing. A custom 802.1X server certificate that the page called “Deployed” and no supplicant ever saw. A RADIUS attribute the authentication log called applied and the switch never received. A directory synchronisation that finished with a green status and zeroes across every counter. An update that replaced the container images, left the rest of the product at the old version, and reported success.
Those are worse than errors. An error sends you somewhere. A silent success sends you nowhere, because as far as every screen you can see is concerned the work is already done — so the fault gets attributed to the network, the switch, the directory, anything but the thing that quietly did nothing.
There are two new capabilities as well — your own RADIUS attribute dictionary and groups inside groups — and one breaking change that deserves five minutes before you pull.
Before you update: two CA certificates that were quietly one
An LDAP configuration holds two CA certificates, and they check opposite directions of two different handshakes:
- the one on Connection checks the certificate the directory server presents to Taranac over LDAPS;
- the one on EAP-TLS checks the certificates client devices present to Taranac during 802.1X.
In a Windows domain the same enterprise CA usually issues both, which is exactly why they look like one setting entered twice. Until now the TACACS+ and RADIUS services leaned on that resemblance: with the connection’s own field empty, they quietly used the EAP-TLS certificate for the LDAPS handshake. That substitution is gone. Each field now does its own job and nothing else — which also ends a disagreement inside the product, since the web interface and the 802.1X service never behaved that way.
Who is affected: installations where the LDAP configuration’s CA
certificate field is empty, Validate TLS certificates is on, the directory is
reached over ldaps:// or StartTLS, and the EAP-TLS CA certificate field
is filled in. For those, TACACS+ and RADIUS logins against that directory stop
working after the update. Configurations with their own CA, configurations
without encryption, and the EAP-TLS certificate’s real job are all untouched.
What to do, before updating: open Settings → LDAP → your configuration → Connection and paste the CA that issued the directory server’s certificate into CA certificate. If it is the same enterprise CA as on the EAP-TLS tab, the Use the EAP-TLS CA button next to the field copies it across.
The copy is not automatic on purpose. The two are the same certificate only in a Windows domain, and silently moving one into the other would be putting a certificate you did not choose into a security setting. And if you update first, nothing is lost and nothing is hidden: an empty field in this situation is now reported in three places — the service log, Test connection (including when the test otherwise passes), and a notice on the form itself.
Attributes your equipment defines, and FreeRADIUS does not ship
Vendors define attributes all the time that the dictionaries bundled with the RADIUS services do not carry. Until now there was no supported way to teach Taranac about one: a profile referring to such an attribute produced nothing, and said nothing, anywhere.
Both services now carry an Attribute dictionary on their settings page — Settings → RADIUS for device administration, Settings → NAC for 802.1X. Paste the vendor’s own dictionary text into it:
VENDOR Example 32473BEGIN-VENDOR ExampleATTRIBUTE Example-Policy-Id 1 stringEND-VENDOR ExampleOnce saved, that name works like any other — as a raw attribute on a RADIUS
profile or an extra attribute on an authorization profile — and it is sent in
Access-Accept. VALUE lines work too, so an attribute can have named values
instead of bare numbers.
The text is kept exactly as you typed it, which means you can paste a vendor’s file and still diff it against theirs line for line. A mistake is refused on save and named with its line number in your text, not in the file the daemon eventually reads — those are different files, and the second number is useless to the person who typed the first.
Next to the editor, What already exists opens a searchable list of everything the service already ships — roughly 8500 attributes across 195 vendors — filtered by attribute name or by vendor. It is worth opening first: your own dictionary is only needed for names that are not in there. A name that already exists is refused on save; a new name that reuses a number already in use is accepted and then quietly takes the original’s place in what the daemon reports, which is a thing worth knowing before it happens rather than after.
Saving restarts the service, in under a second, automatically — attribute dictionaries are only read at start-up, so a reload would apply nothing. The standard dictionaries stay loaded throughout, and the two services keep separate dictionaries: an 802.1X attribute does not have to be shared with device administration.
And an attribute given by numbers now actually leaves the building
The feature above arrives next to the fix that makes it matter.
An additional attribute on a RADIUS profile can be identified by name or by the vendor + attribute-number pair, and the form has always accepted both. Only the named ones were ever sent. An attribute given by numbers was left out of the Access-Accept entirely — while the sign-in still succeeded, the interface still showed the profile, and the authentication log still recorded it as applied. The only trace in the entire system was one line in the RADIUS service’s own log.
It is now sent the way the dictionaries address it: Attr-26.<vendor>.<number>
for a vendor-specific attribute, Attr-<number> for a standard one. That covers
every vendor the service already knows, and for the rest it works the moment you
declare the vendor in your own dictionary. An entry with neither a name nor a
number cannot be addressed at all and is now skipped with a warning, instead of
being handed to FreeRADIUS as an empty attribute.
NAC authorization profiles got the same treatment: an extra attribute whose value was not text — a number entered through the API — used to disappear the same way.
Groups inside groups, and who decides membership
Directories express a role as a group containing other groups far more often than as one flat list. It is the ordinary shape of an Active Directory tree, and FreeIPA has a dedicated object class for it.
Taranac imported direct members only. Everyone who belonged through a nested group was left out — and the run reported success, so a group that looked imported carried a fraction of its real membership, indefinitely.
Members of a nested group are now imported and become members of the group you selected, however deep it goes. The nested group itself is not created in Taranac: a group here carries a permission model, and inventing a group you never chose means inventing permissions. A nested group living outside the search bases you configured is followed correctly too, because members are read one entry at a time rather than searched for inside those bases.
It is on by default, for existing configurations as well as new ones, and can be turned off per configuration (Synchronization → Follow nested groups) — at which point a nested group is named in the sync log rather than passed over in silence. A loop, where two groups contain each other, ends that branch instead of the run: directories accept such loops, and one of them must not stop everyone else from synchronising. The number of nested groups a run walked through goes into the audit log, so a sync that suddenly brings in three times as many people as expected can be explained without opening the directory.
Three fixes in the same area, and the first two are the reason this section is not just a feature note.
A nested group was being imported as if it were a person. In an Active Directory domain a group carries a login-style attribute, so a group contained in a synchronised group was created as an ordinary user account named after the group — active, listed among your users, usable in policy — while the people inside it were never imported at all. It could not sign in, a group having no password, but it counted as a user everywhere else, and with automatic certificate issuing enabled it would have been issued a certificate. Objects that are not people — groups, OUs, containers — are now refused, and the log says which was which. Accounts created this way before the update are left alone rather than deleted by an upgrade; remove them from the user list when you have looked at them.
An account the directory does not list can no longer stay in an imported
group. The membership of an imported group belongs to the directory — editing
it by hand is refused — but synchronisation only ever removed members that had
themselves been imported from that same directory. Anything else stayed for good,
removable by neither the sync nor the interface. That is how a local account
became a permanent member of an imported group: an earlier version matched
directory entries to local accounts by name, and directories ship their own
admin. Membership is now simply what the directory says, and each removal is
recorded in the audit log. This removes a membership, never an account.
Picking groups by hand now works on OpenLDAP, FreeIPA and anything else.
Choosing specific groups to synchronise imported nothing at all on those
directories — no groups, no users, no error, a green run with zeroes everywhere —
and the member list on the group-picking screen came back blank, reporting “LDAP
group not found” for a group plainly sitting in the tree. The cause was a search
term only Active Directory understands (objectClass=group) used to look up
groups everywhere; LDAP does not treat “matched nothing” as an error, so nothing
was ever reported. Searching by name missed accounts there too, whose login
attribute is uid rather than sAMAccountName. Each of these now uses the
vocabulary of the directory type the configuration is set to.
And one that locked a whole platform out: accounts imported from Google Workspace can sign in again. A directory could import every one of its accounts and then refuse all of them at the sign-in screen, reporting the password as wrong when it was right. The password check was in fact succeeding — immediately after it the LDAP library asked the directory to describe itself, Google Secure LDAP permits that question from a privileged account only, and the refusal arrived on the same connection and was read as a failed login. That question is no longer asked on a connection opened to verify somebody’s password; it was never needed there. 802.1X had the same fault, and worse, because a refusal that is not a verdict on a password still counted toward the domain guards.
Things that reported success
A custom 802.1X server certificate never reached FreeRADIUS. Uploading or issuing one wrote it to a directory the RADIUS service could not read — and every layer above reported success. The file was written. The record was stamped deployed. The page said “Deployed”. Supplicants kept being shown the built-in certificate. Nothing failed, which is exactly why it went unnoticed: this had never worked, in any release.
The certificate is now delivered to the service and applied on the spot with nothing to restart by hand, and the page reports what the running service is presenting rather than what was written to disk. Existing installations repair themselves: a certificate already marked deployed is re-delivered at start-up. Other 802.1X material — trusted CAs, revocation lists — was never affected.
An update that could not fetch the bundle updated half the product. With the
bundle undownloadable — no internet, a proxy, a blocked network — ./taranac update offered to continue with the container images alone, leaving
docker-compose.yml, the taranac wrapper, the installer and the documentation
at their old versions. A release is not only its images: a fix can live just as
much in docker-compose.yml, and new images may depend on it. This very
release is such a case. So the outcome was the worst kind available — an update
that reports success while the part that mattered never arrived, with nothing on
the operator’s side to reveal it.
It now stops with nothing changed, including the pinned version, and says what to
do: restore access to the release server, or carry the bundle over by hand and
apply it with ./taranac update --from taranac-bundle-<version>.tar.gz.
An LDAPS trust anchor configured before 1.2.2 was silently dropped. 1.2.2
changed how an LDAP CA certificate is stored — from a path to a file inside the
container to the certificate itself — and did not carry the old value across. On
an upgraded installation the setting was simply empty, and every LDAPS connection
was then verified against the container’s built-in list of public certificate
authorities. Against a private CA — the normal case for Active Directory — that
stops LDAP authentication outright with certificate verify failed, and it takes
everything with it: TACACS+ and RADIUS logins, the web sign-in, user and computer
synchronisation. Behind a publicly-trusted directory certificate it is worse,
because verification keeps succeeding, against an authority nobody chose.
Updating repairs it: at start-up the old file is read and its certificate stored in the configuration. If the file cannot be read — an unmounted volume, a path that no longer exists — nothing is discarded, the attempt repeats on the next start, and the LDAP page now names the file it could not open instead of showing an unexplained empty field.
The general case of that is closed too: a directory with an empty CA field now says so before the first failed login, on the form, on Test connection, and in the service logs. An encrypted connection validated against the public CA store is legitimate for a directory behind a public certificate and identical in appearance to a configuration that has lost its own — so it is named rather than left to a handshake error days later.
While in there: TACACS+ picks up an edited LDAP configuration without a restart. It read the LDAP settings once at start-up and kept that copy for as long as it ran, so a correction made in the interface — most painfully a CA certificate — looked saved while authentication kept failing against the old values. It now re-reads them on the same schedule the RADIUS service already used.
A switch that denied every command
On some platforms — Edgecore ECS4100 is the reported one — signing in worked and landed at the expected privilege level, and then every single command was refused.
The cause is on the wire. The standard requires each authorization attribute to
carry a = or * separator; these devices split the trailing cmd-arg=<cr> in
two and send a bare <cr> as an argument of its own. The TACACS+ service treated
the request as invalid and discarded it before any policy rule was considered,
so nothing matched and the authorization log showed a refusal with an empty
profile — pointing squarely at the policy, which was never the problem. Sign-in
was unaffected because it carries no command arguments, which is what made the
symptom so hard to read.
Such an attribute is now ignored and the rest of the request is authorized normally. A Vendor Compatibility setting on the TACACS+ settings page controls it and is on by default; turn it off only if you want the protocol enforced strictly.
One quiet security fix
The 802.1X private key was world-readable inside the RADIUS container. A blanket permission sweep over the configuration directory reached the EAP private key and left it readable by every process in that container. It affects every installation to date.
Updating fixes it: the retired certificate volume is detached and the key is
written with restrictive permissions from then on. The old nac_certs volume is
left behind untouched — nothing uses it any more, and it can be removed once the
update is confirmed working (docker volume rm taranac_nac_certs, prefixed by
your compose project name). The key inside it is the built-in one rather than
anything you uploaded, and it is replaced on this node by the update.
Screens that showed the wrong thing
A live session no longer shows another session’s disconnect. Opening an active session could show an “Accounting stop” dated weeks earlier — sometimes two of them, one belonging to a different device on a different switch — while the session was in fact still running. Network equipment hands out accounting identifiers from a short counter and reuses them, and the timeline matched on that identifier alone, so it collected the disconnects of every earlier session that had held the same number. Matching is now confined to the same equipment and to the session’s own lifetime.
Termination reasons are reported by name, too. Equipment that sends a cause the
RADIUS dictionary does not cover had it stored as a digit — which is why a large
share of closed sessions gave their reason for ending as 0. Sessions already
recorded keep the values they were stored with.
Editing a policy rule shows the profile you chose, not the previous one. Changing a rule’s profile saved correctly, but the response the form reads back still named the old profile, so the interface went on displaying a profile the rule no longer used until you reloaded the page. Group assignments behaved the same way — replacing a rule’s groups came back with the previous membership, and adding the first group came back empty. Both device-administration and 802.1X rules were affected.
Two regressions from 1.2.3, in high availability
Both arrived in 1.2.3 and are named here because 1.2.3 is the release that made clusters easy to build.
Adding a node without the setup wizard works again. A join that did not go
through the wizard’s broker — the manual flow with a join token, and --primary —
stopped on its first step with an interpreter error instead of doing anything, so
the only route into a cluster was the wizard. Behind it, on that same route, the
node registered its cluster membership under an empty name: the option that names
it has always been documented as defaulting to the node’s own name, and that
default had stopped applying outside the wizard. Clusters built with the wizard
were never affected, and nothing about that path changes.
Turning a node back into a standalone install finishes cleanly. The last step
— reducing the leaving node’s copy of the cluster roster to itself — died on an
interpreter error, so detach reported failure after it had in fact succeeded,
and the node went on listing its former peers under Cluster status as though
it were still a member. The node, its data and the cluster it left were never at
risk; it spent six steps earning a clean exit and then misreported the seventh.
Upgrade notes
- The LDAP CA change at the top of this post is the one thing to do before you pull. Everything else in this release is safe to apply and then look at.
- User accounts created from nested groups are not deleted by the update. They are left in place deliberately — an upgrade that removes accounts is not an upgrade anyone wants surprising them. Find them in the user list and remove them once you have confirmed what they are.
- Vendor Compatibility is on by default on the TACACS+ settings page. If your policy is that the protocol should be enforced strictly, that is the switch.
taranac_nac_certscan be dropped after the update is confirmed working.- Follow nested groups is on by default, including for existing configurations — a synchronisation may legitimately bring in more people than the last one did. The count is in the audit log.
1.2.4 ran 4457 passing, 0 failing on the dedicated runner — unit through
sustained-rate load under HA failover — at 82% backend line coverage, dated
2026-08-12 and commit-pinned. The breakdown ships in the bundle as TESTING.md,
including the eight end-to-end cases tracked openly as expected-fail.
There is no headline in this release, and that is the point. If you run 802.1X with your own server certificate, a directory that is not Active Directory, RADIUS profiles carrying vendor attributes, or a cluster you built by hand — one of the things above has been quietly not working for you, and now is not.
Existing installations update in place with ./taranac update — read the LDAP CA
note at the top first, and see backup & upgrades
for the full procedure. New ones start from the appliance. Where the
platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.3 — a module in one command, a cluster in three
The last two releases were about what Taranac can authenticate. This one is about what it costs you to deploy — the part that never shows up in a feature list and is the reason half of a product’s capabilities go unused.
Two things in Taranac were runbooks rather than features. Attaching a remote
collector or a DMZ captive portal meant copying an archive, editing a compose
file, carrying a CA certificate and hoping the address you typed was the one that
worked. Building an HA cluster meant hand-editing .env on every machine, copying
three TLS files to each one over scp, and moving the master key by hand.
Both are commands now. The collector and the portal attach with one. A cluster is built with three.
And because a remote site is usually where the hardware is a hypervisor and the registry is unreachable, this release was cut around a third thing: one appliance image that can be any of them.
One command, and the module is attached
The core prints the command; the site runs it. Issue an enrollment token on the
core and the dialog hands you a complete, ready-to-paste line with the address
already filled in — the address you are reaching the core at right now, derived
from your own request, so it is demonstrably one that works rather than a
placeholder or a PUBLIC_BASE_URL that drifted months ago.
./collector-join.sh --core https://taranac.example.net/api/v1 --token …./portal-join.sh --core https://taranac.example.net/api/v1 --token …That address sits in its own editable field in the dialog, because a site behind NAT reaches the core by a different name than the browser you issued the token from. Edit it before you copy.
What used to be around those two values is gone:
- No CA file to carry. Trust defaults to pin: the certificate presented on first contact is the only one accepted afterwards — the same bargain the product already makes with a device’s SSH host key. Verifying against a CA bundle instead is still there for installations whose policy demands it, named up front rather than asked of everyone.
- No archive to copy. The module downloads its own deployment bundle, over the same channel it just authenticated on.
- No compose file to edit. The script writes the config beside itself, starts the container, and — this is the part that matters — waits until the module has actually attached before reporting success. If it did not, it prints the daemon’s own reason and exits non-zero, instead of leaving you with a container quietly restart-looping.
And it can be asked what it is doing, and removed
A thing you can install and not uninstall is a thing you install once, carefully, and then never touch. Both modules answer for themselves now:
sudo taranac-module collector --status # where it points, what it last did,sudo taranac-module portal --status # and whether the core is answeringsudo taranac-module collector --uninstall # cleanly — containers and volumesTwo related changes came with it:
- Replacing the core’s certificate is a handled event. A module refuses an
unexpected certificate and prints both fingerprints; re-pinning is a deliberate
one-shot (
--reset-trust), because a pin that re-learns itself on every restart protects nothing. - The core tells modules which version it is. Reporting used to run one way — a module announced itself and could not learn what it was supposed to be running. A module must match its own core rather than the newest release, so this is what makes updating modules possible at all.
- Every collector and portal names the cluster node it is bound to. Under HA there is no client-facing address shared by the nodes, so each remote party talks to exactly one — and until now nothing recorded which, leaving an operator whose collector went quiet unable to see whose outage explained it.
One image, four appliances
Until this release the OVA was one thing: a Taranac server. Which is fine until the machine you need is a collector in a branch office, a captive portal in a DMZ, or the third voting member of a two-node cluster — and every one of those is a place where downloading a second image and a compose file is the awkward part of the job.
So the appliance became universal. The first-boot wizard now asks what the box is:
- Taranac — the complete system, installed now, from this disk
- Module — a Collector, a Captive Portal or an HA Witness, set up over SSH
Neither is marked “recommended”, deliberately. Which one is right is a fact about
the box in the rack, not a preference we can hold: someone deploying a
branch-office collector is not making the lesser choice, and nudging them toward
the full stack only makes them doubt an answer they already had. Changed your
mind after answering? sudo taranac-setup runs the wizard again.
The module path hands over to SSH on purpose, and asks nothing on the console. Everything a module is configured with is material that is miserable to type on a hypervisor console and trivial to paste: a 32-character enrollment token, a portal API token, a TLS key. So the screen gives you the address and gets out of the way:
ssh taranac@<the address the wizard just configured>sudo taranac-module # collector | portal | witnessAnd all of it installs with nothing to reach. Fourteen container images are already on the disk — the twelve Taranac ones plus PostgreSQL and etcd — and the first boot runs the installer with pulls disabled. That includes images the full stack never starts: the collector image rides along for the Module Installer, because a remote site is precisely where there is no registry. The HA overlay images travel too, so converting a running appliance into a cluster node works offline as well.
This is why the download got bigger — 5.09 GB against 1.1.1’s 3.64 — and it is the honest trade: the image now carries every role it can be asked to play, and the first boot never asks the network for permission.
One small thing, since an appliance is still a machine somebody fixes at three in
the morning: it ships a real vim. The base system leaves vi pointing at
vim.tiny, which runs vi-compatible — no -- INSERT --, no syntax, and A/B/C/D
typed into your file when you press the arrow keys. An editor that misreports its
own mode is not what you hand someone editing a config under pressure.
A cluster, in three commands
./taranac cluster init # on the future primary./taranac cluster add-witness --address <addr> # the etcd arbiter, first./taranac cluster add-node --address <addr> # once per nodeThat is the whole sequence, and the order is enforced by the mechanism rather than by a document. Each command says what it is about to do, checks that it actually happened, and names the step when it did not. The node being added runs a single command the primary prints for it.
Underneath, three things that used to be manual are now part of the protocol:
- The master key and the etcd CA travel encrypted over the join channel. The join token authenticates and encrypts under two separately derived keys, so the token itself never crosses the wire and the material it protects is never in plaintext at any layer. The joining machine pins the channel by a fingerprint printed on the primary’s console — not trust-on-first-use.
- A joining node keeps its private key. It generates its own and sends only a
certificate request; the answer is a signed certificate. That is what removed the
scpof TLS material, for database nodes and for the witness alike. - Adding a node consults every existing member first. A member that has not reported recently stops the add rather than being assumed healthy, and a member that does not yet know a node which has already joined is named along with the command that fixes it. Nothing is rewritten on a live member during someone else’s join.
And the cluster is operable, not just buildable:
cluster connectivityproves the cluster by logging in. Every node attempts a real replication login to every other and publishes the result — a port that answers is not proof, because a refusal arrives only after the handshake.cluster switchoverhands leadership over on request, and states the cost concretely when forced: how many bytes of WAL the target is behind, and that those writes will be lost.cluster syncbrings a node that has fallen behind up to date with what the cluster reports about itself. It is run on that node, deliberately.cluster remove-nodeevicts a node with one command on the primary; the surviving members need nothing, because membership is read from etcd.detachturns a removed machine into an independent install — a separate, explicitly confirmed step, because it keeps a full copy of the data, which is a fork rather than a decommission. It says so before it acts.
Two hardening changes worth naming here:
- The etcd CA is now on every node, not only on the one that created it. It previously existed on a single disk and in no backup — so losing that machine left a cluster that could never add or replace a node again. A single point of failure inside the feature that exists to survive one. It is in the backup archive now.
- A command run in the wrong terminal could destroy a healthy node. A join token is a bearer credential, and with several machines open it is easy to paste the wrong command. A node now refuses a token issued for a different machine, and nothing irreversible happens until every remote step has succeeded.
Fixes that change whether things worked at all
Every backup failed, on every installation. The installer writes .env with
the installing operator’s permissions while the api container runs as a different
user, and the file is mounted read-only — so nothing inside the container could
widen it, and every scheduled and manual backup died whole on that one unreadable
file. Permissions are now set on the host where they can be, and a file that still
cannot be read degrades the archive — reported as missing, with an alert —
instead of aborting it.
Patroni’s REST API required no credentials. Anyone able to reach port 8008 could hand leadership to another node or restart the database. Authentication is now unconditional: a node refuses to start rather than come up open.
Taranac Push MFA works over MS-CHAPv2. A push challenge on that transport could not complete, so an account with push required could not log in on devices that offer nothing else — which, after 1.2.2, is exactly the set of devices that gained MS-CHAPv2.
./taranac rotate-key never worked from a released bundle. The script was not
packaged, and the wrapper’s advice — refresh the bundle, then retry — fetched the
same bundle again. The packager now refuses to build unless every file in the
bundle directory is accounted for.
On the appliance specifically:
- The wizard’s final screen survives. It carries the URL, the administrator name and the generated password, and it was being wiped twice over: the unit reset the console the moment the wizard exited, and a keystroke buffered during the minutes-long install dismissed the dialog before it could be read.
- The operating system’s own auto-upgrade no longer kills first boot. An hour after boot the daily upgrade restarted everything whose libraries it had replaced — including the container runtime and the wizard — leaving the operator at a bare login prompt. Updates still run; they no longer restart what is not theirs.
- The domain sidecar no longer breaks its own DNS. A default it applied to itself could leave it unable to resolve the domain it was meant to join.
- Commands on the witness say why they do not apply. A witness runs etcd only, so none of the cluster commands work there; it now says that, and lists what can be done on that host.
Upgrade notes
- The installer no longer asks about certificate addresses. It was a question about TLS put to someone who had not yet seen the product, and nothing depended on the answer: the certificate generated at install time is self-signed, and the two things that verify it — a collector and a portal — pin what they are shown rather than matching a name. Widening it is a post-install setting, where hardening belongs. On the appliance the prompt was worse than redundant: the wizard passes its answers through the environment, an unanswered one arrives empty, and the installer sat waiting on a hidden prompt behind the wizard’s own dialog.
- A node can be reached by name and by address at once. Both go into the
certificate and into the front end’s server names — and an address is written as
an
IP:entry rather than aDNS:one, a distinction browsers merely warn about while a collector or portal refuses the connection outright. - Join tokens report expiry. An issued-but-unused token used to read as outstanding forever; it now reports as expired and is absent from the list of live ones. A successful join retires the other invitations to the same address.
- The certificate-enrollment gateway is pinned to nginx 1.30.4 like the other three listeners, instead of floating onto whatever was newest at build time.
1.2.3 ran 4316 passing, 0 failing on the dedicated runner — unit through
sustained-rate load under HA failover — at 81% backend line coverage, dated
2026-08-10 and commit-pinned. The breakdown ships in the bundle as TESTING.md,
including the eight end-to-end cases tracked openly as expected-fail.
If a remote site, a DMZ portal or a second node has been on your list and the deployment cost was the reason it stayed there — this is the release that removes the reason. It is one image now, whichever of those you are building: download the appliance. Where the platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.2 — MS-CHAPv2, and a directory that isn't AD
1.2.1 was a pass over what already existed. 1.2.2 is the opposite: it closes the biggest hole in the product’s identity story, and a few more besides.
The hole was MS-CHAPv2. Plenty of gear will not authenticate an administrator any other way, and plenty of 802.1X supplicants will not put anything else inside the tunnel. Taranac could not do it. That is fixed — for local accounts, for Active Directory accounts, over RADIUS device administration and over 802.1X alike.
Underneath it sits a second change we did not plan on shipping in the same release: the LDAP integration stopped assuming every directory is Active Directory. And alongside both, certificate enrollment works end to end for the first time.
There is also a security section in this release, and it is not decorative. If you are running 1.2.1 or earlier, this is an upgrade worth scheduling.
MikroTik administrators can log in now
A MikroTik RouterOS box sends only MS-CHAPv2 for an administrator login. No PAP, no TACACS+ client at all. Which meant that until this release a RouterOS device could not authenticate against Taranac in any configuration — and that the RouterOS RADIUS template we shipped with a certain amount of fanfare in 1.2.1 was unreachable.
Device-administration RADIUS now speaks MS-CHAPv2. Local accounts are verified inside Taranac. An Active Directory account is verified by a domain controller, because its password hash is unreadable by design — that is what the domain-membership machinery below is for.
There is nothing to switch on. The branch is chosen by what the device actually sends, and a device that sends PAP never enters it.
Which is also why the RADIUS “authentication type” dropdown is gone. It offered PAP / CHAP / MS-CHAPv2 and nothing read the value — the handler authenticated with PAP regardless of what you picked. A control that advertises a capability the product does not have is worse than no control at all.
One RouterOS-specific fix that will show up in your numbers: a successful
MikroTik login no longer writes a phantom failure. RouterOS probes the SSH
“none” method by proving the empty password before it prompts the human, so every
successful administrator login produced a counted bad_password — halving the
brute-force threshold for exactly the devices that need MS-CHAPv2, and topping the
“top failed authentications” widget with the customer’s own admins.
And the same identity over 802.1X
A directory account now authenticates over PEAP/MSCHAPv2 and over EAP-TTLS/MSCHAPv2. Same mechanism, same domain controller, no separate switch.
While we were in there, native EAP-TTLS/MSCHAPv2 — what a supplicant sends when there is no EAP inside the tunnel — was broken for everyone, including a local account typing the correct password. All three EAP-TTLS inner methods work now.
Two things that used to send you looking in the wrong place:
- A rejected server certificate is named as such. An endpoint whose TLS
handshake dies — almost always because it does not trust the RADIUS server
certificate — was logged as PEAP with
auth_failed, which sends an operator after credentials for a certificate problem. The EAP method now comes from what the daemon reports, and an account that cannot use PEAP is told to use EAP-TTLS instead of being reported as a bad password. A missingntlm_authhelper is likewise named rather than surfacing as a wrong password. - A re-authentication no longer loses the session’s identity. An 802.1X
re-auth closes one session and opens the next milliseconds apart on the same
port, which is what made an active session read
LAB\anonymous.
And a correctness fix that is really a security fix: a domain-qualified name is
no longer an alias for a local account. Over 802.1X, ANYTHING\alice and
alice@whatever.invalid all authenticated as the local account alice, and the
log named the bare account either way. The name is now tried exactly as sent, and
a qualified name may only ever resolve to a directory account.
The node joins the domain by itself
Verifying a domain password means being a domain member, and domain membership is traditionally a ticket to somebody else’s team: create a computer object, delegate rights, wait.
Add an LDAP configuration with the MS-CHAPv2 settings and the installation becomes a domain member using the account it already binds with. A domain’s default machine-account quota is what makes this possible. The machine account name is derived so a node re-joins its own object rather than making a new one each time, and checked against the directory so two installations in one domain cannot overwrite each other. It happens once and never repeats.
Around it:
- An MS-CHAPv2 tab on the LDAP configuration — membership per node, the
feature switch, and a join dialog that asks the directory about the credentials
and the target OU before spending them. The two commonest failures — wrong
password, mis-ordered OU path — are named in under a second instead of coming
back as
net ads join’s own diagnostics. At most one LDAP configuration may own the domain, and the refusal names the one that already does. - A twelfth service,
winbind, ships with every installation. It owns the node’s machine account. With no domain configured it starts, reports “not joined” and does nothing — a service you must first install is a service nobody enables, and MS-CHAPv2 is needed precisely by the administrator who discovers the need at the moment their own login fails. - An alert when the domain trust breaks — narrowly: MS-CHAPv2 is on and this node can no longer verify a domain password. A member with the feature deliberately off never pages anyone, and “never joined” is reported as an incomplete setup, not a broken trust.
- Failures are diagnosed, not guessed. Twenty-four
NT_STATUScodes and nine join failures map to a cause and a next action — expired account, logon-hours policy, a dead controller, NTLM disabled by policy, clock skew, a broken machine trust — and each carries whether it counts as a credential attempt, so a rebooting domain controller no longer looks like password guessing.
A directory no longer has to be Active Directory
This one was not on the roadmap for 1.2.2; it fell out of the work above.
The LDAP integration was built for Active Directory and said so only in its code:
bind by UPN, look accounts up by sAMAccountName, track them by objectGUID,
filter on userAccountControl. Every one of those is false for OpenLDAP,
FreeIPA and Google Workspace Secure LDAP — which is why connecting one of
them could not be made to work by configuration alone, no matter how patiently you
edited the fields.
A directory now has a type: Active Directory, OpenLDAP, FreeIPA, Google Workspace, or generic. The type supplies two different things — what the protocol can do there (MS-CHAPv2 and computer sync are Active Directory only, and the UI says so rather than letting you configure something that cannot work), and the schema guesses you are free to overwrite: search filters, attribute maps, the example under each field.
The type is chosen once, when the configuration is created, and cannot be changed afterwards. Each directory imports accounts by its own login attribute and tracks them by its own identifier, so switching would mean converting users already synchronised — better refused than half-done.
Existing installations are untouched. A configuration with no type reads as Active Directory, and the Active Directory defaults are the previous literals character for character. The documentation covers each type, what it can and cannot do, and how OpenLDAP and FreeIPA connections are set up: LDAP directories.
Three long-standing LDAP annoyances went with it:
- An unreachable directory fails instead of hanging. The client read a boolean server pool as “retry forever”, sleeping between rounds — so a directory that did not answer never produced an error; the request simply hung, holding the worker that made it. Including Test connection, which is exactly where you go to find out what is wrong, and inside the RADIUS and TACACS+ daemons, where that loop sat in the thread serving the request.
- A wrong CA file is named instead of returning HTTP 500 on every operation.
- Computer synchronisation says which setting is missing rather than reporting “connection failure” and sending you to check the network, the controller and the certificates while the real cause sat in a container log.
Certificate enrollment works end to end
EST was in the product and unreachable by any supported route: the frontend
proxied only /api/, so /.well-known/est/ handed an enrolling client the web
UI’s HTML, and the only working path was the API’s bare port with an
internal-only certificate.
EST and the revocation lists are now published through the front door, on the installation’s own certificate, and the CRL distribution point fills itself in.
Approval is a queue now, not a wall. Turning on “require approval” used to
stop issuance rather than defer it: the request was stored as a certificate with
serial 0 and placeholder dates, no approval screen existed anywhere, and every
client poll appended another row. Requests are their own records, deduplicated by
the request fingerprint across polls, with a queue in the UI and an approval group
that says who gets notified. Deciding is still nac_pki.edit.
You can approve from the email. Members of the approval group get the request by mail with what is being issued and to whom. The link never acts on being opened — mail scanners, Safe Links and messenger previews follow links by themselves, so the GET only renders a page and the decision needs a deliberate confirmation.
A revoked certificate can no longer renew itself. A renewal is checked against the revocation list before it is granted.
One honest limit: renewal authenticated by the client’s own certificate needs a listener that verifies client certificates, and this release does not ship one. Until it does, a renewal authenticates the same way an enrollment does. The mTLS gateway is additive and lands in 1.2.3.
802.1X sessions stop lying about themselves
Filed as bug fixes; if you run 802.1X in production they are the reason to upgrade.
- A CoA “session not found” no longer kills a live session. A switch’s 503 answers “did I find a session matching your identifiers”, not “is anybody on the network” — it usually means the port re-authenticated in place. Taranac used to archive the session and report the disconnect as successful, with no way to bring it back.
- The orphaned-session probe no longer disconnects healthy sessions. It asked “are you still there?” with a real Disconnect-Request, so on hardware that sends no periodic interim accounting — the Cisco default — every healthy session eventually crossed the threshold and was torn down for being healthy. CoA also goes to the port configured for the device instead of a fixed one.
Reauthentication timerreaches the hardware. The value was carried everywhere — model, UI, reports, daemon cache — and serialized nowhere. You typed 3600, saw the “seconds” hint, and got nothing. It is rendered as Session-Timeout plus Termination-Action, per the vendor’s own mapping.- A session the NAS keeps reporting is no longer archived behind your back.
Security
Ten findings, several from adversarial review of our own diffs and several found on live hardware. Named plainly, because the alternative is you finding out some other way.
- Certificate enrollment could be reached with no credential at all. A request carrying two invented headers and nothing else got HTTP 200 and a signed certificate: the endpoint trusted client-identity headers that no proxy in the deployment ever set or stripped, so anyone who could reach it controlled them. Reproduced on a stand; now a 401.
- The token issued after the password but before the second factor was accepted as a session. Same key, same shape as a full access token, and nothing checked what it was for — so a stolen password alone was enough to enroll an attacker’s own second factor over the victim’s and then log in normally. Every token now declares its purpose and every endpoint accepts only the purposes it names, deny by default.
- The Telegram MFA webhook accepted unauthenticated requests. The secret check
began with “if a secret was sent”, so a request without the header passed it
entirely — and a forged one could make the bot disclose which Taranac accounts
are linked to a given Telegram account, into a chat of the caller’s choosing.
/mfa/verifyis now attempt-limited too. - Fetching a CRL was an internal port scanner. Only the URL prefix was checked: no loopback or private-range block, no re-check after a redirect, no size limit, TLS verification disabled outright, and the exception text handed back verbatim — on a schedule. It now validates the target, connects to the address it validated, verifies the certificate, caps the download, and reports failures without describing what it found.
- A TACACS+ rule whose device restriction resolved to nothing applied to every device. An emptied device group, or a device sitting on an FQDN object, turned a narrow rule into a fleet-wide one — and the generated ruleset is the only place that restriction is enforced. The same rule already failed closed in RADIUS, which means one rule had opposite meanings in the two protocols.
- A group’s RBAC model could be assigned by anyone who could edit groups, and the model behind a privileged group was visible to anyone who could view them — so an operator could grant their own group a privileged model and log in again. Assigning a model, and membership of administrative groups, now require administrator rights.
- Removing someone else’s second factor required nothing but the record’s UUID.
It requires
mfa.editnow. - A value containing a newline could append arbitrary directives to a generated daemon configuration. Escaping covered only quotes and backslashes, in all three generators. A mangled value breaks one device; a raw newline breaks the configuration of the whole fleet.
- A network object of type FQDN can no longer be attached to a device. Such an object carries no network, so the device silently dropped out of the RADIUS clients list — it was not authenticated at all — and its TACACS+ rules lost their device restriction entirely.
- Every container that runs nginx is rebuilt on a current one — 1.30.4 — and it
no longer announces its version. The listeners ran 1.27.5 (edge, frontend) and
1.26.3 (captive portal) — branches that ended in 2025 on tags frozen ever since,
so rebuilding those images never brought a single fix, however often we rebuilt
them. Nineteen and eighteen published advisories apply. None is reachable through
the shipped configuration, which uses only proxy, TLS, gzip and
return— but that list grows by itself, and an operator whose scanner flags it should not have to take our word for why it does not matter. All four listeners now suppress the version in theServerheader and on error pages; only the captive portal did before. Thanks to Andrew S, who raised it — this is exactly the kind of report that is easy to shrug off from the inside and obvious from the outside.
Keys and backups
The “Rotate Master Key” button destroyed installations. It re-encrypted the
system settings and nothing else — device keys, enable passwords, the internal
CA’s private keys, NCM credentials, TOTP secrets and the license stayed on the old
key while the new one was swapped in memory — and it answered 200. The endpoint
now refuses.
Rotation is a tool: ./taranac rotate-key, on a stopped system, in a single
transaction. You present the current key, name the new one, a backup is taken,
every encrypted column is re-encrypted, the system starts again. It can be
rehearsed on a dev stack, because a tool with nowhere to practise is a tool nobody
has practised.
A backup can now actually be restored. The archive carries .env, the compose
file and config/ — the database’s secrets are ciphertext whose key lives outside
it — and restoring asks for the five keys that matter. pg_dump gained a timeout,
so a hung dump no longer occupies the single scheduler slot forever while the
dashboard shows the last good archive. And an incomplete archive is no longer
reported as successful: the host-files part was never collected on any
deployment, and the only trace was a manifest field nobody reads — discovered at
restore time, the worst possible moment. It is loud now: audit entry, error log,
and a backup:incomplete alert that clears on the next complete archive.
Smaller things
- The username filter matches a substring, case-insensitively. Filtering the
NAC Auth Log for
ad_user1found nothing when the account had authenticated asad_user1@example.comorEXAMPLE\ad_user1. Applied to NAC Auth Log, NAC Sessions and the AAA logs, with trigram indexes on the seven journal tables so it stays fast on a large archive — 53 ms → 1.5 ms on a 60k-row partition. - A VLAN filter on authorization profiles. “Which profiles drop an endpoint into VLAN 40” no longer means paging through the list.
- The UI downloads only the languages it will show. All seven bundled languages — 3.58 MB of translations — were in the entry chunk, so an English-only installation fetched about 3.2 MB it would never display.
- Tables show an error instead of “no records”. On a failed request the log pages rendered their empty state, so an operator mid-incident read an empty authentication journal as “there were no authentications” — the worst kind of failure, silent and pointing the wrong way.
- Escape closes only the topmost drawer. Pressing it in a third-level form used to close all three and lose what you had typed.
- Syslog forwarding has a ceiling and reports what it drops. The forward window was unbounded, so after any downtime the whole missed range loaded at once and went out one message per record — on a large installation that is not a slowdown but an outage, at the moment the system comes back.
- Failed RADIUS MFA challenges say what actually arrived, so an expired code and a value that was never a code are no longer the same log line.
- The dashboard stops polling while its tab is hidden.
- Group matching on FortiGate. The seeded TACACS+ template named the AV-pair
member_of; a FortiGate asks formemberof, so it was silently dropped and group matching could never succeed. Migration 0019 renames it in existing templates and profiles too.
Upgrade notes
Three things worth reading before you pull:
- The RBAC list lost its Type column and System/Custom filter. No role in a
running system is ever a system role, so the column read “Custom” on every row
and one filter option returned everything while the other returned nothing. If
you built a custom report dataset on the
config_rbac_rolestemplate and picked theis_systemcolumn, it fails validation on its next run and needs its columns re-picked. No seeded dataset uses it. - The Security menu section is gone. Authentication & Sessions moved to System; the Crypto page went with it, since it existed for the one button described above.
- EAP-TEAP is gone from the log filter and the policy schema, having never been implemented — and EAP-TTLS, which is fully supported, is finally rendered and filterable in the log views and the session drawer.
1.2.2 ran 4149 passing, 0 failing on the dedicated runner — unit through
sustained-rate load under HA failover — at 81% backend line coverage, dated
2026-08-05 and commit-pinned. The breakdown ships in the bundle as TESTING.md,
including the eight cases tracked openly as expected-fail.
If you have MikroTik gear, or a directory that was never Active Directory, or 802.1X sessions that behaved strangely on re-authentication — this is your release. Where the platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.1 — more vendors, lighter pages
1.2.0 was a big release — a whole reporting domain — and the natural thing after a big release is to go back over what’s already there. That’s what 1.2.1 is. Three threads run through it: more gear speaks Taranac natively, the Configuration Tracker stops being surprised by very large configurations, and the UI got a lot lighter.
Seven more vendors for device administration
If your fleet is Cisco and Juniper, Taranac has always had you covered. Everyone else was doing more work than they should have.
RADIUS admin-auth templates go from 8 to 15 — MikroTik RouterOS, Arista EOS,
Aruba CX, Nokia SR OS, H3C/Comware, Ruckus/Brocade ICX and Extreme EXOS.
TACACS+ goes from 12 to 16, adding H3C/Comware 7 (roles AV-pair, RBAC), Nokia
SR OS, Ruckus/ICX and Extreme EXOS. Each one carries the right attribute for its
platform — Mikrotik-Group, Arista-AVPair, Aruba-Admin-Role, the Timetra
trio, H3C-Exec-Privilege, Foundry-Privilege-Level — including the ones with an
inverted privilege scale, where 0 is the most privileged and getting it
backwards means handing out full access by accident.
On the NAC side, the vendor dictionary gains Extreme EXOS and Ruckus ICX as 802.1X authenticators, both with CoA port bounce wired up. And the long-mislabelled “Aruba” entry is finally split: Aruba AOS-CX / Controllers and a distinct HP ProCurve, which is a different vendor with a different PEN and was never the same thing.
One upgrade detail worth stating plainly: new built-in vendors now reach an existing install. Seeding used to be all-or-nothing — if the dictionary had ever been seeded, later additions never arrived. It’s per-entry now, so you get the new vendors on boot and nothing you edited gets overwritten.
FortiGate command accounting that shows the command
A FortiGate doesn’t report the command it ran in the command field. It reports a
category — Config, Show, Diagnose, Execute — and puts the actual command
elsewhere. Which meant command accounting from a FortiGate technically worked and
told you nothing: page after page of Config.
Records from a FortiGate now carry the real command, in the log views and in the syslog export. If you have FortiGates and you’d written off their accounting, have another look.
Configurations too big to store are now tracked, not failed
The Configuration Tracker had a hard ceiling. Past it, a capture simply failed — and a device whose config is too large is exactly the device you most want to track.
A text configuration larger than a configurable threshold is now recorded as a version — the change is still detected, still dated, still in the timeline — without storing the body. The hash and size describe the full scrubbed text, so an oversized snapshot deduplicates exactly like a normal one: no phantom versions, no spurious “changed” when nothing did. The configuration page and the version timeline say so plainly rather than showing an empty diff.
The three ceilings are now settings, not constants:
ncm.text_store_max_bytes— 2 MiB by default; past this, text is tracked by hash only.ncm.text_buffer_max_bytes— 64 MiB; the largest text a capture will buffer.ncm.binary_max_bytes— 2 GiB; the largest binary it will download (0= unlimited).
The text buffer ceiling used to be 8 MiB, which made a 20 MB text config fail while a 2 GB firmware image sailed through. The defaults are what the product guarantees; the settings exist so an install that legitimately needs more can go further, knowingly.
And a too-large capture is no longer retried. It used to be classified as a transient error, which was wrong twice over: the retry re-fetches the same oversized payload, so every sweep burned the full retry budget — three extra device logins per configuration, for nothing — and the operator was told a threshold problem was a network problem. It now fails immediately and names the setting to raise.
Large configurations open instantly
Viewing the full text of a configuration with tens of thousands of lines used to freeze the tab. The view is windowed above 2,000 lines now and opens immediately at any size. While we were in there: the diff had a word-wrap toggle and the full-text view didn’t — it does now, and the preference carries across both views.
The UI stopped downloading catalogues it never needed
This is the change most people will feel every day, and it’s boring in the best way: forms and detail pages no longer fetch whole catalogues just to display the name of something you already picked.
Users, groups, TACACS+ and RADIUS profiles, NCM credentials, sources, recipes and configurations, LDAP RBAC assignments, the report builder — all of them now read those names straight from the payload they already received, and search the server through a drawer when you actually need to choose something.
Opening an NCM configuration went from about 27 requests and 37 MB to about 5 requests and 2.5 MB.
There’s a second, quieter win in there. Those catalogue fetches capped out at 100 entries — so past a hundred users, or groups, or profiles, the picker silently hid the rest and you had no way to know. Server-side search doesn’t have a hundredth entry.
Smaller things
- Taranac Push MFA is configurable again. The provider card is enrollable once more, marked Soon while the mobile app rolls out.
- LDAP configurations show up under an RBAC model’s “Used By”. A model referenced by an LDAP configuration’s default role or a group override was invisible to the where-used view and could be deleted, leaving a dangling reference. It’s listed now, and deletion is blocked until the reference is gone — matching how groups already behaved.
- Daemon configuration is re-checked at startup. The reload skip-gate added in 1.2.0 watches for data changes, so after an upgrade that changed how a configuration is generated, the new configuration wasn’t deployed until some unrelated edit or a manual Reload. Each daemon now reconciles its on-disk configuration once per boot. If you’re upgrading from 1.2.0, this is the one that matters.
- “Sources” highlights in the NCM sidebar. It was the one NCM settings page
routed outside
/ncm/settings, so the section never expanded for it on a page refresh. Old bookmarks redirect. - The PKI certificates page no longer loads twice. Selecting a CA fired two identical requests for the certificate list.
Vendor coverage is the kind of thing that’s never finished — there’s always one more box in someone’s rack. If yours isn’t in the list, tell us which platform and which attribute it wants; that’s a small, well-shaped change and it tends to land fast. Where the platform goes next is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.2.0 — reports, without writing a single query
Every platform that holds operational data eventually gets asked for reports, and there are two easy ways to answer. Bundle a BI tool and let people build whatever they want — powerful, and now your users have to learn a second product to find out how many logins failed last week. Or ship a fixed list of PDFs — simple, and useless the moment someone needs the same thing for one device group instead of all of them.
We spent most of this release looking for the line between those two, and the answer turned out to be a layer in the middle: datasets.
Template → dataset → report
The model has three levels, and it’s worth two minutes because everything else follows from it.
A template is the lowest level, and it lives in code, not in the database. It’s a hardcoded query plus a declaration of what you’re allowed to change about it — which period windows are offered, which columns can be sorted, what the row limit is. Templates ship with the release and get reviewed like any other code.
A dataset is what you save: a template plus your filter values plus your display choices. One template, many datasets — “TACACS+ authentication” is a template; “TACACS+ authentication, last 7 days, core switches, as a bar chart” is a dataset you name and keep.
A report is a multi-page composition of datasets. One dataset per page, ordered however you like. Everything that goes into a report is a dataset — there’s no second path, which is what keeps the whole thing predictable.
That middle layer is the entire trick. You never write SQL — not a line, not ever — but you’re also not stuck with whatever we imagined. The query is ours; the question is yours.
Two kinds of template, and who owns the look
Templates come in two flavours, and the difference is who controls the presentation.
Presets are the curated ones. We author the layout in code — donut rings, KPI
strips, heatmaps, multi-series trends, hand-placed and pixel-checked. You pick the
period and, where a template offers more than one, the view. AAA overview gives
you the full dashboard or a bare row of totals. NAC access gives you the outcome
overview or the reject forensics. Presets export to PDF, because we own the layout
and it’s a designed page.
Tables are the open ones. The query is still fixed, but the display is yours: show it as a table, or map its columns onto a bar, line, pie or donut chart, pick which columns are visible, sort it, filter it. The template declares each column’s role — dimension or measure — so the chart picker only ever offers you mappings that make sense. Tables export to PDF, CSV and XLSX, because a flat result is exactly what belongs in a spreadsheet.
“Table” doesn’t mean “only a table”, by the way. The same result set is a table, a top-N bar or a pie depending on which columns you point at which channel.
43 templates in the box
Sixteen presets and twenty-seven tables ship in 1.2.0, across every surface the platform already writes to:
- AAA — activity trends, policy and command insights, per-device usage for TACACS+ and RADIUS (including the devices that have gone silent, which is the interesting half), and a cross-surface failure view that puts UI, AAA and NAC login failures side by side as rates rather than raw counts.
- NAC — port-access outcomes with a day-by-hour heatmap, reject forensics, live and historical sessions, and certificate posture bucketed by time to expiry.
- NCM — collection health, change activity across tracked configs, and just-in-time credential behaviour.
- System — configuration inventory, platform health, alert overviews, and the accounts that haven’t logged in for the longest.
- Raw tables — every log surface (TACACS+ authn/authz/acct, RADIUS auth/acct, the audit trail) and every configuration table, as flat exports.
What comes out, and why the preview is honest
Reports are assembled on the server. One HTML document is generated, and that same document feeds both the on-screen preview and the PDF — they’re identical because they’re literally the same bytes. Charts are server-rendered inline SVG, so nothing depends on what your browser felt like doing. PDF rendering runs in a small dedicated service that ships alongside the rest of the stack.
The practical effect: what you see in the preview is what lands in the mailbox. No “looked fine on screen, broke in the export”.
Scheduled delivery, with live windows
Any report or dataset can be put on a schedule and mailed out periodically.
The part worth calling out: the schedule doesn’t freeze the period. A dataset scoped to “last 7 days” stays relative — every delivery resolves that window at the moment it’s generated. You set it up once and it keeps meaning the same thing in six months. Nothing about the data is stored on the schedule; it triggers exactly the same generation path as the export button.
Read-only, and a spot on the dashboard
Reports and datasets support a read-only view, so you can hand someone the numbers without handing them the ability to change what’s being measured — the common case for an auditor or a manager who just needs the monthly figure.
Saved reports also surface as a dashboard widget, so the ones you actually use are one click from the front page.
A note on safety
Since the queries are ours, we get to be strict about them. Filter values are bound parameters, never interpolated into a query string. Sortable columns are a whitelist that’s validated against the query’s actual output at import time, so drift gets caught in CI rather than in production. And there’s a registry of columns that can never be projected under any circumstances — private keys, CSRs, session tokens, verification hashes, just-in-time credential plaintext. A reporting engine that can read everything is a data-exfiltration primitive; this one can’t.
UX sharpening
- Picker drawers apply on click. Choosing members, groups, devices, endpoints or datasets in a selection drawer now takes effect immediately, instead of making you confirm with a separate “apply”. Small change, enormous difference if you do it fifty times a day.
- Password rules live on the backend. Length and complexity are enforced identically everywhere a password is set or changed — the web UI and every login path now agree, because they ask the same authority.
- “Must change password” is honoured at device login. A local account flagged to change its password is now denied TACACS+/RADIUS/NAC sign-in until it does, instead of being blocked only in the web UI. If you use that flag as a control, it’s now actually a control.
- MFA push expiry is audited. An unanswered push approval that times out now records its own event, so “nothing happened” is visible in the trail.
Fixes and quiet improvements
- One bad batch can’t wedge TACACS+ logging. A poison log batch is dropped rather than stalling the entire authentication-log writer.
- Faster config reloads on large inventories. The TACACS+/RADIUS/NAC auto-reload now skips a full regeneration when nothing config-relevant changed, and generation no longer blocks the control-plane event loop — a large cut in idle CPU if you run a lot of devices.
- Correct timezone for scheduled jobs across nodes. The system timezone is resolved without stale caching, so scheduled reports fire at the right local time on every node of a cluster.
- Backups work on a fresh install. Creating a backup on a brand-new install no longer fails with a 500 — the backups volume gets correct ownership at boot.
- Auth settings form. Nested fields and zeroed values persist correctly.
Reporting was the biggest single thing we’ve built since the Configuration Tracker, and the design took longer than the code. If the balance lands wrong for you — a preset you’d rather bend, a table you wish were curated — that’s exactly the kind of note that shapes the next one. Where the platform goes after this is on the roadmap.
Self-hosted, artifact-only, Elastic License 2.0.
1.1.2 — you wrote this one
Before the changelog, the important part: thank you. To everyone who pulled the images, stood Taranac up against real gear, and then took the time to write in when something felt off — this release is mostly yours. Almost every item below started as a message, an issue, or a “hey, is this supposed to…” — and that feedback loop is exactly what a young project lives on. Keep it coming.
Here’s what 1.1.2 changes.
Vendor authorization profiles now deploy exactly as previewed
The headline fix, and the one that mattered most to the people who found it. If you
built a TACACS+ authorization profile on a non-shell vendor template — Juniper
junos-exec, FortiGate, Palo Alto and friends — the per-profile preview looked
perfect, but the deployed daemon config quietly collapsed it into a generic
shell / privilege-15 block. The Policy Tester preview showed the same flattened
version, so the two agreed with each other and disagreed with what you actually
authored.
The daemon config now renders every vendor service and raw attribute, matching the per-profile preview one-to-one. What you see is what the box gets.
LDAP failures that tell you why
A failed LDAP / Active Directory login used to report a flat “invalid credentials” no matter what actually went wrong. Now it names the real cause — wrong password, account locked, disabled, expired, or “must change password.” Less guesswork at the help desk, faster answers for the person locked out.
MFA replay protection, now cluster-wide
A one-time TOTP code is supposed to be exactly that — one time. It turned out that in a multi-worker or multi-node deployment, a code could be replayed inside its short validity window by aiming the retry at a different worker process or cluster node, because “already used” was tracked per process. Used codes now live in a shared store, so a code burned on one node is burned everywhere. If you run Taranac clustered, this one’s worth the upgrade on its own.
The smaller sharp edges
- Correct privilege level on a brand-new user’s first login. A user created moments earlier could authenticate but land on a default privilege level for that first session — the authorization step didn’t yet know about them. It now loads the user the same way login does, so the privilege from the matched policy rule applies immediately.
- Matched policy rule, by ID. The TACACS+ authentication and authorization log detail now show the UUID of the rule that matched, next to its name — a stable handle for correlation and scripting, even after a rule gets renamed.
- New-IP alert emails name the browser. The “sign-in from a new IP” notification used to render an empty Browser row; it now reports the client’s browser (or “Unknown” for header-less API callers).
Self-hosted, artifact-only, Elastic License 2.0. Small, fast, and shaped by the people using it — that’s the cadence we want to keep. Found another rough edge? Tell us; the next one has your name on it too.
Boot it and it's done — Taranac now ships as a VM
Installing Taranac has always been short — download the bundle, run two scripts. But “short” still assumed you had a host ready, Docker installed on it, and outbound access to a registry. For a pilot that’s an afternoon and three places to get something subtly wrong, and for a locked-down segment the registry access alone could be a non-starter.
So Taranac now also ships as a virtual machine. One file, one boot.
Two artifacts, one image
There’s an OVA for VMware — ESXi, vSphere, Workstation, Fusion — and a
QCOW2 of the same appliance for Proxmox VE, libvirt/KVM and plain QEMU. They
aren’t two builds: the QCOW2 is converted from the very OVA we publish, so
whatever we tested is what you run either way. The OVA is exported at hardware
version vmx-13, which keeps it importable all the way back to ESXi 6.5,
and the image carries qemu-guest-agent next to open-vm-tools so it’s equally
at home on either hypervisor.
Both are on the download page, with SHA256 sums published beside them. Check the hash. This image gets root on the box that authenticates your network — that’s exactly the kind of file you verify before you boot it.
The install runs offline
This is the part worth the engineering. Every container image is already baked into the disk. When the wizard installs Taranac, it doesn’t pull anything: no registry, no internet, nothing to mirror. An air-gapped segment that would have needed a whole internal registry story now needs a file copy.
What is not baked in is anything secret. No .env, no master key, no
pre-generated passwords — the installer generates every secret on your box, at
install time. Two appliances deployed from the same file share no credentials.
An appliance that shipped with secrets inside would mean one leaked image
compromises everyone who downloaded it, and that trade was never on the table.
Four questions on the console
Power the VM on and the setup wizard is already there — nothing to log into
first. It asks for the hostname, the network (DHCP or a static address,
prefix, gateway and DNS), the timezone — from the full tzdata list, not a
curated handful of cities — and a password for the taranac login user.
Then it offers to install. Choose Install Taranac, give it the address people will open the admin UI at, an admin username and email, and it goes away and brings the stack up. The last screen hands you the URL, the username and a generated password. Copy it then and there.
Underneath, this is not a third way for Taranac to exist. Inside the appliance is
the ordinary deployment bundle, in /opt/taranac. Every ./taranac command, the
TLS certificate procedure, ./taranac update — all of it works exactly as
documented, because it is the documented thing, pre-deployed.
What it doesn’t do yet
Three honest limits, all of them deliberate rather than forgotten.
The wizard installs a core node — collector, witness and captive-portal roles aren’t in it. Those are still deployed as their own docs describe.
The appliance is not self-updating. OS patching and product upgrades stay on your schedule, the same as any VM you run.
And one to act on: the published images carry SSH host keys baked in, so every appliance deployed from the same file presents the same fingerprint. A future build will regenerate them at first boot and make the password step mandatory rather than skippable. Until then the download page and the guide both carry the three commands that fix it — run them once, right after the wizard finishes, before the box sees production traffic.
Where to start
The appliance guide walks through
both imports with screenshots — including the Proxmox settings that are not
optional, since the image is SeaBIOS rather than UEFI and wants a VirtIO SCSI
controller with the disk on scsi0. Get those wrong and it won’t boot; get them
right and you’re at the wizard in a couple of minutes.
Self-hosted, artifact-only, Elastic License 2.0 — same as always. Now with a shorter road to the first successful authentication.
1.1.1 — ArcSight-ready, and a JIT fix under real load
The Configuration Tracker landed a week ago, and 1.1.1 is the fast follow: one new capability and two fixes that came straight from watching the thing run under real load.
CEF, for the SIEM you already have
Taranac has always forwarded its audit and AAA events over syslog. Now it can speak your SIEM’s native dialect: pick CEF (ArcSight) as the format on a syslog destination and every event ships in Common Event Format, ready for ArcSight, Microsoft Sentinel and the many other tools that parse CEF out of the box — alongside the plain RFC 3164 and RFC 5424 you already had.
The detail that matters for correlation: each event carries the canonical rt
event-time field, so it’s timestamped by when it happened, not when the
collector got around to it — no timezone drift smearing your timeline.
The JIT fix real load asked for
In the 1.1.0 post we made a lot of noise about just-in-time credentials — a password minted for a single collection run, then rotated away. Honest confession: the first cut minted that password per run, and the moment two collections hit the same JIT account at once — exactly what happens when you point one credential at a rack of devices — the second job tripped over the first and failed.
The fix is the version we should have shipped: the minted password is now an account-scoped “warm” lease. Every concurrent collection on that account borrows the same live password and logs in in parallel, and the password is rotated once it goes idle — never yanked out from under an in-flight login. Same security story — no long-lived secret, nothing a human ever sees — now with the concurrency a real fleet actually needs.
Backups that survive an update
A quieter fix, and the kind of quiet you want from infrastructure: the backups directory now lives on a named volume. Recreate the container, pull a new image — your backups are still there, instead of quietly vanishing with the old container. Boring. Correct. Exactly what a backup is supposed to be.
There’s also a small structural tidy-up: for SCP/SFTP sources the remote path now lives on a file recipe, matching how HTTP recipes already work — the source describes the connection, the recipe describes what to fetch. Existing sources migrate themselves on upgrade.
Self-hosted, artifact-only, Elastic License 2.0. Small release, quick turnaround — that’s the cadence we’re aiming for.
1.1.0 — Configuration Tracker ships
A couple of weeks ago we published the shape of Configuration Tracker — a design doc, no code, just locked decisions. Today it’s 1.1.0, and the tracker is real. It backs up, versions and diffs your device configs — and because Taranac is already your AAA server, it does two things a standalone backup tool structurally can’t: it can log in with a throwaway password nobody has ever seen, and it can tell you who typed each change.
Point a tracked config at a device, pick a recipe (which commands to run, plus scrub sets that mask secrets and strip volatile lines before anything is stored), and Taranac captures a versioned snapshot on a schedule or on demand. Every capture is content-hashed and deduped, rendered in a side-by-side diff, and you can mark any version as the baseline — plus add your own labels. A config-status column then flags drift the moment a capture stops matching that baseline, and starter recipes + scrub sets ship for a range of vendors.
Collect over whatever the box speaks
A tracked config reaches its device however that device is happiest to be read:
- SSH and Telnet for CLI gear — run the show/running commands and capture the output.
- SCP and SFTP for kit that hands you a config as a file — pull the blob straight off the device.
- HTTPS (and plain HTTP) for API-driven platforms. And this is the open door: an HTTP recipe carries its own request — path, headers, body — so you can author a custom recipe to fetch essentially any file or endpoint a device exposes, not just a fixed command set. If it answers over an API, you can track it.
Two ways to hold the keys
Device logins live in an encrypted credentials vault. Every stored secret is Fernet-encrypted with the deployment’s master key, per value, at rest — the plaintext never sits on disk and is never returned by the API.
But the vault isn’t the interesting part. The interesting part is not storing a password at all — and this is where being the AAA server changes the game. Taranac doesn’t just use the account it logs in with; it owns it. So a tracked config can authenticate with a just-in-time credential: at collection time Taranac mints a strong, 32-character random password on a managed local account, logs in with it for that one run, and rotates it away the moment the job drains.
Follow what that removes. The service account an automated system uses to reach your whole fleet is the classic soft target — a shared password sitting in a config file, reused for months, one leak away from a fleet-wide compromise. A JIT account has none of those handles:
- Nobody has ever seen it. The password is machine-generated and never displayed — not in the UI, not in logs, not over the API. There is no human copy to phish, paste or forget in a wiki.
- It’s genuinely strong. 32 random characters, not a memorable string a person picked.
- It’s already gone. It rotates on every collection, so a password sniffed off the wire is dead before it’s useful — and tomorrow’s doesn’t exist yet.
For the account that reads your configs, service-account compromise simply stops being a category of risk that applies to you.
Who typed what, delivered
The other AAA dividend: open any change and the AAA Activity drawer shows who logged into that box, which commands they ran, and the session accounting — scoped to the exact window between the two config versions. The diff and the culprit, side by side, no extra plumbing. (Per-command attribution needs TACACS+; a RADIUS-only device still gives you who-logged-in and session accounting — an honest limit, not a bug.)
Collection itself runs through a collector — the embedded one needs no setup and covers the single-site case; a dial-out standalone collector handles segmented and remote networks. That part deserves its own post, and it’ll get one.
Self-hosted, artifact-only, Elastic License 2.0. Same as always: kick the tyres, file issues, tell us what’s rough.
What's next — a config tracker that knows who typed what
With HA landed and 1.0.8 out the door, we’ve started sketching the next big piece — and it’s one of those ideas that only makes sense because of what Taranac already is.
Meet Configuration Tracker: backup, versioning and diff for your network devices’ running configs. On its own that’s a well-trodden space — Oxidized, Unimus, SolarWinds NCM all live there. So why build another? Because Taranac is also your TACACS+/RADIUS server, and that gives it two things the others structurally can’t have.
It knows who typed what. A diff usually tells you a line changed. Because Taranac already collects TACACS+ command accounting, ours can tell you which engineer ran which config commands to cause it — the diff and the culprit, side by side.
It doesn’t need a stored password on every device. Instead of stashing credentials for the whole fleet, it can mint a one-time ephemeral account for a single collection job, validate that login itself, and let it expire. Nothing permanent to leak.
Under the hood it’s Postgres-backed rather than a git repo — which means “show
me every version of this config” is one indexed query instead of a git log
crawl that turns into minutes at a few thousand devices. And because the
snapshots live in the same database, your Patroni/etcd HA replicates them for
free.
Fair warning: this is a design doc, not a shipping feature — no code yet, just locked decisions and a build order. We’re publishing the shape early because the interesting arguments (flat-list-plus-tags vs. folder trees, credential-spray safety when you are the AAA server that could ban yourself) are worth having in the open. More as it takes form.
1.0.8 — 1792 tests and nothing to hide
Four weeks ago we shipped the 1.0.0 release candidate. Today it’s 1.0.8, and the arc between them was mostly one word: high availability. Clustering, auto-failover, etcd with TLS on by default, backup/restore that survives a schema change, per-node dashboards so you can see which member is doing what — a long parade of small, unglamorous fixes that add up to “this thing stays up.”
But shipping infrastructure people log into means asking them to trust it, and “trust me” isn’t a feature. So 1.0.8 also ships a receipt. Every release now runs the full suite on a dedicated runner before the images publish:
- 1792 passing, 0 failing — unit and service logic, full HTTP across every
domain, real
tac_plus-ng/ FreeRADIUS / NAC daemons doing live auth, and sustained-rate load through an HA failover. - 70% backend line coverage, commit-pinned, dated.
- Three known-gap cases tracked openly as expected-fail — not swept under the rug, not counted as green.
That last line matters more than the big number. Anyone can quote a test count;
we’d rather tell you exactly which cases we haven’t nailed yet. The full
breakdown ships in the tarball as TESTING.md — a plain table you can read
before you ever pull an image.
Self-hosted, artifact-only, Elastic License 2.0. Same as always: kick the tyres, file issues, tell us what’s rough.
Learning to fall over gracefully
AAA is the kind of infrastructure nobody notices until it’s gone. When your TACACS+ server takes a nap, engineers can’t log into switches; when RADIUS blinks, 802.1X ports stop letting laptops on. So the request we heard most after the RC was blunt: “what happens when the box dies?”
For the last few weeks the answer has been the whole story. Taranac can now run as a cluster — Postgres streaming replication managed by Patroni, with etcd holding the quorum. Lose the primary and a replica gets elected, no human in the loop. Two nodes plus a tiny witness is enough for safe automatic failover; split-brain is settled by the quorum, not by a clever rule we’d inevitably get wrong.
The trick that makes it calm rather than scary: the auth daemons don’t depend on the database at runtime. They answer from generated config and their own caches. So during a failover the control plane (the editor, the writes) pauses for a moment while a new leader is elected — but authentication keeps answering the whole time. Availability beats freshness when the alternative is a locked-out network.
The bit we’re proudest of is the boring bit: turning a standalone install into
a cluster is ha-convert.sh, adding a node is ha-join.sh, and if you change
your mind, ha-deconvert.sh shrinks it back. No reinstall, no data migration —
the replication-ready database has been shipping to everyone since before HA
existed, waiting quietly for the overlay to land on top.
HA is a paid Pro feature — the honest split is that keeping critical infra alive is exactly the kind of thing worth paying for, and it funds the free core. More on the trade-offs as people put it under real load.
Taranac 1.0.0-rc
The first release candidate is here. Taranac 1.0.0-rc brings the whole platform together:
- AAA — TACACS+ and RADIUS device administration, command authorization, templates and profiles, first-match policy with a built-in tester.
- NAC — 802.1X with EAP-TLS, PEAP, EAP-TTLS/TEAP and MAB; endpoints, NAC policy with VLAN/dACL/redirect, and CoA.
- Captive portal — guest access with flows, SMS and sponsor approval.
- PKI — an internal CA plus AD CS and third-party trust, EST and CRL.
- Identity & MFA — AD/LDAP sync and user-selectable MFA: Taranac MFA push, Telegram, email or TOTP.
Everything is self-hosted in Docker under the Elastic License 2.0. Kick the tyres, file issues, and tell us what’s rough — that’s what an RC is for.
One policy core — the idea behind Taranac
Most networks end up with access policy scattered across boxes: a bit of TACACS+ here, a RADIUS server there, 802.1X configured per switch, and an identity story that never quite lines up. Each piece drifts on its own.
Taranac starts from the opposite idea: one policy core. Every request — whether it’s an engineer logging into a switch over TACACS+, a RADIUS login, or a laptop appearing on a port over 802.1X — is evaluated against the same ordered set of rules, matched on who, where, when and source.
That single source of truth is what makes the rest tractable: consistent decisions across protocols, a built-in tester to prove a rule before you ship it, and an audit trail that reads the same no matter how the request arrived.
This blog is where we’ll write about how that core is built — the wins, the trade-offs, and the occasional yak we had to shave.
From TACACS GUI to Taranac — the long way round
Before there was Taranac, there was tacacsgui.
For more than three years it was my project — a web GUI for TACACS+, built and maintained mostly on evenings and weekends. People ran it in real networks. It did an honest job. And I’m genuinely proud of it.
But if you build one thing for that long, you also collect a long list of “if I ever start over, I’d do this differently.” tacacsgui was TACACS+ and only TACACS+. The data model had grown ring by ring like a tree, and some of those rings I’d have cut back. I knew where the seams should be, where the policy engine wanted to live, how identity and NAC and a captive portal should hang off one core instead of being bolted on. I could see the whole thing.
And then I ran out of road. Not out of ideas — out of time and energy. A rewrite that big is a mountain, and I was one person with a day job. So the new architecture stayed where it was: in my head. I carried it around for a long time, turning it over, refining it in the quiet moments, never quite able to start.
What changed isn’t that I suddenly found the hours. It’s that I found people to build it with — a whole bench of tireless assistants I can think out loud with. Sketch an idea, have it argued back at me, poke holes in the data model, weigh a trade-off at 2am, and then actually write the thing — exactly the way I’d been picturing it all along. The mountain didn’t shrink. I just stopped climbing it alone.
So this is where tacacsgui goes: not abandoned, outgrown. Same instinct — make network access control something a human can actually reason about — but rebuilt from the core out, with room for RADIUS, 802.1X, identity, MFA, PKI and more, all deciding on one engine. New name to match the new shape: Taranac.
If you followed me here from tacacsgui — thank you. You backed the caterpillar. This is the butterfly, and it’s finally out of my head and into code. This blog is where I’ll show my work.
Get the release notes
Release notes go to Taranac account holders. Create a free account and tick “Email me about new releases” — one short email a month, what changed and what to upgrade first. Your account also gets you support tickets and licensing.