Aller au contenu

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 twelve containers together: the API and the admin UI, the three protocol engines, the EST gateway, 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 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, the domain sidecar and the EST gateway are left out of the drawing — none of them 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 EST gateway, 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.

The internal network carries IPv4 and an IPv6 ULA. Only the containers that publish a port for clients — the edge, the three protocol daemons, the EST gateway, the MFA backend and the captive portal — get an IPv6 address on it, so each of them sees an IPv6 device or browser by its own address; the others are kept off IPv6, and names inside the stack always resolve to IPv4. See IPv6.

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. It also does the core’s own share of discovery — the core’s DHCP Probe listener, SNMP polling and Web Probe passes that are not handed to a collector — and derives every endpoint’s device profile. No published port.postgres, taranac-renderer, all engines (via shared volumes)
tacacsTACACS+ device administration. Runs tac_plus-ng, built from a pinned, tested revision of the TaranacLabs fork, 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.10 (built from the TaranacLabs fork, pinned by commit) with a Python handler that does the full policy evaluation. Virtual server aaa, on 1812/1813 UDP. A supervisor in the container applies a new configuration by starting a new radiusd on the same sockets, so a change is applied without a restart and without losing a request.postgres, shared config volume, winbind
nac802.1X / port access control. A separate FreeRADIUS 3.2.10 container with its own handler, EAP stack, and policy engine. Virtual server nac, on 1814/1815 UDP. It listens for no CoA: Change-of-Authorization and Disconnect requests are sent by the api to the switch’s own CoA port. Its configuration is applied the same way as for radius, without a restart.postgres, shared config volume, winbind
est-gatewayDevice certificate enrollment (nginx). Mutual-TLS listener for EST and SCEP on host port 8445, and plain-HTTP SCEP on 8446. Serves only the EST and SCEP paths and proxies them to the node’s own api. Device certificate renewal over EST works only here; it trusts only enabled internal CAs (and the chain above them) and leaves revocation to the api.api
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. 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. A connection left idle inside an open transaction for an hour is closed by the server (DB_IDLE_IN_TRANSACTION_TIMEOUT_MS, 0 disables), so a stuck session cannot hold its locks indefinitely.— (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
collectorWorks a site on the core’s behalf: collects device configurations for the Configuration Tracker, polls SNMP, runs Web Probe, can listen for DHCP and scans. Dials out to the core, claims jobs, runs them locally, and posts results back through a durable outbox. No database, no API, no port toward the core — every connection to the core is one it opens.Collectors
standalone captive portalA captive portal serving its own segment with its own DHCP and DNS, reaching the core only for the guest API and a heartbeat. It can open access for signed-in guests itself.Standalone 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. The API validates the configuration before it deploys it, and signals the container only when something the daemon must re-read has changed:

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

The reload is deliberately careful. In the tacacs container a watcher sends tac_plus-ng a HUP; open sockets stay open. In radius and nac, a supervisor applies every new configuration by handover: a new FreeRADIUS process starts beside the running one on the same sockets, takes the new requests once it is ready, and the old one finishes what it has and exits. The port never closes, the device list included. A configuration that cannot start is not applied and raises an alert, and a process that fails after taking over is rolled back (see Applying a configuration). 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 ↔ EST gateway) 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.

Two listeners that network devices verify by address carry certificates from the Infrastructure CA instead: the TACACS+ over TLS listener and the EST gateway. Taranac generates them with the node’s addresses (TARANAC_NODE_IPS, plus TARANAC_DOMAIN and NODE_ADDRESS when set) and reissues them when those change or before they expire; one you deploy yourself is never replaced.

All four nginx listeners in the stack — the public edge, the admin UI, the captive portal, and the EST gateway — run a current, supported nginx branch, and none of them announces its version in the Server header or on error pages, so a port scanner is not handed 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 the packet's source address (NAS-IP-Address is
not used); 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

Devices behind one NAT address are therefore one device to Taranac. See How a request finds its device.

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
Discovery sources, profile tree, profiling rulesNAC → Discovery SourcesDevice Profiling, which names each endpoint for endpoint-group classification
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, or a standalone portal opens access itself

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, and an etcd-agent that renews that member’s etcd certificate before it expires and carries out an etcd CA rotation, with no restart. 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 its etcd-agent and nothing else — no Postgres, no Taranac stack, no master key. It does hold the etcd CA key, so protect it like a database node. 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. On Postgres’ own default ceiling of 100 connections, three API engines alone would ask for 90 of them before a single daemon connects, so the ceiling is budgeted from what the code actually asks for, at 300. It is applied when a cluster is first built, so a cluster built with a lower value keeps it through every update: 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.