Pular para o conteúdo

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 its cluster ordering rule, 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.

Until 1.2.8 the answer was “nobody knows until recovery”. The manifest now carries the archive’s own verdict, in plaintext beside the payload:

  • 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 and files are walked in sorted order, so an unreadable 0600 private key used to leave its ca.crt/tls.crt siblings inside an archive that nothing claimed — restoring a certificate with no private key, and looking like it had worked. A tree is now 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 vendor 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 for new installs since 1.2.6 — 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 no longer kills the whole archive either. Before 1.2.3 the installer wrote .env with the operator’s own permissions while the api container runs as a different user, and the file is mounted read-only — so every backup, scheduled and manual, failed outright. 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.

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. Put the archive back where the api can read it, then run ./taranac restore.

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 (since 1.1.1): on the container’s writable layer they were silently lost the moment an update recreated the api. Its ownership is set at boot, so creating the first backup on a brand-new install works (it used to fail with a 500). 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/'

Recovery reads the archive from that same volume, so on a rebuilt host the archive has to be copied into it (the reverse of the command above) before ./taranac restore can open it. The volume name is prefixed by your Compose project — docker volume ls | grep backups if you renamed the install directory.

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

Terminal window
./taranac restore taranac-backup-YYYYMMDDTHHMMSSZ.tar
./taranac restore <archive>.tar --master-key <KEY> # this install runs a different key
./taranac restore <archive>.tar --yes # skip the phase-2 confirmation

In order: verify the master key against the archive; extract the host material; overlay the five recovery keys onto this install’s .env (keeping the previous file as .env.pre-restore); clear the daemons’ master_key volume so their entrypoints adopt the restored key; copy the certificates into the taranac_certs volume and the operator’s files back into ./config; recreate only the services whose secrets changed; then load the database dump and restart everything. Under HA it detects the Patroni overlay and merges it — recovery runs on the leader, and the other nodes rejoin afterwards.

The guided script exists because recovery has to edit host files and recreate containers; if you need to drive it yourself, these are the same steps.

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 an explicit --master-key <KEY>), decrypts the payload and reports which of env/, compose/, host_config/ and certs/ the archive actually held.

Between the phases — take the five recovery keys out of restore/env/env into .env (do not copy the file over), then let the daemons adopt the restored key and bring the stack back:

Terminal window
docker run --rm -v taranac_master_key:/d busybox rm -f /d/master_key
./taranac exec -T api sh -c 'cp -a /app/backups/restore/certs/. /opt/taranac/certs/'
./taranac up -d --force-recreate

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.

Phase 2 — the database:

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

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) 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. A full stack restart afterwards is recommended so every service re-reads the restored certificates and configuration.

Add --yes to skip the interactive confirmation (for automation).

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, verifies its checksum, replaces every framework file (leaving your .env and config/ alone — those are operator-owned and never overwritten), pins TARANAC_VERSION in .env, then pulls the images 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 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)
./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 --allow-out-of-orderOverride the cluster-readiness refusal (see below)
./taranac update --yes (-y)Skip the confirmation prompt

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. Before 1.2.4 it offered to continue with the images alone, which reported success while the part that mattered never arrived.
  • An abort names the phase it stopped inreplacing the bundle files, writing the version pin, or pulling images and restarting the stack — and points at the backup of the previous bundle it took before the swap (.taranac-backup-<timestamp>/ inside the install directory). Re-running ./taranac update completes it; restoring from that directory goes back. Before 1.2.8 an abort in a reporting step ended with a green ✓ Bundle files updated. as the last thing printed, before the version was pinned — which is how a customer watched an upgrade appear to work and stayed where they were.
  • ./taranac version catches a half-finished upgrade. The bundle’s own VERSION file and .env’s TARANAC_VERSION pin are two different questions, and they agree only when the last upgrade ran to the end. Where they differ you get both numbers and the one command that finishes it. (Since 1.2.6 the pin is also read forgivingly, so a .env with Windows line endings or a quoted value no longer reports an update for the release you are already running.)
  • 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. Before 1.2.6 every superseded image stayed on disk forever: 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). Before 1.2.8 the list never shrank — a host going 1.2.6 → 1.2.7 was read 13 settings, every one of which had shipped by 1.2.3.
  • 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 now writes through the existing file rather than replacing it, so the file keeps its inode — and with it the group — instead of losing it on every update.
  • A witness is not a Taranac node. ./taranac update on an HA witness refreshes the bundle’s files (witness-join.sh, the witness compose file, diagnose.sh, the HA runbook) and stops: it neither reads nor edits the witness .env, and it starts nothing. Before 1.2.7 it ran the node’s update there and would have started PostgreSQL, the API and the AAA daemons on the host whose whole purpose is to be cheap to lose.
  • A collector’s version follows its core, through ./collector-join.sh, never from here.

Upgrading a cluster: replicas first, primary last

Section titled “Upgrading a cluster: replicas first, primary last”

On an HA cluster this is not advice, it is the only order that works: update every replica first, and the primary last.

Recreating the primary’s container triggers a failover. If a node still running the old code takes the lead, the upgraded machine becomes a replica running new code — and a replica does not create the schema, while the old leader has no such migration to make. Both then wait forever, and handing leadership back does not help: the schema check is exact in both directions.

Since 1.2.8 the primary refuses instead of warning, and it refuses before anything is written — before the file swap, before the version pin. It reads every member’s reported version from the replicated roster, waits briefly for a node that was just updated to re-publish (a node reports every 30 seconds), then names the members that are behind and the ones it could not confirm. The old guard printed “update the replicas first” and carried on after the bundle and the pin were already written, so obeying it left a half-updated node — and an order warning is powerless the moment an earlier step lies about having succeeded, which is exactly what one cluster met on 15 August 2026.

If you are certain — for instance because you are repairing exactly that state — ./taranac update --allow-out-of-order continues anyway.

A replica whose API is waiting for a schema head only the leader can create now says so in those words: which way the two revisions differ, that only the leader can create the schema, and that the order is replicas first and the primary last. It used to restate the two revision hashes, the one fact the operator was already looking at.

Two more cluster-specific points:

  • A cluster converted before 1.2.3 carries no PATRONI_REST_PASSWORD, and since 1.2.3 the database image refuses to start without one — so an update handed it an image that would not run, surfacing three steps later as a container in a restart loop. The updater now provisions the key, and 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.
  • The cluster CLI itself (./taranac cluster status, switchover, add-node, …) is documented on High availability. Confirm every member is on the target version there before you touch the primary.

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
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 (1.2.2) 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 <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 rule — replicas first, primary last, enforced by a refusal since 1.2.8. 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 <archive>.tarGuided disaster recovery — the whole runbook, both phases
./taranac restore … --master-key <KEY>Open the archive with an explicit key
./taranac restore … --yesSkip the phase-2 confirmation
./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 <KEY>Decrypt with an explicit key instead of the loaded one
python -m app.scripts.restore db <archive>Phase 2 by hand: 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 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)
./taranac update --no-restartRefresh files and the pin; skip the pull and restart
./taranac update --keep-imagesKeep the superseded images
./taranac update --allow-out-of-orderContinue past the cluster-readiness refusal
./taranac update --yesSkip 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.