Skip to content

1.2.3 — a module in one command, a cluster in three

← Blog

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.

Terminal window
./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:

Terminal window
sudo taranac-module collector --status # where it points, what it last did,
sudo taranac-module portal --status # and whether the core is answering
sudo taranac-module collector --uninstall # cleanly — containers and volumes

Two 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:

Terminal window
ssh taranac@<the address the wizard just configured>
sudo taranac-module # collector | portal | witness

And 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

Terminal window
./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 node

That 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 scp of 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 connectivity proves 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 switchover hands 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 sync brings 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-node evicts a node with one command on the primary; the surviving members need nothing, because membership is read from etcd. detach turns 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 a DNS: 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.