Aller au contenu

Backup & upgrades

Taranac backs up its configuration, not its history. A backup captures everything that defines how the system authenticates and authorises — devices, policies, profiles, users, groups, RBAC, the internal CA, daemon configs and the host secrets needed to bring the stack back up — and packages it into a single encrypted archive. Logs, live sessions, alerts and other dynamic data are deliberately excluded.

Backups are created and managed from the admin UI under System → Backup (the page itself is titled Backup & Recovery). Restore is not in the UI — it is destructive (it erases the current configuration) so it runs from the ./taranac restore CLI, off the API, and verifies your master key before it touches anything. This page covers what is in an archive, how an archive reports its own completeness, scheduling and retention, recovery, the ./taranac update upgrade path and the extra steps some older releases need, and how backups relate to high availability.

A backup archive: an outer tar holding a plaintext manifest.json and an encrypted payload.enc (db, certs, host_files, host_config, daemon_configs), sealed by the master key that is stored separately.

An archive is named taranac-backup-<UTC-timestamp>.tar. It is an outer tar with two members:

  • manifest.json — plaintext, non-secret metadata: format_version, created_at, trigger (manual/scheduled), actor, the Taranac version (app_version), the database schema revision (alembic_head), a short master-key fingerprint (kek_fingerprint) and an encrypted canary token (kek_canary), a payload_sha256, and the archive’s own report of itself — included, missing and per-item issues (see Does the archive hold what it claims? below). Because it is readable without the key, recovery can verify the right master key before decrypting anything, and the answer to “is this archive usable” travels with the archive and survives being copied.
  • payload.enc — the actual contents, an inner tar encrypted with the master key (Fernet).

The encrypted payload contains:

ComponentBadgeWhat it isAlways included?
Database dumpdbA data-only dump of the configuration tables (dynamic tables excluded)Yes — not a toggle
CertificatescertsThe internal CA plus all server key/certificate pairsOptional (include_certs)
Host fileshost_filesThe active .env file and Docker Compose file — i.e. the non-database secrets: master key, API keys, DB password. All-or-nothing: if either is uncollectable the component is reported missingOptional (include_host_files)
Host confighost_configThe operator’s own files under config/: tls/ (your CA-signed certificate), firebase/ (the Push MFA service-account key) and etcd-ca/ (the cluster’s etcd CA — what admits or replaces a node). Carried under the same include_host_files toggleOpportunistic (see below)
Daemon configsdaemon_configsGenerated TACACS+ / RADIUS / NAC config snapshots (informational — regenerated from the DB on restore)Optional (include_daemon_configs)

A leaked archive is useless without the master key. Two things are deliberately not in it, and both matter at recovery time: the MFA enrollments (taranac-mfa has its own database and its own crypto — users with Push/TOTP re-enroll unless you protected that side separately) and the Active Directory machine account (Samba rotates its password weekly, so a restored one is a broken trust that reads as a mysterious authentication failure; re-join the domain from System → Authentication). config/cluster-secrets/ is also excluded — it describes this cluster’s identity, not portable configuration.

host_config is opportunistic in declaration, never in reporting. An install with no CA-signed certificate and no Firebase key legitimately has nothing there, and an empty tree raises nothing; a tree that holds something the api container cannot read is always an operator fault and is named, per file, in the archive’s report.

The manifest carries the archive’s own verdict, in plaintext beside the payload, so the answer is known at backup time rather than at recovery:

  • included — the components that actually made it in.
  • missing — declared components that did not. Anything your settings promised and the build could not collect.
  • issues — the per-item half: one entry per path that was not archived, with component, item, a stable reason (unreadable, not_mounted, empty, unlistable) and the raw OS message. included/missing answer which components; that is the right granularity for a headline and the wrong one for acting on.

An incomplete archive is still written — a partial archive beats no archive — but the outcome is reported as degraded: an error in the log, the detail in the BACKUP_CREATED audit entry, and one standing backup:incomplete alert that clears only when a complete archive is produced. Deliberately one alert, not one per file. In the UI the row carries an incomplete badge and a Show what was not archived panel listing each path and reason.

Directories are all-or-nothing. tar writes members as it walks, in sorted order, so without a check an unreadable 0600 private key would leave its ca.crt/tls.crt siblings in the archive — restoring a certificate with no private key, and looking like it had worked. A tree is therefore proved readable before a byte of it is written.

The boundary between “config” (backed up) and “dynamic” (excluded) is defined in one place in the product, so the backup side and the restore side never disagree. Excluded from the database dump:

  • Log tables (authentication, sessions, accounting, audit logs, LDAP sync logs)
  • Live NAC sessions, the alert queue and firing alerts, MFA challenges
  • Ephemeral tokens (refresh tokens, password-reset tokens, certificate-download tokens) and password-reuse history
  • Endpoint discovery staging (auto-repopulates from live traffic)
  • The NCM collection-run journal (ncm_runs and its ncm_results children) and NCM collection-runtime state — collector enrollment tokens, in-flight dispatch, JIT mint leases, the notification outbox. The durable NCM record (sources, recipes, scrub sets, credentials, tracked configs, snapshots) is configuration and is backed up
  • The IEEE OUI manufacturer database (re-synced on a schedule)
  • HA cluster-runtime state — the node roster (cluster_nodes), its join tokens (cluster_join_tokens), and per-node service health (node_service_health). This describes this physical cluster, not portable configuration; the running node keeps its own current roster on restore, so a stale roster can’t resurrect decommissioned or foreign nodes
  • The schema-version row (the restore target is already at the right schema)

Everything else — your real configuration — is included.

System → Backup has three sections: schedule & retention, the archive list, and a recovery-instructions panel. Access is gated by the backup RBAC permission set (view, create, download, delete, manage); if you lack backup.view the page is empty.

Backup & Recovery settings page in the Taranac admin UI The Backup & Recovery page: schedule and retention on top, the archive list with per-archive badges below.

Editable settings (saving requires backup.manage):

SettingDefaultMeaning
schedule_enabledonMaster toggle for automatic backups. On by default — an install that has to be visited before it protects itself spends its first weeks unprotected, and nobody discovers that until a disaster
interval_hours24Hours between scheduled backups (floored to a 1-hour minimum)
retention_count5How many archives to keep on disk; older ones are pruned. Applies to manual backups too, not only scheduled
include_certsonPack the internal CA and TLS certificates
include_daemon_configsonPack the generated TACACS+/RADIUS/NAC config files
include_host_filesonPack the .env and Compose file (the host secrets)

The database dump is always packed; there is no toggle to exclude it.

Paths are internal and not editable from the API: archives are written to /app/backups in the container, certificates are read from /opt/taranac/certs, and the host material comes from the read-only mounts /app/host/env, /app/host/compose.yml and /app/host/config.

When scheduling is enabled, a background worker runs on the configured interval, creates one archive, and prunes to the retention count. A scheduled-backup failure is loud: it records a BACKUP_FAILED audit event and raises a firing backup:scheduled alert (cleared automatically on the next successful run). The worker never crashes the scheduler.

Create backup now (backup.create) builds an archive synchronously — it usually takes a few seconds. On success the archive list refreshes.

The dump runs under a 15-minute ceiling. A configuration dump is small (logs are excluded — under a megabyte in practice), so a run past that is hung, not slow, and a hung dump was the quietest failure this feature had: it held the scheduler’s single slot forever, every later tick was dropped as a misfire, and the dashboard kept showing the last successful archive. The timeout turns that into an ordinary failure, with an audit event and an alert.

A single unreadable file does not kill the whole archive either. The api container runs as a different user from the operator who installed Taranac, and .env is mounted read-only, so the installer and the updater set the group permissions on the host; a file that still cannot be read degrades the archive (reported as missing, with an alert) instead of aborting it.

Each row shows the creation date (in your configured timezone), the filename with its app_version, the size, the trigger badge (manual or scheduled), and “included” badges (db, certs, host files, host config, daemon configs). An archive that fell short also carries an incomplete badge and a Show what was not archived panel — the per-item report out of the manifest, one row per path with its reason. Two actions:

  • Download (backup.download) — streams the .tar through the authenticated API; the file never appears in a URL. Store it somewhere safe and off-box.
  • Delete (backup.delete) — asks for confirmation with the exact filename, then permanently removes the archive. This cannot be undone.

The Backup status widget leads with a verdict, not a timestamp — “a backup happened” is not the question an operator has at a glance. It reports three states, read from the same plaintext manifest as everything else:

StatusMeaning
successThe archive holds everything it declared
incompleteAn archive exists and was written without part of its content
failedNo archive at all — there is nothing to restore from

incomplete is deliberately not called failed: they demand different actions, and an operator reading “failed” over an archive that is 90 % usable — possibly their only copy — may delete it or skip restoring from it.

Stale is orthogonal and keeps its own warning tone: a complete archive can still be too old, and an incomplete one can be minutes fresh. With scheduling on, “stale” means older than twice the configured interval; with scheduling off, only “no archive at all” is stale — the manual cadence is the operator’s call.

Recovery is CLI-only and destructive. A fresh Taranac deploy is not empty — startup migrations and seeds build a working default system — so recovery first wipes the current configuration and then loads the backup. That is why it is a deliberate, off-the-API action.

The intended flow:

  1. Deploy a fresh, healthy Taranac of the archive’s version and log in as the default admin. Confirm the deploy itself works (this separates “deploy failed” from “recovery failed”).
  2. Have the original master key at hand.
  3. Copy the archive onto the host and run ./taranac restore with its path.

The Recovery instructions panel in the UI prints the same commands; there is no recover-from-browser button by design.

Archives live in the backups Docker volume, mounted into the api container at /app/backups. A named volume on purpose: on the container’s writable layer they would be lost the moment an update recreated the api. Its ownership is set at boot, so the first backup on a brand-new install works like any other. Besides the UI’s Download button you can take them off the box with your own tooling:

Terminal window
docker run --rm -v taranac_backups:/b -v "$PWD:/out" busybox \
sh -c 'cp /b/taranac-backup-*.tar /out/'

The volume name is prefixed by your Compose project — docker volume ls | grep backups if you renamed the install directory.

Getting an archive back needs nothing of the kind: ./taranac restore takes the archive’s path on the host — relative to the directory you run it from, or absolute — and copies it into the volume itself, readable by the api. An archive that is already in the volume (one this install wrote, listed under System → Backup) is named by its file name alone.

Run it from the install directory. It performs the whole runbook, both phases included:

Terminal window
./taranac restore /root/taranac-backup-YYYYMMDDTHHMMSSZ.tar # a file on this host
./taranac restore taranac-backup-YYYYMMDDTHHMMSSZ.tar # already in the backups volume
./taranac restore <archive> --master-key-file /root/old.key # this install runs a different key
./taranac restore <archive> --yes # skip the phase-2 confirmation

When this install does not already run the original master key, give it in one of two ways — never as an argument, where it would sit in the shell history and in ps for every user of the host:

Terminal window
./taranac restore <archive> --master-key-file /root/old.key # a file that holds only the key
./taranac restore <archive> --master-key-file - --yes < /root/old.key # '-' reads it from stdin
read -rs TARANAC_MASTER_KEY && export TARANAC_MASTER_KEY # or the environment: paste the key, press Enter
./taranac restore /root/taranac-backup-YYYYMMDDTHHMMSSZ.tar

--master-key <KEY> is refused, with nothing changed. The file wins when both are given. Whichever way it arrives, the key is written into .env — every container that reads it is recreated from that file.

Restore asks you to type yes before it erases the configuration. With no terminal to type it on — piped, ssh without -t, or the key read from stdin — it refuses before it changes anything and prints the same command with --yes.

In order, the script:

  1. Refuses up front what it cannot finish: an install that reads its key from a file (MASTER_KEY_SOURCE=file or prompt — switch to MASTER_KEY_SOURCE=env with the original key as MASTER_KEY for the recovery, or put the original key where your key source reads it), and, under HA, any node that is not the leader.
  2. Copies the archive in, verifies the key against it and extracts the host material (phase 1).
  3. Overlays the five recovery keys onto this install’s .env, keeping the previous file as .env.pre-restore.<UTC time> — a new one per run, so a second attempt never overwrites the original.
  4. Checks that the key now in .env opens the archive. If it does not, .env is put back and nothing else has changed; the message tells you to pass the original key with --master-key-file (or TARANAC_MASTER_KEY). Everything after this step is irreversible, which is why the check sits here.
  5. Restores the certificates into the taranac_certs volume and the operator’s files — your TLS certificate, the Firebase key, the etcd CA — into ./config, with the group the api needs to read them, so this node’s next backup carries them too. An etcd CA that this host does not trust — an archive taken before a cluster rotate-ca, or another cluster’s — is not put over the one in place: it is set aside in config/etcd-ca.restored-<time> and the script says so. That directory holds a CA private key; delete it once you are sure the restore went to the right cluster.
  6. Loads the database (phase 2) from a one-off container while the api is stopped: the api refuses to start on a database whose key check belongs to another key, so the data goes in first, and nothing can write into it under the old key meanwhile. If the load fails, the script tells you whether the data went in: if it did not, the api is started again as it was and you re-run the same command once the cause is fixed — the load truncates and reloads, so a second run is safe.
  7. Clears the daemons’ master_key volume so their entrypoints adopt the restored key, recreates the api, the AAA/NAC daemons, MFA and the renderer, waits for the api to report healthy, restarts every service so each re-reads what was restored, and removes the decrypted extraction from the volume.

A failure before step 6 leaves the database untouched and says so; a failure after it says not to put the previous .env back — its key no longer opens the restored data.

Run the restore on the leader. It loads the dump into the cluster’s one writable database, and every replica takes it from there by replication; on a replica the script refuses, changes nothing and points to ./taranac cluster status to find the leader. It merges the Patroni overlay by itself and never recreates the database or etcd containers.

Two more refusals keep the cluster in one piece:

  • An archive cut under other keys than the cluster runs now — taken before a rotate-key — is refused before .env is touched. Every node carries the same five keys; restoring the old ones on the leader alone would leave the other nodes unable to read the restored data or talk to it. Restore an archive taken after the last key change, or, if you need exactly that one, bring the cluster down to this node first (./taranac cluster remove-node --node <name> for each other node, then ./ha-deconvert.sh), restore, and build the cluster again.
  • The cluster’s live topology is kept. The node roster, its join tokens, per-node health and the published cluster manifest describe the cluster as it is, not as it was when the archive was cut, so they survive the restore as they are — a stale node list is never handed to every node.

The guided script exists because recovery has to edit host files and recreate containers in a precise order; if you need to drive it yourself, these are the same steps. The extraction lives in the backups volume, so it is read through the api container.

Phase 1 — verify the key, extract the host material:

Terminal window
./taranac exec -T api python -m app.scripts.restore host \
/app/backups/<archive>.tar --out /app/backups/restore

It verifies the canary against the loaded key (or a key read from stdin with --master-key -), decrypts the payload and reports which of env/, compose/, host_config/ and certs/ the archive actually held.

Between the phases — read the five recovery keys out of the snapshot and set them in .env (do not copy the file over), check that the key in .env opens the archive, and put the certificates and config/ back:

Terminal window
./taranac exec -T api cat /app/backups/restore/env/env # take the five values from here
printf '%s\n' '<MASTER_KEY from .env>' \
| ./taranac exec -T api python -m app.scripts.restore check-key /app/backups/<archive>.tar
./taranac exec -T api sh -c 'cp -a /app/backups/restore/certs/. /opt/taranac/certs/'
./taranac cp api:/app/backups/restore/host_config/. ./config/
sudo chgrp -R 999 ./config && sudo chmod -R g+rX ./config # the api reads them as gid 999

check-key reads one key from stdin and exits 0 when it opens the archive, 10 when it is not a key at all, 11 when it is another key. Do not go on past anything but 0.

Phase 2 — the database, with the api stopped:

Terminal window
./taranac stop api
./taranac run --rm --no-deps -T api python -m app.scripts.restore db /app/backups/<archive>.tar

A one-off container, not exec api: the api would refuse to start on the restored key until the dump is in. Phase 2 verifies the master key, the version and the schema, asks you to type yes to confirm, then in one transaction truncates the configuration tables and reloads the dump. The HA cluster-runtime tables (cluster_nodes, cluster_join_tokens, node_service_health) and the published cluster manifest are left untouched — the running node keeps its own current cluster membership. An older archive taken before those tables were excluded is filtered on the way in, so a stale roster is never reloaded. Restore then regenerates the TACACS+/RADIUS/NAC daemon configs from the restored database and records a BACKUP_RESTORED audit event. Add --yes to skip the interactive confirmation (for automation).

Then let the daemons adopt the key and bring the stack back:

Terminal window
docker run --rm -v taranac_master_key:/d busybox rm -f /d/master_key
./taranac up -d --force-recreate api tacacs radius nac taranac-mfa taranac-renderer
./taranac restart
./taranac exec -T api rm -rf /app/backups/restore

Clearing that volume is not optional: tacacs, radius and nac read the KEK from a file in a shared volume and their entrypoints provision it absent-only — an existing file is never updated. A fresh install has already pinned its own generated key there, so without this the daemons decrypt device secrets with the wrong key while the api is perfectly healthy: shared-secret and enable-password reads fail on a system that otherwise looks fine. The database (and etcd under HA) are deliberately not recreated — their data has just been restored.

Phase 2 refuses to run unless three checks pass:

GateCheckOverride
Master keyThe archive canary decrypts with the loaded keynone — a key mismatch always aborts
VersionArchive app_version equals the running version--force
SchemaArchive alembic_head equals the current DB schema revision--force

The master-key gate cannot be overridden. The version and schema gates can be bypassed with --force, but only do so when you are certain the configuration is compatible — see upgrades below.

Taranac ships as a single platform stack: backend, frontend, captive portal and the TACACS+/RADIUS/NAC daemons all roll together under one version and one Git tag. (The optional taranac-mfa push service versions on its own line.) The daemons are downstream consumers of config the backend generates, so they cannot be released separately — what actually guarantees compatibility is the config-format contract between the backend and the daemons, not the version number alone.

An upgrade is not only new images. A release can change docker-compose.yml, the taranac wrapper, install.sh, the HA scripts or the docs, and the new images may depend on those changes — so ./taranac update refreshes both halves. It fetches the release bundle and verifies its checksum, then pulls and verifies the new images while the running version is still in place, and only then replaces every framework file (leaving your .env and config/ alone — those are operator-owned and never overwritten), pins TARANAC_VERSION in .env and recreates the stack. Migrations run automatically when the api container starts (alembic upgrade head), before it serves traffic, and missing internal CA/server certificates are regenerated on the same boot.

CommandWhat it does
./taranac versionInstalled version, plus a check for a newer one
./taranac updateUpdate to the latest published stable version
./taranac update --checkReport only; change nothing
./taranac update --version <X>Update to a specific version
./taranac update --from taranac-bundle-<ver>.tar.gzApply a local bundle (air-gapped). The version is read from the bundle’s own VERSION file, never from the file name
./taranac update --version <X> --repairRe-apply the version this node already runs: a new backup, the files swapped, the stack recreated
./taranac update --no-restartRefresh files and pin the version; skip the pull and restart
./taranac update --keep-imagesKeep the superseded images instead of reclaiming their disk
./taranac update --skip-image-verifyAccept pulled images whose digests differ from the release’s list — a registry mirror that re-pushed them
./taranac update --skip-space-checkPull even when the free-disk estimate says there is not enough room
./taranac update --allow-out-of-orderCluster repair only: skip the cluster’s update order (never its lock); asks for the word OUT-OF-ORDER, or --confirm OUT-OF-ORDER without a terminal
./taranac update --ipv6-own-address-okGo on although inventory entries that matched IPv6 devices through the Docker gateway stop matching them (see IPv6). Also TARANAC_IPV6_OWN_ADDRESS_OK=1
./taranac update --yes (-y)Answer the confirmation prompt — nothing else

Every step that can fail for a reason outside the node runs before the node is touched:

  1. The host. Docker Engine 28.0 or newer (not Podman), IPv6 in the kernel, and Docker able to create a network with IPv6 beside IPv4 — each a refusal with the command that fixes it. Docker’s ip6tables turned off is a warning. Where an interface takes its IPv6 address or route from router advertisements, accept_ra=2 is set and kept in /etc/sysctl.d/99-taranac-accept-ra.conf. The result is one Host: … line, also written to .taranac-update.log. See IPv6.
  2. The device inventory. Inventory entries that match IPv6 devices only through the Docker gateway are named, and the update stops while the inventory has no IPv6 entry at all (IPv6). The TACACS+ addresses that logged in during the last 30 days without an inventory entry are listed — a warning, not a stop — and kept in .taranac-update.log: add them to the inventory, because TACACS+ answers only inventoried devices (Only inventoried devices log in).
  3. The image every running container uses is recorded, with its digest, in the update’s backup directory (.taranac-backup-<timestamp>/images-before.txt).
  4. Free space in the Docker image store is checked against the images that are not on the host yet.
  5. The new release’s images are pulled from the new bundle’s compose files, while .env still pins the running version and the old files are still in place.
  6. Every pulled image is compared with IMAGES.lock — the content digest of every image of the release, PostgreSQL and etcd included, shipped inside the bundle and covered by its checksum. A mismatch names the image.
  7. Every image’s files are read against its own layers, and a damaged one is replaced (below). This takes a few minutes; the running stack is not touched.
  8. Only then are the bundle files swapped, the version pinned and the stack restarted — and the update reports done only when every container runs the new images.

A failure in steps 1–7 — a host requirement, an unreachable registry, a full disk, a digest that is not the release’s, a damaged image with no replacement — leaves the bundle files, .env and the running stack exactly as they were; only downloaded layers remain. The usual cause of a digest mismatch is a registry mirror filled with docker tag + docker push, which changes digests: copy the images with a digest-preserving tool instead (docker buildx imagetools create, skopeo copy --all, crane copy), or, if you trust that mirror, re-run with --skip-image-verify. A bundle built without IMAGES.lock is accepted with a warning that the images were not verified. The shipped compose files pin PostgreSQL and etcd by digest too, so an update never moves the database image to whatever upstream published that day.

A digest proves what an image is, not that its files on this disk are whole: a host that lost power while an image was being unpacked can keep the image’s name and digest with files cut short, and a container then fails with errors such as exec format error. So ./taranac update, ./taranac cluster init, add-witness, add-node, a node’s join and witness-join.sh read each image’s files against its layers before they change anything.

A damaged image is replaced only once its replacement is in hand: first a copy of its own layers, saved on the host (they are intact when only the unpacked files are damaged; no network needed), otherwise the registry, which must answer for the image at that moment. Then the image and its stopped containers are removed and the image is loaded back and read again. Without a replacement nothing is removed, and the message names the damaged files and the cure — the docker save / docker load commands, or docker save <image> | ssh <this host> docker load from a host whose copy passes. A container that is running on a damaged image is never stopped by the check; the message names it. --skip-image-verify skips this check together with the digest comparison.

What the updater guarantees, and what it will not do

Section titled “What the updater guarantees, and what it will not do”
  • No half-updates. If the bundle cannot be fetched — no internet, a proxy, a blocked network — the update aborts with nothing changed, the version pin included, and tells you to restore access or bring the bundle over by hand and apply it with --from. It never continues with the images alone: that would report success while the part that mattered never arrived. A failed checksum aborts the same way.
  • An abort names the phase it stopped in — pulling the images, verifying the pulled images, replacing the bundle files, writing the version pin into .env, starting the stack — and says what is and is not changed at that point. The backup of the previous bundle it took before the swap is .taranac-backup-<timestamp>/ inside the install directory. Re-running the same command completes the update; restoring from that directory goes back.
  • “Nothing to do” means the node really runs the version. A re-run on a node whose bundle, pin and running containers all agree answers Already on <version> — … Nothing to do.: no backup, no file swap, nothing recreated — it only starts containers that are stopped. If the containers still run something else (a pull or restart that failed after the pin), the update proceeds and finishes the job. -y only answers the prompt; to re-apply a version on purpose, use --version <version> --repair.
  • ./taranac version catches a half-finished upgrade. The bundle’s own VERSION file, .env’s TARANAC_VERSION pin and the images the containers run agree only when the last upgrade ran to the end. Where they differ you get what differs and the one command that finishes it. The pin is read forgivingly: Windows line endings or a quoted value in .env do not make it report an update for the release you are already running.
  • It never offers a downgrade. On a pre-release, or on a version newer than the latest stable release, ./taranac version says so, and ./taranac update without --version leaves such a node as it is. Going back is always an explicit --version <older>.
  • Superseded images are removed once the stack is back up, keeping the version just installed and the one before it — the rollback target. Only images under this install’s IMAGE_PREFIX are ever considered, never anything else on the host, and Docker refuses to delete an image a container still references. --keep-images opts out. Without it, every superseded image would stay on disk: roughly 4.5 GB per release across the ten service images, enough to fill an appliance’s disk months after the update that caused it.
  • A release’s new settings are listed once, as a notice. After the file swap the updater prints the settings this release added — a delta against the .env.example it just replaced, not a comparison against the whole shipped example. The defaults already apply; there is nothing to do. A standalone node is not read cluster-only knobs it will never set (it is told how many were withheld).
  • The sudo prompt happens once. The api container reads .env as gid 999, and granting that needs sudo on an installation that predates the grant. Pinning the version writes through the existing file rather than replacing it, so the file keeps its inode — and with it the group — across updates.
  • One run per host. A second ./taranac update started on the same host while one runs is refused, with nothing changed.
  • A witness is not a Taranac node. ./taranac update on an HA witness refreshes the bundle’s files, writes the one line TARANAC_VERSION=<version> into the witness .env, pulls and recreates its etcd-agent (the one Taranac image a witness runs), applies a changed witness compose file itself on a healthy etcd ring, and starts no Taranac service. It goes last, after every database node — see Living with the witness.
  • A collector’s version follows its core, through ./collector-join.sh, never from here.

Run ./taranac update on any member: it works out the order — every replica, then the leader, then the witness — from the cluster’s live state, refuses on the wrong machine before changing anything and names the node to update next, and takes the cluster’s single update lock in etcd so only one member changes at a time. Every run ends with a table of the whole cluster. Copy the printed commands — they carry --version <target>. The whole procedure, what a replica’s update looks like while it waits for the leader’s schema, and the repair options are in Upgrades under HA.

Two cluster-specific points belong here:

  • PATRONI_REST_PASSWORD is required by the database image, which refuses to start without one. Where .env lacks it, the updater provisions it identically on every node: the value is derived from the master key the cluster already shares, so every node computes the same answer with no coordination and nothing new on the wire. A cluster that already has a password keeps it untouched.
  • install.sh is not an upgrade path on a cluster. Re-run on a cluster node or on the witness it refuses, changes nothing and names ./taranac update — it would restart the node outside the cluster’s order and lock.

Most upgrades need nothing beyond ./taranac update. A few older starting points need one extra action. The notes for an update from 1.3.3 come first; an update that skips releases needs the notes of every release it passes.

Before you update

  • Docker Engine 28.0 or newer and IPv6 in the kernel. The stack network carries IPv6, and the update refuses, with nothing changed, on an older Docker, on Podman answering the docker command, on a kernel booted with ipv6.disable=1, or when Docker cannot create a network with IPv6 beside IPv4; each refusal prints the fix. Check with docker version --format '{{.Server.Version}}'. See IPv6 → What the host needs.
  • On an HA cluster, update Docker on every database node before updating the first one. The update checks it node by node and refuses on a node with an older Docker with nothing changed there — but the members updated before it already run 1.3.4, and the rollout stops half-way until that node’s Docker is updated. The witness runs no stack network and is not checked.
  • TACACS+ answers only inventoried devices, over classic TACACS+ and over TLS. A device without an inventory entry no longer logs in with the global key; the global key stays the fallback key of inventoried devices. Before its “Proceed?” question (after it, on this first update, whose prompt comes from the updater already installed) the update — and install.sh run on an existing installation — lists the addresses that logged in with the global key without an inventory entry in the last 30 days, and keeps the list in .taranac-update.log. Add them, or enable their entry. A TLS connection from an address outside the inventory is refused whatever its certificate. See Only inventoried devices log in.
  • IPv6 devices are matched by their own address — check their keys. Until 1.3.3 every IPv6 device reached TACACS+ and RADIUS from the Docker gateway (10.255.255.225), so an entry covering it — the Docker subnet, a /8, 0.0.0.0/0 — matched all of them with its key. While such entries exist and the inventory has no IPv6 entry at all, the update and install.sh stop, name them and say what to add: an IPv6 entry beside each with the same key, or ::/0 for all. With an IPv6 entry in the inventory they name them and go on. To go on regardless, type CONTINUE, or run TARANAC_IPV6_OWN_ADDRESS_OK=1 ./taranac update — the updater already installed runs this first update and does not know --ipv6-own-address-ok yet; ./install.sh --ipv6-own-address-ok for the upgrade through install.sh. IPv6 devices are refused until they have entries. ./taranac diagnose names such entries too. See IPv6 devices and A rewritten source address.
  • Take the backup. Going back to 1.3.3 needs either a database downgrade or the backup taken before the update (last item below).

During the update

  • The stack goes down and comes up once. The update recreates the stack’s Docker network, which gains its IPv6 subnet; so does install.sh run on an existing installation. Data volumes stay.
  • Each image’s files are read against its layers before anything changes, a few extra minutes (Damaged images). Appliances installed from the 1.3.3 OVA shipped two preloaded images cut short (quay.io/coreos/etcd:v3.5.17 and docker:27.5.1-cli), which made cluster add-witness fail with exec format error and stopped an HA node’s update with the replica in awaiting schema; the update finds and replaces them.
  • Where an interface takes its IPv6 address or route from router advertisements, the update sets accept_ra=2 and says so (IPv6). It needs root for that: run it as root, with password-less sudo, or at a terminal.

After the update — network and firewall

  • IPv6 to the client-facing ports no longer passes the host’s ufw INPUT rules, as IPv4 already did not: web interface (80/443), MFA, TACACS+, RADIUS, NAC, EST/SCEP and the captive portal. Filter IPv6 to them in the DOCKER-USER chain. The HA ports (5432, 8008, 2379) are unchanged. See IPv6 → The host firewall.
  • Port 8446 (plain HTTP, SCEP paths only) is published on every installation, whether SCEP is on or not. If you do not use SCEP, filter it in DOCKER-USER; SCEP_HTTP_PORT in .env moves it. See SCEP → Ports and firewall.
  • Per-address limits count an IPv6 client by its /64, and “login from a new IP” means a new /64. See IPv6.

After the update — TACACS+

  • The first configuration apply reloads tac_plus-ng once (a new tls crl-dir line). From then on TACACS+ over TLS checks client revocation: a CA whose CRL was ignored before starts refusing its revoked certificates. See Revocation.
  • The global Single Connection setting (tacacs.single_connection) and tacacs.ipv6_enabled are gone. The catch-all for addresses outside the inventory always covers IPv4 and IPv6 and keeps no connection open. Single Connection per device stays.
  • The built-in Palo Alto PAN-OS template matches service == PaloAlto on new installations only. An existing installation keeps its template, whose paloalto never matched: correct the service name in it by hand.

After the update — RADIUS and NAC

  • Every RADIUS Access-Reject is answered 1 s later. Reject Delay (default 1 s) takes effect. Set it to 0 on AAA → Settings → RADIUS to answer at once. See Server settings.
  • OCSP responder URLs stored earlier on Trusted CAs take effect — check them. A stored URL is the responder for that CA, ahead of any address in the certificates; a stale one refuses every client of the CA. Only http:// is accepted. A stored URL that cannot be used (https://, a user name and password, spaces or quotes) keeps the node’s NAC configuration as it was, with the NAC CA bundle not updated alert naming the CA, until it is corrected, cleared or OCSP is turned off. See The responder URL field.
  • CRL checking applies per CA. Enable CRL Check keeps its value; with it on, the update only removes rejects, because a CA with no CRL configured no longer needs one. The per-CA CRL switches on Trusted CAs and LDAP are gone: the API refuses check_crl on a Trusted CA and eap_tls_check_crl when creating an LDAP configuration (422), and an LDAP update ignores it. A sub-CA client whose chain has a CRL only on the root is refused tls_crl_missing: add the issuing CA’s CRL. See The CRL that counts is the issuing CA’s.
  • Update the radius and nac images of every node with the api. A node whose nac image does not know the per-CA revocation policy raises NAC daemon does not understand the per-CA revocation policy; it keeps a CRL-only configuration and does not deploy one with OCSP.
  • The NAC configuration is applied once after the update, without a restart, where its files change: installations with no trusted CA, a CA bundle held in another order, and server.pem rewritten with its intermediates.
  • Memory. While a RADIUS or NAC configuration is applied, two radiusd processes run in the container for up to about a minute. See Applying a configuration.
  • NAS timeouts and push MFA. A push is answered at most Max Request Time − 3 s after the request (27 s by default); a NAS whose timeout × (retransmits + 1) is shorter gives up first. Max Request Time accepts 5–120 s. See Push MFA over RADIUS.
  • Endpoint groups. Blocked and Unclassified stay with their members as ordinary groups that can be renamed and deleted; a default group deleted before the update is not recreated. A group whose scan exclusion was saved again after it was enabled is scanned again — reopen the groups you excluded and enable it. See Endpoint groups.

After the update — PKI, EST and SCEP

  • EST: the Device identity binding becomes Device accounts. An instance set to Device lists every local account that has already enrolled with a password; an account that has not enrolled yet is not listed, and a router enrolling with it gets a certificate in the account’s own name — add it to Device accounts first. An instance set to Strict starts with an empty list. Requests waiting for approval are issued as requested. See Device accounts.
  • EST: devices configured with port 443 enroll but cannot renew. Point them at the device enrollment URL https://<host>:8445/.well-known/est. See The device enrollment URL.
  • EST: wrong passwords do not lock the account. They count against the source address instead: five in 15 minutes block that address from EST for 15 minutes. The web console keeps its own lockout; deny Device accounts the web console. See The enrollment guard.
  • EST: GET /api/v1/nac/pki/est-requests returns pending requests first, then newest. A script that needs the oldest first passes sort_by=created_at&sort_order=asc. An empty result has pages: 0.
  • EST: certs/est/crl.pem is gone. The gateway checks no CRL; revocation for renewal is decided by the api.
  • An Internal CA’s certificate field takes exactly one certificate, and a root that is not self-signed is refused; the API refuses a certificate field holding several certificates, with the count. The issuing chain has its own field. See Import an externally signed CA.
  • A CA imported earlier with “certificate + chain” in its certificate field keeps the extra certificates as EAP-TLS trust anchors until you take them out with Replace chain. A corporate SubCA imported earlier as a root shows Stored as a root, but issued by … and is repaired with Replace chain, keeping its key. See Replace chain.
  • A CA that its expiry disabled before the update is not enabled again by a renewal: enable it by hand.
  • Issued certificates from before the update show Source Unknown.
  • An Internal CA’s ocsp_responder_url is no longer accepted by the API (422) or returned. No UI field existed.

Going back to 1.3.3

The 1.3.3 api does not start on the 1.3.4 schema. Either restore the backup taken before the update, or roll the migration back first: in the 1.3.4 api container (on HA, on the leader) run

Terminal window
./taranac exec -T api python -m alembic downgrade 0030

then switch to the 1.3.3 images with ./taranac update --version 1.3.3 (a cluster member asks you to type DOWNGRADE; see Running it again, repairs and downgrades). The downgrade drops what only 1.3.4 stores: the issuing chains of Internal CAs signed outside Taranac, pending CA requests with their keys, SCEP’s settings, its RA certificates and the protocol of queued requests (a request queued over both EST and SCEP keeps its EST row), the EST/SCEP enrollment guard and its blocks, the reason and count of refused TACACS+ connections (the rows stay), the User-Name the NAS holds NAC sessions under, and the source and renewal link of issued certificates. Device accounts turn back into the Device binding if the list holds any account, Strict otherwise. The two CA columns 1.3.4 removed come back empty.

  • From before 1.3.3, on an HA cluster — update every member, the witness included. Each member renews its own etcd certificate in an etcd-agent container, on every database node and on the witness. Update in the usual order; until a member is updated the cluster raises cluster:etcd_agent_outdated:<member> (… needs an update to renew its etcd certificate). The witness pulls ghcr.io/taranaclabs/taranac/etcd-agent:<version>: on an air-gapped witness, copy that image to your mirror with a digest-preserving tool and add ETCD_AGENT_IMAGE_PREFIX=<your mirror path> to the witness .env before updating it. After the update the witness receives the cluster’s etcd CA key and keeps it in config/etcd-ca (0600) — protect the witness like a database node. A database node that joined under 1.3.1 or earlier has its etcd container recreated once by the update, which keeps quorum one node at a time. See etcd certificates and the etcd CA.
  • From before 1.3.3, with remote collectors. A collector on an older release gets no configuration-collection work (its SNMP and Web Probe work continues), shows Requires update and raises collector:update:<id>. After the core, run ./collector-join.sh --update on each collector host. See Keep the collector in step with its core.
  • From before 1.3.3, with standalone captive portals. Until a portal runs the matching configuration the core does not believe the guest address it passes on. Run ./taranac captive-portal update (or ./portal-join.sh --update) on each portal host. See How the core learns the guest’s address.
  • From before 1.3.3, with devices that enroll over EST. First enrollment is by password only: a device that obtained its first certificate by presenting a client certificate enrolls once by password (HTTP Basic), then renews; tls in the allowed methods means renewal (what tls means). Configure devices with the one device enrollment URL on the est-gateway, https://<host>:8445/.well-known/est (EST_GATEWAY_PORT), which serves both enrollment and renewal, and open that port in the firewall (the device enrollment URL, ports). The gateway presents its own certificate, signed by the Infrastructure CA — devices must trust that CA, or you put your own est-gateway/server.{crt,key} in place (its certificate) — and renews only certificates from this Taranac’s enabled internal CAs (what it trusts).
  • From before 1.3.3, with EAP-TLS for directory accounts. The user comes from the certificate only (UPN, then email, then CN; User-Name is never used). A directory account matches only its own email, or a UPN/CN in the LDAP configuration’s domain suffix; anything else is rejected before policy with tls_identity_domain_mismatch or tls_identity_no_domain. For OpenLDAP and FreeIPA configurations, fill in the domain suffix, or EAP-TLS matches their users by email only. Local accounts are unaffected. See EAP-TLS identity and domain suffix.
  • From before 1.3.3, with RADIUS device administration. The device is the packet’s source address; NAS-IP-Address decides nothing. Check devices behind NAT or sending from one shared address (they become one device and, without a Calling-Station-Id, share one brute-force ban), and devices that send RADIUS from an address other than the one configured on them (they match the device record for their source address — check their policy rules). Log entries written before the update still show NAS-IP-Address in the NAS IP column. The RADIUS configuration redeploys itself and the RADIUS service restarts once; if the RADIUS configuration auto-reload is paused or off, reload it by hand after updating — until then only rules with WHERE = Any match. See How a request finds its device.
  • From before 1.3.3, with TACACS+ MFA. A login that will be asked for an OTP is prompted Password+MFA: (the MFA password prompt). If you customised the default password prompt to mention the OTP, set the MFA password prompt too. If you exempt particular accounts (break-glass) from a rule that requires MFA for everyone, leave the MFA password prompt empty — it would pick those accounts out. See Password prompts and MFA.
  • From before 1.3.3, with TACACS+ over TLS (port 6049). The listener accepts clients: by default it trusts every enabled internal CA, including the CAs that issue EAP-TLS user and laptop certificates. To narrow it, set TACACS+ Settings → TLS Client Trust to Selected CAs. See TLS client trust.
  • From before 1.3.3, with TACACS+ command sets. The TACACS+ configuration is regenerated once after the update. A command set whose command holds characters other than letters, digits, _ . : - and single spaces, or whose pattern is not a one-line regular expression, must be corrected the next time it is saved. See Command sets.
  • From before 1.3.3, with an LDAP server that caps paged searches (OpenLDAP’s default is 500 entries). The sync fails with a clear error instead of syncing a truncated list. Raise the server’s limit for the bind account. See When the answer cannot be trusted.
  • From before 1.3.3, with Taranac MFA. TARANAC_MFA_API_KEY must be at least 32 characters, or the MFA service does not start. Installer-made installations already have one. If .env had none, the update generates it: enter the same key under Settings → MFA, or push MFA stops working. A shorter key must be replaced in both places. A standalone MFA service behind your own reverse proxy must list that proxy in TARANAC_MFA_TRUSTED_PROXIES. See Where it sits.
  • From before 1.3.3, with Telegram MFA and shared logs. Older api logs hold the Telegram bot token. If those logs were forwarded or attached to a support ticket, revoke the token in @BotFather and set the new one in Settings.
  • From before 1.3.3, with scripts that pass secrets on the command line. collector-join.sh --enrollment-token, portal-join.sh --token (and ./taranac captive-portal join --token) and ./taranac restore --master-key are refused. Give the token at the prompt, with --enrollment-token-file / --token-file (- reads stdin) or in TARANAC_ENROLLMENT_TOKEN / TARANAC_PORTAL_TOKEN; give the master key with --master-key-file or TARANAC_MASTER_KEY. See Run the join on the site host, Installing a portal and The guided restore.
  • From before 1.3.3, with API scripts that manage server certificates. POST …/server-cert/csr (TACACS+ and NAC) is gone: generate the key and CSR outside Taranac and upload the signed certificate. tls_cert / tls_key / tls_ca on /tacacs/settings are ignored without an error, so a script that still sends them changes nothing. See Server certificates.
  • From before 1.3.3, with custom email or SMS templates. Templates are checked when saved. One saved earlier that the template sandbox rejects makes sending that email or SMS fail until it is corrected — open and save each custom template. See Templates run in a sandbox.
  • From before 1.3.3, without APP_ENV in .env. A missing APP_ENV means production (it meant development), so the initial admin password is not shown on the login page. Set APP_ENV=development explicitly where that is wanted. See Installation.
  • From before 1.3.3, with NAC_COA_PORT in .env. The NAC container does not publish 3799/udp, and that line is ignored: CoA and Disconnect are sent from the backend to the switch. Nothing to open inbound. See Network ports.
  • From before 1.3.3, with protocol ports changed in .env. Devices keep the port they use: each port is now the same number on the host, inside the container and on the settings pages. To change one, edit .env and run ./taranac up -d; under HA keep them equal on every node. See Changing a protocol port.
  • From before 1.3.3 — the Docker network. ./taranac update replaces the stack’s network with 10.255.255.224/27 and recreates the containers on it; the data stays. The HA host-firewall rule from 10.255.255.240/28 stays as it is. Only rules of your own that name container addresses need a look. See The node’s own Docker network.
  • From before 1.3.3, with CRL checking. The CRL fetch interval is set on the Workers page in minutes. Installations on the former 6-hour default, or with fetching set to 0, move to 15 minutes; a custom interval is kept. The update restarts the NAC service once on installations with CRL checking on. See Fetching CRLs from a URL.
  • From before 1.3.3, with sponsored guest access. Sponsor links emailed before the update do not work; the guest requests access again. See The sponsor’s link.
  • From before 1.3.3, behind a reverse proxy or CDN of your own. Login, MFA and guest-registration rate limits apply per client address. With an extra proxy in front, raise TRUSTED_PROXY_HOPS by one per proxy, or every client shares the proxy’s limit. See What the schema documents.
  • From before 1.3.2, on an HA cluster — the first update. Start it the usual way, on any member. The older updater downloads the release and hands the run to the new one, so the order, the lock and the cluster table apply from the first node on. Two differences while members are still on the older release: the order is checked after the download and the confirmation prompt rather than before (answering the prompt on the wrong node still changes nothing), and the table shows the version of a member that has not run the new updater yet from its api’s heartbeat, marked * — the witness as ? no record until it is updated. The witness’s update adds a TARANAC_VERSION= line to its .env.
  • From before 1.3.2, on an HA cluster whose max_connections was never raised. The warning cluster:connection_ceiling appears right after the upgrade. It is the shortfall cluster verify already reported, not a new fault: raise the connection ceiling — replicas first, the leader last — and it clears within a minute of the last restart.
  • From before 1.3.2, with LDAP groups shared by two configurations or adopted from local groups. Each configuration gets its own directory group: the second one’s is imported as name (configuration) on its next sync. Add that group to the AAA and NAC rules that referenced the shared one, or its members stop matching them. Local groups an older sync had adopted keep following the directory, under the same id and in the same rules — except administrator groups, which are detached and stay local with the membership the last sync left. Check who is in them. See Which accounts and groups a sync may take.
  • From before 1.3.2, with roles that administer other users. A role changes only users and groups whose permissions it holds itself; only an administrator changes an administrator or an administrator group. Roles that edited richer accounts, handed out richer groups or granted permissions they lack get “forbidden” — see Who may change whom. MFA administration (reset, setup links, backup codes, removing someone else’s enrollment) needs mfa.manage: grant it to the roles that do this. Import in Update existing mode needs <entity>.edit as well as .create.
  • From before 1.3.2, with custom roles that export logs. Exporting the AAA logs needs the Export right of aaa_logs_view, and the audit log the Export right of audit_logs; without it the button is hidden. The seeded auditor and operator roles have it. Add it to any custom role that exports — see Filtering, searching and export.
  • From before 1.3.2, with a username held by more than one account (a local account and a directory account, or two directories). Every login refuses such a name — while more than one of its accounts is enabled — and the alert users:duplicate_usernames lists them. Remove or rename the extra accounts. See One username, one account.
  • From before 1.3.2, with a SIEM. The syslog HOSTNAME of every message is the node name, TARANAC_NODE_NAME in .env — the installer’s default is taranac-node-1 — instead of the api container’s id. Update SIEM rules keyed on the old value, and give each standalone server reporting to the same SIEM a distinctive name (edit .env, then ./taranac up -d). On a cluster, the SIEM receives one copy of each AAA record (each node sends the records of the requests it served), so drop any de-duplication of per-node copies; forwarding continues from where it stopped. See The HOSTNAME field.
  • From before 1.3.2, with no alert delivery set up. An install with neither SMTP plus alert recipients nor a syslog destination bound to System / alerts shows the warning Alert delivery is not configured until one is set up. See Alerts that reach nobody.
  • From before 1.3.2, with EAP-MD5 on 802.1X ports. Accept MAB over EAP-MD5 is on by default: any supplicant that picks EAP-MD5 with its own MAC as identity reaches the MAB rules, also on ports meant for 802.1X only. Turn it off under NAC Settings → MAC Authentication Bypass to keep EAP-MD5 closed. The NAC service restarts once after this update instead of reloading. See MAB carried as EAP-MD5.
  • Restoring an archive taken on 1.3.1 or earlier. Deploy the archive’s version as usual, but replace taranac-restore.sh in that install directory with the one from the current release before running ./taranac restore — give the archive’s absolute path. The script those versions shipped does not carry the keys and config/ across.
  • From before 1.3.1, on an HA cluster. PostgreSQL and etcd become pinned by digest, so the first update recreates each node’s etcd member once — harmless one machine at a time, which the updater enforces — and the witness’s update applies its changed compose file.
  • From 1.3.0, with a self-built taranac-mfa image (the workaround for the container that restarted in a loop on No module named 'psycopg'): go back to the official image. TARANAC_MFA_DATABASE_URL needs no change.
  • From before 1.3.0, with MikroTik access points. RouterOS wireless takes the VLAN from Mikrotik-Wireless-VLANID. A new installation’s mikrotik vendor dictionary entry sends it; an existing installation keeps its stored entry, so edit its VLAN mapping as described on MikroTik RouterOS.
  • An HA cluster whose archives predate 1.2.8. Those archives lack config/etcd-ca and cannot rebuild the cluster, and nothing flagged them as incomplete. After the upgrade, take a fresh backup and confirm it reports as complete.
  • A cluster upgraded from 1.2.7 or earlier carries one old unscoped backup:scheduled alert row besides the per-node ones. Close it by hand, once.
  • From before 1.2.6, with a PEAP rule catching machine certificates. An EAP-TLS exchange whose certificate has an empty subject (the identity in a subjectAltName, as Windows templates often issue) is reported as EAP-TLS, and policy compares the method exactly — so a PEAP rule written to catch those machines stops matching. Add a rule with Method = EAP-TLS for the same machines before upgrading, and remove the PEAP rule afterwards. See 802.1X authentication.
  • From before 1.2.6, with Huawei attributes added by hand. 802.1X replies resolve Huawei attribute names from Huawei’s own dictionary. An entry you wrote yourself — in the vendor dictionary or as an additional attribute on an authorization profile — against a name that exists only in the carrier dictionary is silently left out of the reply. Check such entries against Huawei VRP. The shipped downloadable-ACL mapping is corrected on upgrade unless you had replaced it.
  • From before 1.2.4, with a directory reached over LDAPS or StartTLS. TACACS+ and RADIUS never use an LDAP configuration’s EAP-TLS CA certificate for the LDAPS connection. Before updating, open Settings → Integrations → LDAP → your configuration → Connection: if Directory server CA certificate is empty and Validate TLS certificates is on, paste the directory server’s issuing CA there (Use the EAP-TLS CA copies it across when it is the same CA). Otherwise TACACS+ and RADIUS logins against that directory fail after the update.
  • From before 1.2.4, with a custom 802.1X server certificate. A server certificate already marked as deployed is delivered to the RADIUS service at start-up; after the update, confirm that In use by the service reads Applied (see PKI). The old nac_certs Docker volume is no longer attached and can be removed once the update works (docker volume rm <project>_nac_certs); it holds only the built-in key.
  • A cluster converted to HA before 1.2.3 has no PATRONI_REST_PASSWORD. The database image refuses to start without it, and ./taranac update provisions it on each node (see above), so upgrade such a cluster with the updater.
  • From before 1.2.2, with a custom report dataset on the config_rbac_roles template that shows is_system. That column is not declared by the template, so the dataset is refused on its next run. Re-pick its columns. No seeded dataset uses it.

Backup before you upgrade — and why version matters

Section titled “Backup before you upgrade — and why version matters”

Because a restore is gated on exact version and schema match, the safe recovery story is: a backup taken on version X restores cleanly onto a deployment running version X. After you upgrade to Y, an older X archive will hit the version/schema gate.

The decision guidance:

You have……and you want to restore ontoDo this
A backup from the running versionThe same versionRestore directly — all gates pass
A backup from an older versionA newer deploymentRe-deploy the older version, restore there, then upgrade forward (so migrations run normally); avoid --force. For an archive from 1.3.1 or earlier, see Upgrading from an older release
A backup whose version you can’t matchA newer deployment--force overrides the version/schema gate — only if you are confident the schema is compatible; not the default path

Because every archive is sealed with the master key, rotating it decides which archives stay readable. ./taranac rotate-key does it on a stopped system, in one database transaction, so “half the data on the old key” cannot happen:

Terminal window
./taranac rotate-key --confirm LOCAL-ACCESS-VERIFIED
./taranac rotate-key --confirm LOCAL-ACCESS-VERIFIED --new-key-file /path/to/key # supply your own

It asks for the current key first, and that is not a formality: if you cannot produce it, every archive you hold is already unreadable and rotating would only make that permanent. It then takes a backup, re-encrypts every stored secret, writes the new key to both places the runtime reads it from (.env and the daemons’ shared master_key volume), and restarts. The previous key is kept under config/retired-keys/ — keep it, because every archive taken before the rotation needs it (--master-key-file <file with the previous key> at restore).

The typed confirmation is the point. Rotation stops TACACS+ and RADIUS along with everything else, and the admin UI that would fix that is down too: while the window is open nobody can log in to switches, new 802.1X authentications fail, and if the local fallback accounts on your network gear are not known-good you can lock yourself out of your own network. Taranac cannot verify that for you.

Other flags: --current-key-file <path> reads the current key from a file instead of prompting, and --no-backup skips the pre-rotation backup.

Taranac ships a turnkey high-availability mode as a licensed Pro feature: the stack runs across two or more hosts with a Patroni-managed primary/replica PostgreSQL cluster (etcd coordination, optional witness for two-node setups), and automatic failover keeps authentication answering when a host goes down. All durable state lives in PostgreSQL, so replicating the database is what makes the system highly available. See High availability for the full architecture, prerequisites, enablement, and day-2 operations, and Licensing for how HA is unlocked.

Upgrades on a cluster have their own order — every replica, then the leader, then the witness — which ./taranac update works out and enforces itself. See Upgrading a cluster above.

Backups complement HA — they do not replace it. HA protects against a host or process failure; it does not protect against operator error, a bad upgrade, or corruption that replicates to every node. Whether or not you run HA, keep regular, off-box backups plus a securely stored master key as your recovery-of-last-resort. Note that a configuration backup deliberately excludes the HA cluster-runtime tables (see Configuration vs dynamic data above), so a restore never disturbs live cluster membership — the running node keeps its own roster.

CommandPurpose
./taranac restore <path on this host>Guided disaster recovery — the whole runbook, both phases; the archive is copied into the backups volume
./taranac restore <archive name>The same, for an archive already in the backups volume
./taranac restore … --master-key-file <path|->Open the archive with the key in that file (- reads stdin), written into .env. Or set TARANAC_MASTER_KEY in the environment. --master-key <KEY> is refused
./taranac restore … --yesSkip the phase-2 confirmation — required when there is no terminal
./taranac exec -T api python -m app.scripts.restore host <archive> --out <dir>Phase 1 by hand: verify key, extract env/ + compose/ + host_config/ + certs/
python -m app.scripts.restore host … --master-key -Decrypt with a key read from stdin instead of the loaded one
python -m app.scripts.restore check-key <archive>Check one key, read from stdin, against the archive: exit 0 it opens it, 10 not a key, 11 another key
python -m app.scripts.restore db <archive>Phase 2 by hand, in a one-off container with the api stopped: verify gates, wipe config tables, reload the dump, regenerate daemon configs
python -m app.scripts.restore db … --yesSkip the interactive confirmation
python -m app.scripts.restore db … --forceOverride the version/schema gate (the key gate still applies)
CommandPurpose
./taranac versionInstalled version + newer-release check; warns if a previous update stopped half-way
./taranac updateUpdate images and bundle files to the latest stable release
./taranac update --checkReport only, change nothing
./taranac update --version <X>Update to a named version
./taranac update --from <bundle>.tar.gzApply a local bundle (air-gapped); the version comes from the bundle
./taranac update --version <X> --repairRe-apply the version this node already runs
./taranac update --no-restartRefresh files and the pin; skip the pull and restart
./taranac update --keep-imagesKeep the superseded images
./taranac update --skip-image-verifyAccept images whose digests differ from IMAGES.lock (a re-pushing mirror)
./taranac update --skip-space-checkPull despite the free-disk estimate
./taranac update --allow-out-of-orderCluster repair only: skip the update order, never the lock; typed OUT-OF-ORDER or --confirm OUT-OF-ORDER
./taranac update --ipv6-own-address-okGo on although gateway-covering inventory entries stop matching IPv6 devices
./taranac update --yesAnswer the confirmation prompt
CommandPurpose
./taranac rotate-key --confirm LOCAL-ACCESS-VERIFIEDRotate the master key on a stopped system, in one transaction
./taranac rotate-key … --new-key-file <path>Use a key you supply instead of a generated one
./taranac rotate-key … --current-key-file <path>Read the current key from a file instead of the prompt
./taranac rotate-key … --no-backupSkip the backup taken before rotating

Backups emit BACKUP_CREATED, BACKUP_DELETED, BACKUP_DOWNLOADED, BACKUP_FAILED, BACKUP_SETTINGS_CHANGED and BACKUP_RESTORED — review them in the audit log. BACKUP_CREATED carries the archive’s included, missing, issues and a complete flag, so the audit log alone tells you whether an archive is usable.