Ir al contenido

Architecture

Taranac is not a single daemon — it is a small set of containers that each own one job (the API, the protocol engines, the database, the edge) and share a single source of truth in PostgreSQL. You manage everything from one admin UI; the protocol containers read their working configuration from a shared volume and the database, never from hand-edited files. This page explains what the moving parts are, how they relate, and what actually happens when a switch, router, or laptop asks Taranac to authenticate.

The whole stack runs under Docker Compose. In the default single-host deployment there is no separate “controller” and “node” tier — one compose file brings up all eleven containers together: the API and the admin UI, the three protocol engines, the captive portal, the MFA backend, the report renderer, the domain sidecar, PostgreSQL, and the public edge proxy. Under the optional high-availability topology this changes: the database becomes a Patroni-managed primary/replica cluster across two or more hosts, layered on by an HA overlay compose file — see the HA section below. Two further pieces deploy away from the stack rather than in it — a Configuration Tracker collector and a standalone captive portal — and are covered under Modules.

Taranac dashboard The dashboard surfaces live activity from every container — AAA and NAC authentications, sessions, and certificate expiry — in one place.

One Compose host, simplified to the request path: the edge proxy fronts the UI and API; three independent engines and the side services all read from a single PostgreSQL source of truth. The report renderer and the domain sidecar are left out of the drawing — neither sits on an authentication path. The full list is in the table below.

Each box below is a separate container. They communicate over an internal Docker network, and only some of them publish a port on the host: the edge proxy, the three protocol listeners, the MFA backend, and the embedded captive portal. The frontend, the API, the report renderer, the domain sidecar, and PostgreSQL publish nothing at all — they are reachable only from inside the stack.

ContainerWhat it doesTalks to
edgePublic reverse proxy (nginx). Holds 80/443, terminates the TLS your browser and phones see, and proxies inward to the UI, the API, and optionally the MFA server.frontend, api, taranac-mfa
frontendThe admin UI (React, served by nginx). Everything an operator configures happens here. No published port — you reach it through the edge.api
apiThe backend (FastAPI). The brain of the system: stores all configuration, renders engine configs, owns the identity store, exposes the REST API. No published port.postgres, taranac-renderer, all engines (via shared volumes)
tacacsTACACS+ device administration. Runs tac_plus-ng with a Python MAVIS handler for authentication, MFA, and enable. Listens on 49, and on 6049 for TACACS+ over TLS.postgres, shared config volume
radiusRADIUS device administration (AAA). FreeRADIUS 3.2.8 with a Python handler that does the full policy evaluation. Virtual server aaa, on 1812/1813 UDP.postgres, shared config volume, winbind
nac802.1X / port access control. A separate FreeRADIUS 3.2.8 container with its own handler, EAP stack, and policy engine. Virtual server nac, on 1814/1815 UDP, with 3799 UDP mapped for Change-of-Authorization (CoA).postgres, shared config volume, winbind
captive-portalGuest, BYOD, self-registration, and AUP flows (nginx + a small Preact app). Can run embedded here, or stand alone in a DMZ.api (over a portal token)
taranac-mfaPush MFA backend (FastAPI) on 8443. Delivers push challenges via FCM with TOTP/OTP fallback, and keeps its own taranac_mfa database.postgres, api
taranac-rendererRenders report exports to PDF. Added with Reports in 1.2.0; internal HTTPS only, and the API is its only client.— (the api calls it)
winbindThe node’s Active Directory machine account. radius and nac verify MS-CHAPv2 for domain-joined accounts through it. A directory that publishes the NT hash itself — FreeIPA — is verified without it. No port and no database of its own; with no domain configured it starts, reports “not joined”, and does nothing.radius, nac (over a socket volume)
postgresPostgreSQL 16 — the single shared store for all configuration, identities, logs, and runtime session state.— (everything talks to it)

Both protocols cover “who can manage the network gear”, but they do policy differently, and this shapes what you can configure:

AspectTACACS+RADIUS (AAA)
Policy evaluationDone by tac_plus-ng from generated configDone entirely by the Python handler
What the auth handler decidesAuthentication + WHO matching onlyAuthentication + full policy evaluation
Enable passwordYes — with an inheritance chainNo concept of enable
Per-command authorizationYes — Command SetsNo concept of it
Console / port conditionsYesIgnored (TACACS+-only)

You do not need to know this to use the UI, but it explains why Command Sets and enable settings appear only under TACACS+, and why a RADIUS policy is purely “match conditions → assign a profile.”

Two components are deliberately not part of the compose file above, because their whole point is to run somewhere the core cannot reach — a segmented network, a branch DMZ. Taranac calls them modules, and each is its own one-container deployment on its own host:

ModuleWhat it isDetail
collectorA Configuration Tracker collector. Dials out to the core, claims collection jobs, runs them locally against the site’s devices, and posts results back through a durable outbox. No database, no API, no published port — every connection it makes is outbound.Collectors
standalone captive portalA captive portal serving a Registration VLAN on its own, with its own DHCP and DNS, proxying only the guest login back to the core.Captive portal

Both are attached with one command that the core prints for you — no CA file to carry, no compose file to edit. Trust in the core defaults to pin: the certificate presented on first contact is the only one accepted afterwards, the same bargain the product makes with a device’s SSH host key. Verifying against a CA bundle instead stays available. A module can be asked what it is doing, told to re-pin after a certificate replacement, and uninstalled cleanly.

Under HA there is no single client-facing address shared by the nodes, so every collector and portal is bound to one specific cluster node and records which — an operator whose collector goes quiet can see whose outage explains it. A module also learns from the core which version it should be running, since it must match its own core rather than the newest release.

The virtual appliance can be installed as a module rather than a server: its first-boot wizard asks whether the box is Taranac itself, or a Collector, Captive Portal, or HA Witness.

Two mechanisms tie the containers into one product.

1. PostgreSQL is the single source of truth. Users, groups, devices, policies, profiles, certificates, logs, and live session state all live in one database. The protocol engines do not own any of this — they read it. Each engine keeps small in-memory caches (users, devices, policy rules, profiles) that refresh from the database roughly every 30 seconds, so a change you make in the UI takes effect within seconds without a restart.

2. Generated config on a shared volume. Some settings — chiefly the list of network devices and their shared secrets — can’t be served from a cache at request time; the protocol daemons need them in their own config files. When you save a device or change a setting, the API renders the engine configuration and writes it to a Docker volume that the relevant container mounts. A watcher inside the container then validates and reloads:

Admin UI → API renders config → writes to shared volume → watcher validates → reload signal → tac_plus-ng / FreeRADIUS picks it up

The reload is deliberately careful. For FreeRADIUS, if only modules or virtual servers changed, the engine is sent a hot HUP with no downtime; if the device list (clients.conf) changed, the container does a full restart, because FreeRADIUS 3.x cannot add or remove clients from its in-memory table on a HUP. You never edit these files by hand — the UI owns them.

The same mechanism carries more than engine config. The domain sidecar’s smb.conf and krb5.conf are generated by the API exactly like a daemon’s config, and a domain-join request is handed over as a file on a shared runtime volume that the sidecar deletes the moment it reads it — which an HTTP request body could not be.

Server-to-server traffic (API ↔ engines ↔ MFA ↔ renderer) runs over HTTPS using a self-signed internal CA that Taranac generates automatically on first start. The CA and per-service certificates are distributed through a shared taranac_certs volume; only the containers that accept HTTPS get a server certificate (the API, the MFA backend, the report renderer, the captive portal, and the NAC engine’s EAP listener), while the rest get the CA alone so they can verify what they call. This is entirely separate from the public certificate the operator supplies for the edge proxy — the internal CA is never exposed to browsers.

All three nginx listeners in the stack — the public edge, the admin UI, and the captive portal — were moved onto a current, still-supported nginx branch in 1.2.3; the branch they had been pinned to had stopped receiving fixes. Each of them now also suppresses the version banner it used to announce in the Server header and on error pages, which had been handing a port scanner a ready-made list of applicable advisories.

The request path depends on which engine the network device talks to.

Network device → tacacs (tac_plus-ng) → MAVIS handler:
1. Look up the user (local or LDAP) and verify the password
2. Apply MFA if the policy requires it
3. Match WHO / WHEN policy conditions → return matching rule profiles
→ tac_plus-ng applies first-match-wins, WHERE/SOURCE conditions, and per-command rules
→ device gets shell, privilege level, and allowed commands
Network device → radius (FreeRADIUS, VS aaa) → Python handler:
1. Identify the device by NAS IP; look up the user
2. Evaluate policy (WHO + WHERE + SOURCE + WHEN), first-match-wins
3. Reject, authenticate-only, or render an authorization profile
4. Apply MFA challenge if required
→ Access-Accept with the profile's reply attributes

Endpoint joining a port over 802.1X / MAB (NAC)

Section titled “Endpoint joining a port over 802.1X / MAB (NAC)”
Endpoint → switch port → nac (FreeRADIUS, VS nac) → NAC handler:
• 802.1X (PEAP / EAP-TLS / EAP-TTLS): negotiate EAP, verify the user or
certificate, enrich the endpoint
• MAB (no supplicant): use the MAC as identity, look up the endpoint,
auto-discover and classify if unknown
→ evaluate NAC policy (HOW / WHO / WHAT / WHERE / WHEN), first-match-wins
→ render the authorization profile into vendor-correct RADIUS attributes
→ Access-Accept with VLAN / dACL / redirect, or Access-Reject
→ accounting tracks the live session; CoA can re-authorize it later

The same authorization profile — say “Printers, VLAN 300, restricted ACL” — works across every switch vendor. The NAC engine consults a vendor dictionary to translate that intent into the exact RADIUS attributes a Cisco, Arista, or Aruba switch expects.

NAC policy table NAC policy rules evaluate first-match-wins; each rule’s conditions map to an authorization profile or a reject.

Almost every object an engine evaluates is a shared record you manage once:

ObjectConfigured underUsed by
Users, GroupsIdentityTACACS+, RADIUS, NAC (PEAP/TTLS)
Network Devices, Device GroupsNetworkAll three engines (as the NAS/authenticator, with shared secrets)
LDAP directoriesIdentityAll engines (external auth, EAP-TTLS bind, NAC computer sync)
Time RangesAAAWHEN condition in every policy
TACACS+ / RADIUS / NAC profilesAAA, NACThe matching engine’s authorization result
Endpoints, Endpoint GroupsNACNAC policy WHAT condition
Internal PKI, Trusted CAsNACEAP-TLS certificate validation
Tracked configs, recipes, scrub sets, device credentialsConfiguration TrackerCollectors — embedded or standalone — capturing versioned snapshots on a schedule
Report templates, dataset presets, schedulesReportsThe API, and taranac-renderer when the export is a PDF

Because the identity store and the device list are shared, the same user or the same switch is defined exactly once and is consistent across TACACS+, RADIUS, and NAC — and the same device record a policy authorizes against is the one the Configuration Tracker backs up and a report counts.

Network devices Network devices are defined once and reused by every protocol engine, with shared-secret inheritance from device groups.

You want to control…UseWhy
CLI / admin access to switches and routers, with per-command control and enableTACACS+Only TACACS+ offers command authorization, enable, and console/port conditions
CLI / admin access where the gear only speaks RADIUSRADIUS (AAA)RADIUS device administration with profile-based authorization
Which devices may connect to a switch port (laptops, printers, IoT, phones)NAC (802.1X / MAB)Port-level access with VLAN/ACL assignment and live session control
Onboarding guests, BYOD, or self-registrationCaptive portal + NACThe portal registers the endpoint; NAC re-authorizes it via CoA

A single-host Compose deployment is the default topology. For deployments where authentication must survive a host failure, Taranac ships a turnkey high-availability mode as a licensed Pro feature. Because all durable state lives in PostgreSQL, HA is built around replicating the database — but the shape of the result is what matters here.

Every node runs the full stack. The whole set of containers comes up on every database node — UI, API, and the TACACS+/RADIUS/NAC daemons alike. What differs is the database underneath them: one node’s PostgreSQL is the read-write primary, the others are read-only replicas streaming from it. An HA overlay compose file layers this on: it replaces the plain postgres service with a Patroni-supervised one and adds an etcd member alongside it, one per database node. Patroni supervises Postgres and decides promotion; etcd is the agreement layer that records who is primary. Alongside them sits a witness: a separate, tiny host that runs etcd and nothing else — no Postgres, no Taranac stack, no master key. It exists because two voting members cannot break a tie, so a two-node cluster could never safely fail over without it. You declare one whatever your node count — the tooling refuses a ring of fewer than three etcd voting members, and refuses to co-locate the witness with a database node — though above two nodes it is the build sequence that needs it rather than the arithmetic.

The daemons read node-local. This is the point of the whole design, and it is why HA buys more than a warm spare. The API and the daemons share one cluster-wide list of every node’s database endpoint; writes are tagged target_session_attrs=read-write and therefore land on whichever node is currently primary, wherever it is, while daemon reads stay on the node’s own replica and are served from there plus the in-memory caches described above. So the AAA/NAC data plane does not depend on the primary being up: point your network devices at every node as an AAA server and they keep authenticating straight through a failover, with only configuration writes pausing for the seconds a promotion takes. This is the sense in which the single-host “no controller/node tier” statement is qualified — the leader/replica split is a database role, not a service tier.

The pieces size each other. Under HA every node’s writes route to the one primary, so that primary carries the whole cluster’s write side on top of its own reads. Postgres’ own default ceiling of 100 connections was in force until 1.2.8 — and three API engines alone ask for 90 of them before a single daemon connects. The ceiling is now budgeted from what the code actually asks for, at 300. It is applied when a cluster is first built, which means an existing cluster does not get it and cannot: see Raise the connection ceiling for the ordered procedure, and note that cluster verify reports the value each node is running with rather than what its configuration says.

You do not operate Patroni or etcd directly. ./taranac cluster init, add-witness and add-node are the whole interface: each prints the single command to run on the machine being added, and the master key and etcd CA travel encrypted over a one-shot join channel on port 9443 — open only while a join is running, pinned by a fingerprint the primary prints on its console.

HA is license-gated: the cluster refuses to issue join tokens beyond the licensed node count unless a valid Pro license is active. See Licensing for how the offline license enables it, and High availability for the full architecture, prerequisites, enablement, day-2 operations, and the ports the interconnect must be firewalled to.