Organisations: Machines by Name
An organisation lets your machines trust each other by name. You hold one root key; an issuer on a machine of yours gives each machine a short-lived certificate for its name and groups. Clients dial web-1.db/pg, services admit the group db, and a revocation or a lost root reaches every machine. The relay carries all of it and can forge none of it.
Why organisations
End-to-end encryption needs each end to know the other's key. Doing that by hand means a pin on every client for every service, and a list of client keys on every service: for N machines and M services, about N × M values to copy and keep current. That stops scaling at a handful of machines, and a stale pin fails exactly when you need it.
With an organisation, each machine has one first step: your provisioning hands it the root's fingerprint, the recovery key's fingerprint and a single-use token. After that:
- Names, not keys. A client names the machine it wants; its certificate proves the name.
- Groups. A service admits groups (
db), not lists of keys. A machine's groups are in its certificate. - One root you control. It stays on your machine, and you approve each of its signatures (Touch ID, or a passphrase).
- Revocation and rotation reach every machine. A revoked machine's sessions end within seconds. A lost root is replaced with the recovery key, and every machine follows.
The relay is not trusted with any of this. It carries enrolments, certificates and revocation lists, but it holds no key of your organisation, so it cannot issue a certificate, hide a revocation for longer than an hour, or put itself in the middle of a session.
Organisation mode covers end-to-end encrypted TCP (nfltr tcp, dial, tcp-connect, connect: mappings), nfltr shell over SSH, and machines' agent-to-agent keys. It is the only end-to-end encrypted mode for those: pins and client key lists are gone.
Concepts
| Piece | What it is | Lifetime |
|---|---|---|
| Root | The organisation's top key (P-256). It signs rarely: the issuer's certificate, a monthly checkpoint of the audit log, a rotation's new issuer. It is a passphrase-encrypted file; on a Mac with the optional helper it can live in the Secure Enclave instead. | Until you rotate it |
| Recovery key | One offline Ed25519 key. It signs only one thing: a new root, after the old one is lost or stolen. | Until you use it |
| Issuer | An automated signer under a certificate from the root. That certificate is your policy: the groups the issuer may give out, and its own end. It issues and renews machine certificates and signs the revocation list, with no touch. | 30 days |
| Machine certificate | A machine's name and groups, for a key the machine made itself (the private key never leaves it). It renews by itself at two thirds of its life, with a new key. | 24 hours |
| CRL | The issuer's signed list of revoked certificates. The issuer signs a new one every hour and at once after a revoke. A machine whose CRL is past its next update admits no one under that issuer. | 1 hour |
| Group | A label in the certificate, from the issuer's policy (`web`, `db`, `ci`). Services admit groups. | With the certificate |
Who runs what, where
| Where | What | How often |
|---|---|---|
| The issuer's machine: one of yours that stays on | The root and the issuer: nfltr org init, then nfltr node (or nfltr org serve in the foreground) keeps the issuer served. Tokens, revocations, the monthly nfltr org issuer renew, nfltr org status and nfltr org audit run here. | Once; always running; monthly; as needed |
| Offline media (a USB key in a drawer) | The recovery key | Only after a lost or stolen root |
| Each machine of the fleet | nfltr org join once; nfltr up, nfltr node or nfltr shell then renew its certificate and follow the CRL and the root | Once; nothing after |
| The relay (nfltr.xyz or your own) | Carries enrolments, answers, CRLs, the root and the sessions, through your account's mailboxes | Always |
The issuer runs on a machine of yours, never on nfltr.xyz. The relay is the party this design does not trust: an issuer there could mint certificates for any name. Today the issuer lives where nfltr org init ran, beside the root, so pick a machine that stays on and awake (a home server or a small VM rather than a laptop that sleeps); see Keep the issuer up for why. Every machine uses the same relay account: enrolment, renewal and revocation travel through that account.
Set up the organisation
Choose the root
The root is one of two kinds, chosen at nfltr org init and kept for the organisation's life. Each signature asks you first.
| Kind | Where | Each signature asks | Notes |
|---|---|---|---|
file (the default) | root-org.key in the organisation's directory, mode 0600, encrypted under your passphrase (argon2id, AES-256-GCM) | The passphrase, typed at the terminal | Works on Linux, macOS and Windows |
secure-enclave (opt in) | The Mac's Secure Enclave: the key cannot be copied | Touch ID | For owners who have the helper nfltr-approval-signer on their PATH. This release does not ship it. Without it, nfltr org init --root secure-enclave refuses and names --root file; it never makes a file root in its place. |
A file root can be copied, and a copy plus the passphrase signs anywhere, with no trace on your machine. So use a long passphrase (at least 12 characters; several random words), keep the file on a machine only you use, and keep the recovery key offline: it alone replaces a stolen root. The passphrase is read from the terminal only, never from standard input, an argument or the environment, so these commands need one (ssh -t works).
Make it
On the issuer's machine, with your relay account configured (nfltr config login, or nfltr config set-relay for a relay you run):
$ nfltr org init --groups web,db,ci --recovery-out /Volumes/USB/recovery.key
New passphrase of the organisation root (~/.config/nfltr/org/root-org.key, at least 12 characters):
The passphrase again:
Passphrase of the organisation root, to make the organisation's root and its issuer (30 days):
Passphrase of the organisation root, to make the organisation's root and its issuer (30 days):
organisation o-dg4lusupegx2eytlh7m6xcccmi
root sha256:19b8ba4a8f21afa2626b3fd9eb884262dfda28701a2da2488867c970d0e34960 (file)
recovery sha256:b343a8935d14b3b1e34b588cb9aa06cb714fda51ddf5c11fc5ead82e601eaee4
issuer groups web,db,ci, until 2026-12-11T17:15:54Z
The recovery key is in /Volumes/USB/recovery.key: move it offline now. It alone rotates the root (nfltr org rotate).
The root line says which kind of root it made. With --root secure-enclave and the helper installed, the four passphrase prompts become two Touch IDs. Keep both fingerprints: every machine needs them. Move the recovery key file offline now. --groups is the issuer's policy: it can give out only these groups.
Keep the issuer up
The issuer answers enrolments and renewals, signs the CRL every hour and at once after a revoke, and publishes the CRL and the root through the relay. nfltr node on this machine serves it (it finds the issuer's files), so the node you run anyway keeps it up. nfltr org serve is the same service in the foreground, for a machine without a node:
$ nfltr org serve
level=INFO msg="org issuer: serving enrolments, renewals, the CRL and the root through the relay" a2a_keys=false
If nothing serves the issuer for longer than the CRL's hour, the CRL lapses, and from then on every machine admits no one under that issuer: every organisation session in the fleet is refused and every open one is cut. This is deliberate. A machine cannot tell an issuer that is down from a relay that withholds a revocation, and the safe answer to both is to stop. It recovers by itself once the issuer runs again.
Watch it with nfltr org status. It exits 1 when no process serves the issuer, when the CRL has lapsed or when the issuer certificate has expired, so a cron job or a monitor can alert well before the next update:
$ nfltr org status
organisation o-dg4lusupegx2eytlh7m6xcccmi
root sha256:19b8ba4a8f21afa2626b3fd9eb884262dfda28701a2da2488867c970d0e34960
recovery sha256:b343a8935d14b3b1e34b588cb9aa06cb714fda51ddf5c11fc5ead82e601eaee4
issuer certificate until 2026-12-11T17:15:54Z (in 720h0m0s; nfltr org issuer renew before then)
CRL #1 next update 2026-11-11T18:16:05Z (in 1h0m0s)
served: yes (nfltr node or nfltr org serve on this machine)
When nothing serves it, the last lines read served: no (run nfltr node, or nfltr org serve, on this machine) and the error says no process serves the issuer and the CRL lapses at its next update, with exit status 1. A crontab line that alerts every 10 minutes:
*/10 * * * * nfltr org status >/dev/null 2>&1 || your-alert-command "organisation issuer down"
Enrol machines
Names
A machine enrols under its name in your relay account: the name nfltr machines lists for it (on a relay you run, the --name you gave its key). Clients dial another machine as <machine>.<agent>, the same address as any agent of your account, and check the machine part against the certificate. So the names must agree, and every machine needs an API key of its own: machines that share one key are one machine of your account. On a relay you run, make one per machine with nfltr-relay keys generate --name <machine> --account admin. The issuer refuses an enrolment whose token's name is not the name of the machine that sent it in your account (another machine's name, or one your account does not have), and says which, before the token is used up. An enrolled machine's nfltr up, nfltr shell, nfltr node or nfltr tcp --allow-group refuses to start (exit 1) under another machine's key, naming both and the fix.
One machine
On the issuer's machine, one token per machine name:
$ nfltr org token --name web-1 --group web --ttl 1h > web-1.token
Single use, for web-1, until 2026-11-11T18:16:20Z. Provision it with the root sha256:19b8… and the recovery sha256:b343….
The token goes to standard output and nowhere else; the line about it goes to standard error. It is single use: it enrols that one name, once, before --ttl ends. Add --machine-id <id> (the machine's id, from nfltr machines) for a machine that will make agent-to-agent calls: its enrolment then carries its agent-to-agent key (below).
Your provisioning (cloud-init, Ansible, a Kubernetes Secret, MDM) delivers the token with the two fingerprints. On the machine, with the same relay account configured:
$ nfltr org join --root sha256:19b8… --recovery sha256:b343… --token-file /run/secrets/nfltr-token
enrolled as web-1 (groups web) until 2026-11-12T17:16:33Z; nfltr node and nfltr up renew it
--token-file - reads the token from standard input; NFLTR_ORG_ROOT and NFLTR_ORG_RECOVERY can carry the fingerprints. The machine makes its own key, checks the root the relay hands it against the fingerprint you gave it, and keeps its certificate in its config directory (key mode 0600). Never put a token on a command line or in an environment variable: other users of the machine can read those.
From then on nfltr up, nfltr node and nfltr shell renew the certificate and follow the CRL and the root. Every other nfltr command on the machine reads what they keep. A machine that is off for longer than its certificate's 24 hours enrols again with a new token.
Many machines
The same two commands in a loop. Here the issuer's machine reaches each new machine over SSH, and the token travels on standard input from one command to the other, never on a command line:
ROOT=sha256:19b8… # nfltr org init printed these
REC=sha256:b343…
for m in web-1 web-2 web-3; do
nfltr org token --name "$m" --group web 2>/dev/null |
ssh "$m" nfltr org join --root "$ROOT" --recovery "$REC" --token-file -
done
With cloud-init, Ansible or a Kubernetes Secret, write the token to a file only the machine's nfltr user can read and pass --token-file. A token lasts an hour by default (--ttl), so make tokens close to when machines boot.
Use it
Services and clients
A service admits groups. On the machine web-1:
$ nfltr tcp 5432 --allow-group db --name db
or under nfltr up:
agent: db
services:
pg: { tcp: 5432, e2ee: true, allow_group: [db] }
A tcp service with end-to-end encryption needs allow_group and refuses to start without it, and it refuses to start on a machine that is not enrolled.
A client on an enrolled machine names the machine and its agent. Its sessions are end-to-end encrypted in organisation mode with no flag ("e2ee":true), and it checks that the certificate is the organisation's for the machine part, not revoked:
$ printf hello | nfltr dial web-1.db/pg
{"event":"open","target":"web-1.db/pg","session":"s-d20341fa","e2ee":true,"time":"…"}
hello{"event":"close","session":"s-d20341fa","bytes_in":5,"bytes_out":5,"time":"…"}
$ nfltr tcp-connect web-1.db/pg --listen 127.0.0.1:5432
So is every connect: mapping of its nfltr up:
connect:
pg: { target: web-1.db/pg, listen: "127.0.0.1:5432" }
A bare agent name (db/pg) is an agent of the client's own account machine. A client outside the service's groups, revoked or expired is refused in the TLS handshake with exit 10 (e2ee_client_refused). The connect sidecar guide shows mappings in a larger setup.
Services by name
A service several machines run is published under a group and dialled by its name, <service>@<group>. Every instance runs the same file, with publish: naming a group its certificate holds:
agent: pg
services:
pg: { tcp: 5432, e2ee: true, allow_group: [app], publish: db }
A client of the organisation dials the name; the relay picks an online instance for each session, and the client checks that the certificate holds group db and that the session names the service pg, never which machine:
$ nfltr services
NAME KIND E2EE DIRECT INSTANCES UP
pg@db tcp e2ee - 2 2
$ nfltr tcp-connect pg@db --listen 127.0.0.1:5432
Each connection of a target: pg@db mapping is its own pick, so a mapping never stays on a machine that went away; an instance the client refuses is skipped and another tried until --open-timeout. nfltr services pg@db lists the instances, --watch follows them. To reach one machine, dial it as <machine>.<agent>/pg. The connect sidecar guide shows mappings in a larger setup.
Direct paths
An organisation-mode session can leave the relay and run straight between the two machines over UDP, under the same end-to-end TLS, falling back to the relay when that path dies. It is off unless both ends ask: direct: true on the service and on the mapping, or --direct on nfltr dial and tcp-connect. Each end then learns the other's public address (and its LAN addresses, unless direct: public). nfltr config set-direct on makes asking the machine's default for every organisation-mode session and service that names no direct setting of its own (direct: false still wins; off unless set). The flags are in the CLI reference.
Plain on purpose
--plain (on nfltr dial, tcp-connect and ssh-proxy) or plain: true (on a mapping) reaches a service that is not end-to-end encrypted, for that call or mapping only. There is no setting that turns it on for a whole machine, and nothing the relay says changes the mode. An enrolled machine that dials a plain service without --plain gets exit 7, and the message names --plain. A machine that is not enrolled opens plain sessions, and an organisation-mode service refuses them (exit 4).
SSH with nfltr shell
nfltr shell on an enrolled machine presents an SSH host certificate for its name, signed by the issuer and renewed with the machine's certificate. On the client, trust the organisation once, then add the machine:
$ nfltr org trust
ssh now trusts organisation o-…'s machines (*.o-….internal) in /home/you/.ssh/known_hosts
per machine: nfltr ssh-config <name>
$ nfltr ssh-config web-1.web-1 --identity ~/.ssh/id_ed25519
$ ssh web-1.web-1 'echo hello from $(hostname)'
hello from web-1
nfltr org trust writes one @cert-authority line, the issuer's key, read from this machine's own organisation files and checked there first, never taken from the relay. nfltr ssh-config writes HostKeyAlias <name>.<org id>.internal and StrictHostKeyChecking yes, so ssh accepts only a certificate for that name and never offers to trust a new key. Its ProxyCommand opens the session with --plain: SSH is the end-to-end layer and checks the certificate itself. web-1.web-1 is the shell agent web-1 (a shell's agent is its machine's name) of the machine web-1; from the same machine, the bare web-1 works. The SSH tutorial walks through it step by step.
Agent-to-agent keys
If the issuer's machine also holds your agent-to-agent root key (a hub makes one on its first start), an enrolment for a token made with --machine-id carries that machine's agent-to-agent key, sealed to the machine's new key: no key file to copy. nfltr org join writes it beside the machine's config, mode 0600. nfltr orch hub machines lists the keys and nfltr orch hub machines revoke revokes one. More in Join machines as nodes.
Manage it
Monthly: renew the issuer
The issuer's certificate lasts 30 days. Once a month, on the issuer's machine:
$ nfltr org issuer renew
Passphrase of the organisation root, to renew the issuer (30 days) and checkpoint the audit log:
Passphrase of the organisation root, to renew the issuer (30 days) and checkpoint the audit log:
issuer renewed: groups ci,db,web, until 2026-12-11T17:21:32Z; log checkpoint c5f0fbb6…
Two signatures (two passphrases or two Touch IDs): the new issuer certificate, and a checkpoint of the audit log signed by the root. --groups changes the policy. nfltr org status shows the days left.
Revoke a machine
$ nfltr org revoke db-1
revoked db-1: no renewal from now; every unexpired certificate of it is in ~/.config/nfltr/org/crl.der
Every unexpired certificate of the name goes into a new CRL, and the name can never renew. The issuer publishes the CRL within 10 seconds. Every machine holding a session with db-1 ends it when the CRL reaches it, and new sessions are refused (exit 10):
- within seconds through the relay's push;
- within 10 minutes if the relay withholds the push: each machine also reads the CRL afresh every 10 minutes;
- within the CRL's hour if the relay withholds everything: a machine whose CRL has expired admits no one under the issuer.
To bring the machine back, enrol it under a new name with a new token.
Someone leaves
Revoke every machine they had a key on (nfltr org revoke <name>), and remove their machines' keys from your relay account (nfltr machines revoke). If they could reach the issuer's machine or the root, rotate the root as below.
A lost or stolen root: rotate
If the root (the Mac, or a copy of the root file and its passphrase) is lost or stolen, rotate with the recovery key on the issuer's machine:
$ nfltr org rotate --recovery-key-file - < /Volumes/USB/recovery.key
rotated: root sha256:e318… (file "org-20261111T172109"), certified by the recovery key; a new issuer. Machines renew into it on their next contact.
The recovery key certifies a new root of the same kind (a file root asks for a new passphrase), which certifies a new issuer. Machines take the new root from the relay, accept it only if it is signed by the recovery key they pinned when they joined, end sessions under the old root and renew into the new issuer by themselves, within seconds. Revoked names stay out. A relay can delay a rotation by withholding it, so a machine stays on the old root until the rotation reaches it.
A machine new after the rotation joins with the new root's fingerprint, the one nfltr org status and nfltr org token print now. Its token names the organisation, which keeps its id across rotations, so the machine finds the organisation's root on the relay and checks it against that fingerprint. Tokens made before v2.1.58 name no organisation and are refused: make a new one.
Audit
$ nfltr org audit --verify
verified 28 entries; head c5f0fbb6…; last root checkpoint 2026-11-11T17:21:32Z
nfltr org audit prints every act: tokens, enrolments, renewals, revocations, CRLs, issuer renewals and rotations. The log is hash-chained, and the monthly issuer renewal signs its head with the root, so a stolen issuer key cannot rewrite what came before. --json prints the raw lines.
Limits
Stated plainly, so you can judge whether they matter for you:
- SSH cannot check revocation. OpenSSH checks the host certificate, not the CRL. A revoked machine's
nfltr shellstays trusted by ssh until its host certificate ends: up to 24 hours. - The CRL fails closed. If nothing serves the issuer for more than an hour, every organisation session in the fleet is refused until it runs again. Run the issuer on a machine that stays on, and alert on
nfltr org status. - The issuer lives with the root. Today the issuer runs where
nfltr org initran. Certifying an issuer on a separate always-on machine, with the root signing it monthly from elsewhere, is not available yet. - One relay account. Every machine of an organisation uses one relay account: its mailboxes carry enrolment and revocation.
- Tokens only. Machines enrol with single-use tokens. Cloud instance identity and Kubernetes service-account tokens are not supported.
- Names follow the account. A client dials another machine as
<machine>.<agent>, or a service by name as<service>@<group>. An organisation machine name that resolves from anywhere in the account is not offered: dial the service by name instead. - Service health stays on the agent.
nfltr upprobes each published service's target and logs when it goes down or up; the relay does not see that yet, so it picks among all online instances andnfltr servicesshows each as up. - The Secure Enclave root needs a helper that this release does not ship. The file root is the default until it does.
- What the relay still sees. It cannot read or change organisation-mode sessions, but it sees which machine talks to which, when, and how much, and it can refuse to carry anything.
Troubleshooting
| You see | Meaning and fix |
|---|---|
nfltr dial exit 10, e2ee_client_refused | The service refused this machine's certificate: not in one of its groups, revoked or expired. The service's log names which (ci-1 is not in group db, db-1 is revoked). |
exit 7, the server is not machine web of the organisation … valid for web-1… | The machine part of the address and the certificate's name differ: a typo, or a machine enrolled under another name than its account name. Dial <account name>.<agent>; enrol machines under their account names. |
exit 7, the service is not in organisation mode; --plain … | The service is plain. Add --plain (or plain: true) if that is what you mean. |
exit 4, takes end-to-end encrypted sessions only: open them from a machine of the agent's organisation | This machine is not enrolled: nfltr org join. |
join refused, the account has no machine named web-1, or nfltr up exits 1, this machine is web-1 in its organisation, but the relay key in use is account machine … | The machine runs on another machine's API key. Give each machine a key of its own, named as its organisation name, or make the token for the machine's own account name. |
exit 3, agent m-…-web-1 is not connected | A bare name is an agent of this machine's own account machine. Use <machine>.<agent>. |
| exit 6 while a session is open | The session was cut: often a revocation, a certificate that ended, or a lapsed CRL. |
| exit 2 on an enrolled machine | The enrolment exists but cannot be read; nfltr refuses rather than open the session plain. Check the files' owner and mode under the config directory's org/machine. |
--allow-group: this machine is not enrolled in an organisation | The service's machine needs nfltr org join first. |
join: the root or recovery certificate is not the pinned one | A fingerprint is mistyped or truncated, or carries a trailing carriage return. Use the two that nfltr org init printed. |
join: the organisation's root: … (is the issuer running …?) | Nothing serves the issuer (nfltr org status on its machine), or this machine uses another relay account. |
join: no answer from the organisation's issuer within 2m0s | The issuer is not served or does not reach the relay; --wait waits longer. |
an nfltr organisation token of the v1 format (before v2.1.58), which names no organisation | The token was made before v2.1.58. Make a new one with nfltr org token. |
join: the issuer refused: refused: the token expired | Make a new token, closer to when the machine joins. |
join: the issuer refused: the token is for web-1, machine m-… of the account, but machine m-… posted the request | The token names another machine of your account. Make a new token for this machine's account name (nfltr machines), or one for web-1 to join on web-1. |
join: the token was redeemed already | Another machine, or an earlier join, used it first. nfltr org audit shows when. If that wasn't you, revoke the name; enrol the machine with a new token. |
join: this machine is enrolled already | It keeps its enrolment; nothing to do. |
join: not joining, the token is not spent: … cannot be written, or … is not an A2A machine key file | Join checks the agent-to-agent key file before it redeems the token, since the enrolment delivers that key once. Make the config directory writable by this user, or move the file it names away, then join again with the same token. |
join: moved the A2A key file already at … (machine m-…'s key, …) to …a2a-machine.key.m-… | The file was another machine's (an earlier account key, or a copied config). The old file is kept beside the new one, mode 0600. Nothing to do. |
nfltr node refuses to start: A2A E2EE key file … is machine m-X's key, but this node signs in as machine m-Y | The key file is another machine's. On the hub's machine, nfltr node (v2.1.60 and later) sets it aside and makes the machine's own. On another machine, a join before v2.1.60 kept the wrong file and lost the delivered key, and the name can't enrol again. Recover: nfltr org revoke <name> where the issuer is; give the machine a new account machine (nfltr config login --machine <new name>, or an API key of its own); remove its org/machine directory under the config directory; then nfltr org token --name <new name> … and nfltr org join there. |
org token: refused: web-1 is enrolled already; a token is for a new name | Names enrol once. Revoke and pick a new name to replace a machine. |
org token: refused: groups [prod] are not all in the issuer's policy | Add the group with nfltr org issuer renew --groups …. |
this machine's certificate expired; enrol it again | The machine was off for more than 24 hours: enrol it with a new token under a new name. |
| Every organisation session refused or cut | The CRL lapsed (nothing served the issuer for an hour; nfltr org status exits 1) or the issuer certificate expired (more than 30 days since nfltr org issuer renew). Start nfltr node or nfltr org serve on the issuer's machine, and renew the issuer. |
the Secure Enclave root needs the helper nfltr-approval-signer, which this release does not include yet | Use nfltr org init --root file. |
the file root's passphrase is read from a terminal, and none is attached | Run the command in a terminal, or over ssh -t. |
root-org.key: mode 0644, want 0600 | chmod 600 the file, and ask who could have read it: a copy and the passphrase can sign. |
Next
- SSH to a machine behind NAT:
nfltr shellstep by step - Join machines as nodes: nodes, hubs and agent-to-agent keys
- End-to-end encryption: what each path protects
- Organisations: trusting machines by name, not by pinned key: the design, its threat model and the evidence