Pular para o conteúdo

Taranac MFA

Taranac MFA is the second factor the project builds itself, rather than renting. It is two halves: a service you run — its own container, its own database, its own encryption key — and a phone app your users install. Between them there is no third party holding anything: no publisher account, no cloud tenant, no shared secret to leak.

It is one of five MFA providers Taranac supports. This page is about that one, end to end, because it is the only one where both sides are ours and the whole path can be described honestly.

Two things make it worth the trouble of hosting:

  • It survives losing Google. Push arrives over Firebase when Firebase is available, over the app’s own polling when it is not, and if there is no network at all the same enrollment still produces a six-digit offline code. A phone with no Google services is a first-class citizen, not a degraded one.
  • Approvals are signed in both directions. The challenge is signed by the server; the answer is signed by the phone, with a key generated on the device that never leaves it. Neither side holds a secret the other could lose.

The service is deliberately shaped so it can live in a DMZ. It exposes one inbound port — 8443, HTTPS — and opens no connection back into your platform. The Taranac core is always the client; the phone reaches the service from the internet. Nothing about that arrangement requires a hole pointing inward.

Every arrow points away from the trusted network. The core calls out to the MFA service; the phone calls in to the same port. The service never initiates anything toward the platform.

What the service keeps to itself:

Its own databasetaranac_mfa — four tables: tokens, token info, owners, challenges
Its own encryption key96 bytes, three AES-256 keys. Deliberately not the platform’s MASTER_KEY — losing one does not expose the other
Only UUIDsIt knows users by identifier. Usernames reach it only as the cosmetic label printed on a QR code

A Flutter application, version 1.0.0+6, shipping on Android (7.0 and newer). The UI is translated into seven languages — English, German, Spanish, French, Portuguese, Russian and Ukrainian — with full parity across all of them.

The app's account list: four accounts, three marked TOTP and one marked Push
Accounts. A Taranac account carries a Push badge and shows which server it belongs to.
An account's detail screen showing a six-digit fallback code and a countdown ring
The same account, offline. The app calls it a fallback code — one enrollment, both ways in.
Settings: app protection, biometrics, lock timeout, cloud backup and language
Settings. App lock and biometrics are opt-in; encrypted backup to the user’s own platform account is on by default.

A single QR code enrolls both a push identity and a TOTP secret. That is the design decision that makes the rest of it work: the user never sets up a “backup method”, because the backup was always there.

The server picks between Firebase and polling per request, from whether Firebase is actually working and whether that token has an FCM token at all. The user is never asked, and the offline code sits underneath both.

Three consequences worth stating plainly:

  • A phone with no Google services enrolls poll-only and works. It is not refused, and the user is not told to go and install anything.
  • If Firebase stops working server-side, everyone falls back to polling — the signed challenge payload is stored for every request regardless of channel, so there is nothing to switch on.
  • Polling runs only while the app is open, every 5 seconds. That is the honest cost of the tier: a poll-only user has to open the app when they log in.

The challenge tells the user what they are approving — which service, which device, which account, from which address — because “approve this login?” with no context is how people are trained to tap yes.

A push challenge showing TACACS+, the target device, the user and the source address, with a countdown
A TACACS+ login to a switch. The icon and label come from the service that raised it — TACACS+, RADIUS, web login, 802.1X or captive portal.
A push challenge with no context fields, showing only the request serial and a countdown
The same screen when the raising service sends no context. The serial is always shown, so a user can match the prompt to the request.

The countdown is real: a challenge lives 120 seconds. When it runs out the app stops offering the buttons and says so, and the server rejects a late answer with a 410. Re-triggering inside 10 seconds reuses the same challenge rather than firing a second notification at the user.

Two signatures, opposite directions, no shared secret. The phone’s private key is generated on the device after the QR is scanned and never leaves it.
  • The QR carries no secret. It carries a URL, a serial and a one-time enrollment credential. Photographing it off someone’s screen after they have enrolled yields a spent credential.
  • The phone generates RSA-4096 on the device and sends only the public half. The server keeps that; it never has the private key to lose. The server sends its own public key back, so the phone can reject a challenge that is not really from the server.
  • The URL is inside the server’s signature, so a man-in-the-middle cannot redirect an approval somewhere else without breaking it.
  • Every other operation is signed too — liveness checks, FCM token updates, deletion — each over a fresh timestamp, so an old request cannot be replayed.

Rate limits are per token serial, not per address, so one user cannot exhaust another’s budget; the ban that follows repeated bad signatures is keyed on the client address instead, precisely because the serial is not a secret and keying the ban on it would let anyone lock out a stranger’s phone.

MFA is not bolted onto one entry point. Each path has its own shape, and the differences are protocol facts rather than choices:

PathHow the second factor is asked forNotes
Web admin UIPush or code, in a dedicated stepThe login returns no session tokens until the factor verifies
TACACS+Code appended to the password, or pushTACACS+ has no way to ask a second question — with a combined enrollment, Taranac tries the appended code first, then falls back to push
RADIUS — PAPCode appended to the password, or push — and it can also ask with a real Access-ChallengeThe full path: every mode is available here
RADIUS — MS-CHAPv2Push onlyThe packet carries a hash, not a password, so there is nothing to append a code to. This is the transport that MikroTik administrators arrive on
RADIUS — CHAPNot supportedCHAP needs a cleartext password on the server; Taranac stores argon2id hashes
802.1X / captive portalPush onlyPEAP and EAP-TTLS cannot prompt, EAP-TLS has no password, MAB has no user at all

Requiring MFA is layered, and all three layers must agree. A user’s enrollment decides which factor; a group’s mfa_required flag decides whether; and an AAA policy rule or NAC rule decides where. Each daemon then has a master switch above all of that.

That last one is the trap worth knowing: if a rule requires MFA but the daemon’s master switch is off, the login succeeds without a second factor and the daemon logs a warning. It is the one place in the design that fails open, and it fails open on configuration, never on an outage.

Everything else fails closed. If the MFA service is unreachable, if a challenge cannot be created, if a poll times out — TACACS+, RADIUS, 802.1X and the web UI all reject. There is no “let them in, the service is down” path.

Health. The service reports db, crypto and firebase separately, and Firebase being absent is not unhealthy — poll-only is a fully supported state. A backend watchdog checks every enabled provider every 90 seconds and raises a mfa:<provider> alert when one degrades.

Under HA, the MFA service runs on every node and its encryption key is part of the cluster secret set, so a token enrolled against one node verifies on all of them. One caveat to handle deliberately: the push registration URL is still node-scoped. Point it at a cluster name or VIP rather than a single node’s address, or enrollment links will stop working when that node does.

The app is not on Google Play. That is a funding matter, and it is worth being straight about it: publishing costs money, Taranac is free and self-hosted, and the project does not currently have it. Everything else for the listing is finished — the store texts in seven languages, the privacy policy, the data-safety answers, the screenshots — and it will go up when the account can be paid for. Until then the build is distributed as an APK you install directly.

Download the APK 72.8 MB
File
taranac-mfa-1.0.0.apk · Android 7.0 and newer
SHA-256
b06994523e147fbc8119e0291b2b711173c163d249cd334ba1c1c22f74f0b28d
Signing certificate
1480d2345359c71d5b3e1b444bf0eceb8188dbc270f20a9bb6a8e18e70b338d2

Check both numbers before you install it. The first says you got the file intact; the second says this project built it. A matching checksum on a file signed by somebody else is worth nothing, which is why the certificate digest is published too:

Terminal window
sha256sum taranac-mfa-1.0.0.apk # → the SHA-256 above
apksigner verify --print-certs taranac-mfa-1.0.0.apk # → the certificate digest above

The checksum is also in SHA256SUMS alongside the appliance images.

Two consequences of installing outside a store, and neither is hidden:

  • A directly-installed APK does not update itself. There is no store channel to push a new version down, so come back here when a release is announced.
  • Android will ask you to allow installing from this source, once, for the browser or file manager you use. That prompt is the platform doing its job; the checks above are how you make it a safe yes.

If you would like this to change sooner, supporting the project is what pays for the listing.

  • iOS is not available. Same codebase, untested, unsigned, unreleased.
  • Polling only runs while the app is open. On a phone without Google services, a login means opening the app.
  • The app lock is a convenience, not a vault. A PIN protects against someone picking up an unlocked phone; it is not resistance against an attacker who can extract the phone’s storage.
  • Secrets are held by the platform’s secure storage — encrypted under a key held in the Android Keystore or the iOS Keychain. That is strong, and it is not the same claim as the seeds themselves living inside a hardware element.
  • Encrypted cloud backup is on by default — to the user’s own Google or Apple account, never to us — which is what stops a lost phone becoming a lost authenticator. Users who would rather not can turn it off in Settings.