Deploying#
Three parts, in this order: the broker (one, inside the private network), one enforcement point per enforced network, and the agent on every device.
Nothing here is optional and nothing here is long. Where a step exists only to avoid a silent failure, it says so.
1. Broker#
Needs Postgres and nginx. It binds loopback and never terminates TLS itself.
The broker is not on any package index, and that is deliberate — unlike the
agent, which every device in a fleet installs, this is the server half and it is
not published. What you install is the wheel from the release you were given,
next to its SHA256SUMS. Verify that first: everything below this line trusts
the file.
The same release carries exordos-nac-docs-<version>.tar.gz: these pages as they
stood at that version, which the published site - built from master - is not.
Unpack it and open index.html; it needs no server.
Installing the wheel gives you the code and the schema, and deliberately not
etc/: the nginx snippet and the example policy are material to copy and edit,
not code to import. Both are in the sdist beside the wheel, and in the
repository — take them from there, and keep them under version control with the
rest of your configuration.
The attestation extra is not optional for a deployment, only for a checkout: it
pulls tpm2-pytss, which is what verifies evidence, and it builds against
libtss2-dev. Without it the broker refuses to start — because the alternative is
what it used to do, which is start, enrol devices that send no evidence, and
answer every machine with a TPM 400 attestation_invalid about evidence that was
in fact fine. That extra does come from a public index; nothing about the broker
being closed changes where its dependencies live.
sha256sum -c SHA256SUMS
apt-get install -y libtss2-dev
pip install './exordos_nac-<version>-py3-none-any.whl[attestation]'
exordos-nac-broker init-ca --cert-out /etc/exordos_nac/device-ca.crt --key-out /etc/exordos_nac/device-ca.key
# prints the expiry: ten years by default, `--valid-years` to change it
ra-apply-migration --config-dir /etc/exordos_nac/ --path "$(exordos-nac-broker migrations-path)"
# tab completion, optional
_EXORDOS_NAC_BROKER_COMPLETE=bash_source exordos-nac-broker \
> /usr/share/bash-completion/completions/exordos-nac-broker
An air-gapped install needs the dependencies too, which is pip download on a
machine that has an index and pip install --no-index --find-links on the one
that does not. tpm2-pytss builds a C extension, so download it on a machine of
the same architecture and distribution or it will arrive as a source tarball
needing a compiler and libtss2-dev at install time.
/etc/exordos_nac/exordos_nac.conf:
[db]
connection_url = postgresql://exordos_nac:...@127.0.0.1:5432/exordos_nac
[nac]
bind_host = 127.0.0.1
bind_port = 11080
ca_cert_file = /etc/exordos_nac/device-ca.crt
ca_key_file = /etc/exordos_nac/device-ca.key
policy_file = /etc/exordos_nac/policy.yaml
# Every address a device can reach this broker by - one per enforced network.
# Devices are handed this list, the compiler puts each into the bootstrap path,
# and `policy routes` prints them. See §5.
broker_endpoints = https://broker.corp, https://10.20.0.10
# The anchor of the chain nginx serves below. `token new` puts its fingerprint in
# every join ticket, so the person who installs the certificate names it here
# once instead of every operator remembering `--broker-cert`.
broker_cert_file = /etc/exordos_nac/broker.crt
Put etc/nginx/exordos-nac-broker.conf in place, with a server certificate whose SANs
cover every address in broker_endpoints, and start it:
The snippet documents four lines that belong at http level — two maps and two
limit_req_zones — and without them nginx refuses to load it. They are two zones
because there are two kinds of caller: a device renews its lease every eleven
minutes, and an enforcement point asks whether anything changed every few seconds
(--poll-interval, §3). One number cannot bound both, and a gateway throttled by
the device's number stops rewriting elements — which ends its whole segment's
access one lease later.
Under systemd, which is how it runs anywhere that is not a stand — the units are
in etc/systemd/, and the service runs as a nac user that owns nothing except
the release directory:
useradd --system --home /var/lib/exordos/exordos_nac --shell /usr/sbin/nologin nac
install -d -o nac -g nac /var/lib/exordos/exordos_nac /var/lib/exordos/exordos_nac/releases
chown root:nac /etc/exordos_nac/device-ca.key && chmod 0640 /etc/exordos_nac/device-ca.key
cp etc/systemd/exordos-nac-broker-*.{service,timer} /etc/systemd/system/
cp etc/default/exordos-nac-broker /etc/default/exordos-nac-broker # retention, §9
systemctl enable --now exordos-nac-broker-api exordos-nac-broker-reap.timer exordos-nac-broker-prune.timer
The two timers are housekeeping and nothing depends on them for access:
exordos-nac-broker-reap closes attachments whose lease ran out and nobody renewed
(every five minutes), and exordos-nac-broker-prune applies whatever retention
/etc/default/exordos-nac-broker names (daily). Access ends when a lease expires on the
enforcement point, with nothing having to be delivered — that is true whether or
not either timer ever fires.
Three things this will refuse to do, all on purpose. It will not start if another
broker is already serving the same bind_host:bind_port — the worker pool shares
the port with SO_REUSEPORT, so two instances would each answer half the requests
from whatever policy and CA they were started with. It will not start without
tpm2-pytss, for the reason above. And it will not decide anything without a
policy: a missing policy file is a misconfiguration, and answering "no grants"
would be indistinguishable from a policy that denies.
The broker belongs inside the private network. The shipped nginx configuration provides request rate limits; a WAF and public exposure are not supported deployment features. Its loopback HTTP listener trusts nginx headers and is reachable by local processes, so loopback does not protect it from untrusted host users. See trust boundaries.
As an image#
exordos/ in this repository builds the same thing as an Exordos element, which
is what a first installation is usually deployed from — everything above, plus
Postgres and nginx, without any of it being typed:
It splits in two the way every Exordos element does. images/install.sh is
everything an image may carry — packages, the venv, the units, the nginx
snippet — and images/bootstrap.sh is what belongs to the machine, run once on
its first boot from /var/lib/exordos/bootstrap/scripts/.
The device CA is on the bootstrap side, and that is the reason the split
matters here. A CA generated at image build time would be one key shared by
every installation booted from that image, and that key is what each of their
nginx instances trusts to authenticate every device — so anyone holding the
image could mint a certificate for any deployment built from it. The database
password is generated there for the same reason. Bootstrap also applies the
schema, writes a self-signed server certificate so nginx has a file to start
with, and moves the database and /etc/exordos_nac onto a second disk when the machine
has one.
Three things are still an operator's, and the login banner says so: the policy
(§2 — until it exists the broker refuses to decide), broker_endpoints in
/etc/exordos_nac/exordos_nac.conf, and a server certificate whose SANs cover every one of those
addresses, replacing the self-signed pair.
2. Policy#
Start from etc/policy.example.yaml (from the sdist or the repository). Check
it before it goes anywhere near a device — the check runs the tests written inside the file:
exordos-nac-broker policy check /etc/exordos_nac/policy.yaml
exordos-nac-broker policy check /etc/exordos_nac/policy.yaml --emit | nft -c -f - # what the kernel will get
Hostnames are resolved at check time, because they are resolved at compile time -
the addresses are what reach a gateway, and an Enforcement Node resolves nothing
itself. On a machine that cannot reach that DNS, --no-resolve checks everything
else; it is what CI runs, and what the shipped example needs, since iam.corp
resolves nowhere.
Do not name the broker in a resource set. Its addresses come from
broker_endpoints, because with more than one enforced network there is more
than one of them, and a ruleset naming a single address deadlocks every device on
the other network: policy drop stops the one request that would have given it a
grant.
The file is re-read when it changes on disk. A file that fails to load leaves the previous one running.
3. Enforcement point#
One per enforced network. It is an ordinary enrolled device carrying one tag — no second credential type, no second CA:
# on the broker
exordos-nac-broker token new --for-user <iam-subject> --tag role:enforcer \
--enforces 10.20.0.0/24
# on the gateway
exordos-nac-agent join --start=false --ticket - # paste it; `-` keeps it out of the history
systemctl enable --now exordos-nac-agent-enforce
--start=false because a gateway is the machine join is not written for. On a
device it starts the prove loop itself, which is the last step of setting a laptop
up; here the unit is enforce, and a machine running both would prove itself
while carrying the fleet's grants.
What that prints is a join ticket, and it is one string rather than a bare token: this broker's address, the fingerprint of the certificate it must present, and the token. The machine recognises the certificate rather than being handed it, so the channel that carries the ticket does not also have to be trusted with which broker this gateway will believe for the rest of its life — which the old instruction, "copy broker-ca.crt across", quietly did.
The address comes from broker_endpoints and the fingerprint from
broker_cert_file, both §1. Where the second is unset the ticket pins nothing
and the device falls back to its own trust store, which works only if nginx
serves a certificate from a public CA — the command says so every time.
--broker-cert overrides it for one ticket. Where the first is unset there
is no address to put in a ticket, so the command prints the bare token instead
and names the option; --as-ticket turns that into a refusal, which is what a
script reading the output wants.
With more than one address in broker_endpoints this refuses to choose: name
the one whose network this gateway is on with --broker.
--enforces is the pool of client addresses this gateway is responsible for.
Write it down even with one gateway. An active enforcement point without a
range is responsible for everything, which makes the fleet's plan empty — and
then no device can check which attachment it is on, including devices behind
other gateways.
Give it exactly the segment whose frames it sees. Too wide is the dangerous typo: devices on a neighbouring VLAN are told they are covered when nothing enforces them.
Two cadences, and both are the same unit. The enforce loop asks the broker
whether anything changed every --poll-interval — three seconds by default — so a
device that has just proven itself reaches this gateway's nftables in seconds. The
answer to that question is a 304 almost every time and costs the broker a few
index lookups. Separately, on the lease cadence (--interval, a third of the
lease by default), it rewrites every element whatever the broker says, because
rewriting is the renewal: an element carries its own timeout, which is what ends
access when nobody is left to end it.
Lengthening --poll-interval costs exactly one thing: how long a device waits
after proving itself. Lengthening --interval is the one that risks access —
elements expire on the lease, not on this schedule.
One unit, and it carries the gateway's whole lifecycle. A gateway runs
exordos-nac-agent-enforce and not the prove one, so that loop is also what
renews this machine's device certificate — sixty days, and renewal authenticates
with the certificate being replaced, so a gateway that let it lapse takes its
segment's grants with it and cannot be put back remotely. It is not in the agent
release channel either: offers ride the proof this machine never makes, so a
gateway is upgraded by hand (§8).
As an image#
exordos-nac-router is the two commands above already run: the agent installed
from its own package, nftables enabled, and a first-boot script that enrols
this machine and starts the enforce unit as soon as it has something to enrol
with. Nothing is running before that, and that is deliberate — a unit restarting
every fifteen seconds against a broker it has no identity for is what an
administrator would find instead of the sentence telling them what to do.
Give it one of two things and re-run
/var/lib/exordos/bootstrap/scripts/0200-nac-router-bootstrap.sh:
| File | What goes in it |
|---|---|
/etc/exordos-nac-agent/join-ticket |
one join ticket — broker address, the fingerprint of the certificate it must present, and the token, in one string |
/etc/exordos-nac-agent/join.env |
BROKER_URL, BROKER_CA and TOKEN, for an operator handed a bare token |
Either file is shredded once it has been spent, because both carry a live credential.
The broker's own node as an enforcement point#
A node can be both, and the broker image carries the same agent and the same
first-boot script for it. What it does not carry is the decision: enrolment is
skipped entirely unless /etc/exordos_nac/enforcer.env exists, so a plain
broker is unaffected.
Fill it in from enforcer.example.env beside it — an owner (the person this
gateway belongs to, by username or by UUID; see Naming a person below) and
enforces (the segment whose frames this machine sees) — and run the same
script. Nothing travels: the token is minted on this
machine by exordos-nac-broker and spent on it over loopback, against the CA this
installation generated at first boot, so there is no ticket and no fingerprint
to check. A machine cannot fail to recognise itself.
Both fields are yours and neither is guessed. owner names a person and no
deployment can invent one that means anything. enforces is the warning
above, one paragraph up: an active enforcement point with no range is
responsible for everything and empties the fleet's plan, and one derived from
whichever interface came up first is exactly the too-wide typo — made by a
script, at three in the morning, on a machine with two NICs.
Naming a person#
--for-user accepts a username or IAM user UUID. The broker stores the value as
entered unless IAM is configured. With IAM, a username is resolved to a UUID
when the token is issued; unknown or ambiguous names are refused. The owner is
retained for attribution and does not affect policy decisions. Policy matches
operator-assigned device tags and rejects rules using owner_groups
(policy model).
Configure IAM in /etc/exordos_nac/exordos_nac.conf:
[nac]
iam_endpoint = https://core.corp/v1/
iam_username = nac-broker
iam_password = <the account's password>
The account needs read access to users; the broker does not write to IAM.
Deployments using a non-default IAM client must also set iam_client,
iam_client_id and iam_client_secret.
4. Devices#
That prints a join ticket — see §3 for what is in it, where the address and the
fingerprint come from, and why. --bare prints the enrollment token on its own
instead, which the two-step configure + enroll below still takes: it is for a
fleet that gets the broker and the CA some other way, the way the Windows
installer writes both from its own properties.
Installing the agent#
Releases carry an installer per platform, and a SHA256SUMS beside them. Verify
it: the agent is a binary that will hold a device's identity, so "download and
run" is not an instruction this product should give.
The native package, and it is worth saying why it matters: a machine that
was given the deb through a converter keeps reporting deb to the broker,
is offered the deb by every release after it, and has no dpkg to install
one with — so it drops out of the update channel with nothing failing.
Installing this over it is what puts it back in. Unlike the other two, it
depends on nftables and conntrack-tools outright, because pacman
installs no weak dependencies and a gateway without them cannot enforce.
msiexec /i exordos-nac-agent_<version>_windows_amd64.msi /qn ^
BROKER_URL=https://broker.corp ^
BROKER_CA=C:\ProgramData\exordos_nac\broker-ca.crt
The properties write config.json, so a fleet is deployed with the broker
already in it. The enrollment token is not a property and must not be
made one: Windows Installer records properties in its log and in the
machine's registry, which outlives the single use that makes a token safe.
The file named by BROKER_CA must be deployed to that path before
enrollment; the MSI stores the path but does not install the certificate.
A ticket used with join carries the broker certificate fingerprint and
does not require a separately deployed CA file.
A single machine does not need them. msiexec /i ... /qn on its own, then
exordos-nac-agent join, is the same one-command setup as everywhere else -
and it is the better one where BROKER_CA would mean copying a certificate
off a share, because a ticket carries a fingerprint instead and the machine
recognises the certificate rather than being handed it.
The package and the binary inside it are both Authenticode-signed, and by whose certificate decides what that is worth. Signed by this organisation's internal CA, as it is today, the signature means something on machines your policy has given that root and nothing at all on the others — Windows warns there exactly as it would for an unsigned file. Where the root is deployed, an AppLocker or WDAC rule can admit the agent by publisher instead of by a hash that changes with every release, which is the reason to sign it.
One package for Apple silicon and Intel; it is signed and notarised, so Gatekeeper accepts it without anybody being told to right-click and open something. It installs an app bundle rather than a binary, and that is not a packaging habit: the entitlement that reaches the Secure Enclave is authorised by a provisioning profile, a profile lives inside a bundle, and macOS refuses to run a binary whose keychain entitlement no profile backs.
Each account on a Mac is a separate device. The key is in the data
protection keychain, which exists only inside a logged-in user's session, so
the agent runs as a per-user LaunchAgent and every account that uses the
machine enrols its own — with its own device_uuid, certificate and tags.
Enrol one account on a shared Mac. Two logged in at once — which is what
fast user switching leaves behind — are two devices at one address, and an
address more than one device claims is enforced for neither of them (§7,
conflicts). That is the same rule that protects you from a NAT, and here it
is the reason not to enrol the second account rather than a fault.
Nothing is running after this, on any platform, and that is deliberate. The
service is registered and left stopped, because none of it can work before the
machine has a device identity: prove has no certificate to authenticate with.
An installer that started it would leave a service failing every fifteen seconds
for an administrator to find, instead of the enrollment step they still have to
run.
What the install owns, and who may write it. Only root, everywhere it matters, and one of these is load-bearing rather than tidy:
| Path | Mode | Why |
|---|---|---|
/var/lib/exordos-nac-agent |
0700 root:root |
the TPM-wrapped key blob, which on Windows and Linux alike is protected by the directory around it and by nothing else |
/etc/exordos-nac-agent |
0755 root:root |
decides which broker this machine believes, so nobody but root may write it — and readable so status can answer the person whose laptop it is |
/etc/exordos-nac-agent/config.json |
0644 root:root |
not a secret; the identity is the blob, not this |
/usr/bin/exordos-nac-agent |
0755 root:root |
|
%ProgramData%\exordos_nac |
SYSTEM + Administrators, inheritance off |
ProgramData's default lets any interactive user write there, which would put the key blob and the broker URL in reach of the machine's own user |
/Library/Application Support/exordos_nac |
0755 root:wheel |
machine-wide on a Mac: the bundle, the example config and the config.json every account on it reads — so only root may write it |
~/Library/Application Support/exordos_nac |
the account's own | the state, per user, because the key it belongs to is: certificate, device_uuid and the report of the last proof — and the config.json of a Mac nobody administers, written by a join that was not run as root and read only where the machine-wide one is absent |
/Library/LaunchAgents/io.exordos.nac.agent.plist |
0644 root:wheel |
what runs the agent in each user's session, shipped disabled |
Removal keeps the device identity. dpkg -r, rpm -e, pacman -R and
msiexec /x take the
binary and the units away and leave /var/lib/exordos-nac-agent and
C:\ProgramData\exordos_nac where they are — they hold the TPM-wrapped key
blob, and the way back from losing it is a fresh enrollment token under a new
device_uuid, which loses the device's history and its pinned keys. dpkg -P is
the one command that means "destroy it", and it says so while doing it. It is
dpkg's alone: rpm and pacman have no purge, so on those two the identity always
outlives the package and exordos-nac-agent reset is what ends it.
macOS has no such command, because a pkg has no uninstaller, so the package
ships the sequence instead — run it in the session of the person whose Mac this
is, without sudo, and it asks for sudo itself where the machine-wide half
needs it:
The order is the whole of it, and it is why this is a script rather than a
paragraph. reset runs first, from the account that owns the key: the device
key is in that account's keychain, only the agent can delete it, and once the
files are gone nothing on the machine can name it again — a data protection
keychain item is not shown in Keychain Access either. Then the LaunchAgent is
booted out, /Library/Application Support/exordos_nac, the LaunchAgent plist and
the /usr/local/bin link are removed, the installer receipt is forgotten so that
pkgutil --pkgs stops reporting a version whose files are gone, and the
account's own state under ~/Library/Application Support/exordos_nac goes last.
Two things it cannot do from where it runs. Every other account that enrolled on this Mac is a separate device with its own key in its own keychain, and only a command running as that account can delete it. And the broker still holds the device record: it stops mattering by itself, because a grant is a fifteen-minute lease and nothing renews it now, but an operator who wants the record gone removes it there.
Setting it up#
A package and a ticket, and the machine is finished.
That writes the configuration, pins the broker's certificate by the fingerprint inside the ticket, enrols this machine, and starts the agent's service — the systemd unit on Linux, the registered service on Windows, the LaunchAgent in the user's own session on a Mac. Its last line says whether this device can install a release it is offered, which an agent installed from a release can (§8).
Starting used to be a separate step, and it was the one that failed quietly: a
device that enrolled and never started the loop looks healthy for fifteen
minutes — it has an identity, a certificate and one proof behind it — and is then
in quarantine with no way out, because leaving quarantine requires proving.
Nothing on the machine tells that apart from a working laptop until the lease has
already run out. --start=false is for a deployment that starts the unit some
other way, and it is what a gateway uses (§3).
On macOS this is the person's own command in their own session, without sudo:
the key is in their keychain, and join refuses to run as root rather than
enrolling into a keychain the LaunchAgent can never read. The configuration then
goes where that run can put it — /Library/Application Support/exordos_nac when
it is run by an administrator, and otherwise the account's own
~/Library/Application Support/exordos_nac, which is the second place every
command looks. A machine-wide file, wherever one exists, still wins over it: that
is how a fleet keeps deciding which broker its Macs believe, and a per-account
copy beside it would be read by nothing. So when one exists and names a
different broker than the ticket does, join refuses before enrolling and
names the command that resolves it — sudo exordos-nac-agent configure --broker
… --ca …, after which the join runs as the person again.
Configuring it by hand#
Where a ticket is not what an operator was handed — the Windows installer with the broker in its properties, an image that was given a bare token — the same two fields are written on their own, and nothing here is about the network the device is on:
exordos-nac-agent configure \
--broker https://broker.corp \
--ca /etc/exordos-nac-agent/broker-ca.crt
That writes /etc/exordos-nac-agent/config.json on Linux, next to the
config.example.json the package ships,
%ProgramData%\exordos_nac\config.json on Windows — where configuration and
state share one place by convention — and
/Library/Application Support/exordos_nac/config.json on macOS, which needs
sudo and is read by every account on the machine. The state on a Mac is not
there: it is under each user's own ~/Library/Application Support/exordos_nac,
because the key it describes is in that user's keychain. Editing
the file by hand does the same job; configure merges, so naming one field does
not drop another, which is what makes it safe for an installer to re-run on
upgrade.
A machine installed before the file moved keeps its copy in the state directory
and keeps working: the agent looks in /etc first and there second. status
prints which of them it read, and when there is no broker it names every place
it looked — the failure this replaces was a file written to /etc, read by
nothing, and "broker URL is not configured" with nothing connecting the two.
reset does not touch the configuration; dpkg -P removes it, and where there
is no purge — rpm, pacman, Windows, macOS — deleting the file does.
The Windows installer writes the same file from its BROKER_URL and BROKER_CA
properties, so a fleet arrives configured.
--start does there what join does by itself: enables and starts the unit,
service or LaunchAgent this platform runs the loop with. Without it the machine
is enrolled and not proving, and these are the commands that finish it by hand:
systemctl enable --now exordos-nac-agent-prove # Linux
sc start exordos-nac-agent # Windows: registered by the MSI, auto at boot
On macOS all of it belongs to the person whose account it is, and none of it
takes sudo. Run it from a Terminal in their own session — an SSH login is a
different launchd domain, where the keychain is refused whoever you are:
join and enroll refuse to run as root on a Mac rather than succeeding into a
keychain the LaunchAgent can never read; anything an administrator does there
(sudo exordos-nac-agent configure, which writes the machine-wide file) is
separate from the person's own enrollment. The agent starts itself at every later
login.
The loop is not optional. A grant is a lease: a device that proves itself once at install time is back in quarantine fifteen minutes later and cannot leave, because leaving requires proving. The same loop replaces the certificate when its renewal date arrives, which fails on a sixty-day fuse into a state where renewal is no longer possible.
In person, where the server cannot check the key#
On macOS the broker will never verify the key (macos.md), and on a machine with
no usable TPM there is nothing to verify either: key_protection_verified stays
software, and the strongest thing said about that device is
key_protection_claimed = hardware_bound — the agent's own word for it.
Enrolling in person is how that word is made worth something. It buys exactly what
attestation buys and nothing else: confidence in the moment the key was first
written. After that the hardware carries it alone — renew refuses a certificate
for a different public key, and an Enclave or TPM key does not move to another
machine — so every later signature really does mean "the same laptop".
The trip is not a statement about the person. It has to be one about the machine:
- Start from a machine you know — freshly imaged, or at least one IT owns, and install the agent from your own artefact rather than from a link the user has. In-person enrollment on a laptop that was already compromised produces exactly the same record as an honest one, and nothing later tells them apart.
- Make the agent fail closed before it enrols. In
config.json:"require_hardware": true. Without it a keystore that is broken this morning silently produces a file-keyed device that was nonetheless enrolled in person, which is the one combination this whole procedure exists to prevent. (A machine that has held a hardware key before is already refused a copyable one withoutenroll --accept-software-key;require_hardwareis what covers the first enrollment, where there is no history yet.) - Record what you did on the token, not on the device:
exordos-nac-broker token new --for-user <iam-subject> \
--tag type:full --tag dept:engineering --tag enrolled-in-person \
--ttl-minutes 10
Tags travel with the token, so this is the operator's statement, kept
server-side, and the device cannot forge it — unlike key_protection_claimed,
which is the device's. That is what turns an out-of-band check into something a
policy may match on. The short TTL costs nothing while you are standing there.
- Enrol from stdin, so the token is not left in
psfor every local user and in the shell history afterwards:exordos-nac-agent enroll --token -. - Read the result before you walk away.
exordos-nac-agent status:
device key secure-enclave-user backend <- or tpm2; never "software"
key protection hardware_bound (claimed)
degradation (absent)
server-verified software <- expected on macOS; on a TPM
machine it reaches attested
after the first proof
A software backend or a degradation line here means the trip proved
nothing. Fix the keystore and re-enrol rather than leaving and coming back.
6. Check the broker's copy while the machine is still in front of you:
exordos-nac-broker device list --tag enrolled-in-person.
7. Start the loop: systemctl enable --now exordos-nac-agent-prove.
Then say it in the policy, once and visibly. A vouched-for Mac is still
key_protection: [software] to every rule, because that is what the broker proved:
- name: mac-vouched
match:
state: proven
tier: managed
posture: compliant
key_protection: [software] # what the broker proved: nothing
device_tags:
enrolled-in-person: true # what an operator vouched for, in person
type: full
dept: engineering
grant: [base, eng]
Where it sits does not matter. Every rule that matches adds its sets, so a
vouched Mac holds this rule's sets and whatever degraded grants every software
device in those departments, and an unvouched one holds only the latter.
The shipped example forbids exactly this —
forbid: { key_protection: [software], grant_contains: [prod_ro, eng] } — so
policy check refuses the rule until that constraint is narrowed. The friction is
the point: accepting a human check in place of a certify chain is decided once, in
one line, in front of whoever wrote it, instead of rule by rule. Narrow it rather
than remove it — leaving prod_ro on the list means the vouching buys ordinary
access and not the valuable set.
What it does not buy. It is checked once and never again: attestation is
re-checked on a cadence, a handshake is not. It does not survive an enrollment done
any other way, so a token carrying enrolled-in-person must never be issued for a
remote one — the tag would then say something nobody did. And non-exportable never
meant "unusable by whoever is root on that machine": it means the key cannot leave,
which is a statement about the second machine, not about this one.
5. Routes#
Devices must reach the enforced destinations through their enforcement point. Generate the list rather than typing it — the policy lives here, and the push lists live in a DHCP server and a VPN configuration owned by other people:
exordos-nac-broker policy routes /etc/exordos_nac/policy.yaml --format dhcp-121 --via 10.20.0.254
exordos-nac-broker policy routes /etc/exordos_nac/policy.yaml --format openvpn-push
With a split tunnel the pushed list must cover every enforced prefix. One left out leaves by the other attachment, so the device holds two enforced attachments while the broker keys one session — the agent names the prefix and keeps the access it can, but the traffic to that prefix is enforced by nobody.
6. Verify, once, per network#
Each of these fails silently if it is wrong. The full list with symptoms is network requirements; these are the commands.
# corporate resources are reachable ONLY through the enforcement point.
# If the ordinary router has a route to them, enforcement is decoration - and
# nothing in this product can see that.
ip route get <resource> # on a device: the next hop must be the gateway
# nothing translates the source address between the gateway and the resource
conntrack -L -s <device-address> # both tuples show the same source
tcpdump -ni any host <resource> # at the far end: the device's own address
# the compiler emits no NAT at all, so anything here came from elsewhere
nft list ruleset | grep 'type nat'
# one device per address, and no NAT in front of the gateway either
exordos-nac-broker device list # and watch `conflicts` in the dataplane answer
Wireless and wired segments additionally need client isolation, DHCP snooping and IP source guard on the switch: in a tunnel OpenVPN guarantees the source address, on Ethernet nobody does, and the grants hang on it.
Enforced segments must be IPv4. The compiled ruleset carries no v6 grants; v6 is counted and dropped, which on a dual-stack segment looks like a broken network.
7. Day to day#
exordos-nac-broker device list # claimed vs verified protection, side by side
exordos-nac-broker device list --verified software # who is not what they claim
exordos-nac-broker device list --stale 24 # not heard from in a day
exordos-nac-broker device list --tag dept:finance
exordos-nac-broker device list --owner gmelikov # one person's machines, by name or UUID
exordos-nac-broker device show laptop-7 # one device: owner, tags, certificate, sessions, trail
exordos-nac-broker device versions # which agent versions the fleet runs
exordos-nac-broker release status # how far the rollout has got
exordos-nac-broker device block <device> # gone from the next dataplane answer, not in 15 minutes
exordos-nac-broker device enforces <device> --set 10.20.0.0/24
exordos-nac-broker session who <address> --at <moment> # who held this address then
exordos-nac-broker session list --active # what is attached now; --owner, --address, --device narrow it
exordos-nac-broker session list --device <device>
exordos-nac-broker session reap # close what stopped proving
exordos-nac-broker audit list --type attestation_failed # what the trail says
exordos-nac-broker audit list --device <device> --since 12h # 7 alone is days; --owner too
exordos-nac-broker audit prune --older-than 90
exordos-nac-broker token list --owner gmelikov # outstanding tokens and what they will enrol as
exordos-nac-broker token revoke 7f3a9c1e # a token that leaked; --owner takes all of a person's
exordos-nac-broker token prune --older-than 30 # spent and expired ones, never a live one
exordos-nac-broker device delete <device> # the record itself; block is what stops access
Every list takes --json, which is what a script should read; the tables are
for people and gain columns.
<device> is the UUID, a unique prefix of it (four characters or more), or the
name the device enrolled with. A reference that matches more than one device is
refused with the candidates.
The gap between claimed and verified in device list is the number worth
watching: it is the difference between what a device says about its key and what
the broker proved about it.
What the log says#
There are two trails, and they answer different questions. exordos-nac-broker audit is
evidence — durable, queryable, kept on its own retention, and the thing to read
about one device. The log is the stream: one line per thing that happened, in
whatever the deployment already ships to a SIEM, and it is what answers "is
anybody trying?" across the whole fleet. The shape is the one exordos_vpn uses
for IAM AUDIT, so one grep habit serves both products:
Where it lands: the broker logs to stdout/stderr, which under the unit in
etc/systemd/ is the journal — journalctl -u exordos-nac-broker-api. Operator commands
audit themselves the same way, from wherever you ran them. Nothing writes a
separate audit file, and there is no log-file option: a deployment that wants one
takes it from the journal, along with everything else it already ships.
Refusals are logged at WARNING and successes at INFO, so a deployment that keeps only warnings still keeps the half that matters most — a fleet proving itself every fifteen minutes is most of the volume.
grep 'NAC AUDIT' /var/log/nac/broker.log | grep failed # everything refused
grep 'NAC AUDIT: authenticate failed' ... # certificates nobody knows
grep 'reason=token_already_used\|reason=token_invalid' ... # tokens being tried
grep 'NAC AUDIT: dataplane_pull' ... # gateways reading the fleet view
A refused request usually leaves two lines: one from the endpoint, carrying what
only it knows (which token, which certificate serial, which range), and one
request_rejected naming the method, path and status. The second is the one that
exists for refusals nobody thought to instrument, including the ones added later.
Client certificates rejected by nginx never reach the broker and do not produce
a NAC AUDIT event. Their errors appear in nginx's error log
(client SSL certificate verify error); ship both logs to include these failures
in external monitoring.
A device that refuses to prove itself#
The agent decides before it opens a connection whether the address it would
speak from is one this deployment enforces, and which broker endpoint is reachable
from there. Both facts come from the last answer the broker gave it, and both are
only refreshed by a proof that succeeds — so a wrong one is self-sustaining: a
typo in broker_endpoints, or an enforcement range that leaves a segment's
addresses out, and the machines that took it keep refusing themselves with the
list that is refusing them. The broker cannot correct a device that will not talk
to it.
Two ways out, and the first one needs nobody:
- the proof loop stops believing it after three failures in a row and tries
the URL in this machine's own
config.json, saying so in the log. A proof that succeeds that way overwrites both fields with the broker's current answer, and the device is back without anybody visiting it; prove --broker https://broker.corpdoes the same on demand, for a person standing at the machine. The refusal itself names this.
So the operational rule is: fix it on the broker (device enforces,
broker_endpoints) and the fleet comes back on its own within a few proof
intervals. The refusal is deliberate — proving from an address no enforcement
point sees produces a session that looks healthy and reaches nothing — but it
must never be the last word.
When a check fails, the device shows its name and, if the policy wrote one,
its fix: — and only the second is an instruction. A name states the requirement
("the boot chain is the one we pinned"), so a device showing it alone tells its
owner to do the thing that is already true, on the one screen that exists to
explain why access was lost. Write the remedy in the words your organisation
uses:
posture:
require:
- name: the disk is encrypted
fix: turn on BitLocker, or ask IT to do it for you
signal: disk_encryption
equals: true
device versions is the other daily number, and it is live rather than
historical: the recorded agent version used to be the one each machine was
installed with, and every proof now writes back what the agent measured. It
counts reachable devices separately from enrolled ones, because a machine that
has not proved itself in hours is not lagging behind a release - it is not there,
and no way of delivering one reaches it.
8. Rolling out an agent release#
The broker can tell devices which release to move to. It says it and nothing more: the broker distributes signatures and never makes one, so a device that takes an offer is acting on a release key this machine does not hold. Devices install what they are offered, and the two numbers in the manifest are what decide who is offered anything.
Make a release key once, and give the two halves to the machine that builds releases. Neither half is on the broker: a broker holding the private one could sign a binary for every device it talks to, which is the one thing this design is arranged to prevent.
openssl genpkey -algorithm ed25519 -out release.key
openssl pkey -in release.key -pubout -out release.pub
Both halves are settings of the exordos_nac_agent repository, and they do two
different jobs:
| half | where | what it does |
|---|---|---|
release.pub |
the RELEASE_PUBLIC_KEY variable |
linked into every agent, so a device can check an offer with nothing configured |
release.key |
the RELEASE_PRIVATE_KEY secret of the release environment |
signs the manifest at tag time |
A variable and not a secret for the public half: it is a public key, and one nobody can read is one nobody can check a release against by hand. An environment and not a plain repository secret for the private half, because an environment is the smallest thing a required reviewer can be attached to, and what this key decides is what runs on every laptop in the fleet. Give it a reviewer and a tag-only deployment branch rule.
A tag build fails without either, and it checks that they are the same key - a fleet built with one public half and a manifest signed with another installs cleanly and then reports every offer as unverifiable for the rest of each device's life.
Keep release.key somewhere outside CI as well, in whatever the organisation
uses for keys of this weight. Nothing is recoverable from GitHub's secret store,
and losing the private half means every device in the fleet has to be reinstalled
from a build carrying a new public one.
The public half has to travel that way — inside the agent — rather than being installed on each machine. Copying a key onto every device is a step that fails silently: the machine proves, reports offers, installs none, and looks exactly like a device a rollout has not reached yet — for the rest of its life, with the way back being a person at the keyboard. And it cannot come from the broker, because a key the broker chooses is a key the broker can replace.
exordos-nac-agent configure --release-key /etc/exordos-nac-agent/release.pub
still wins over the built-in key, which is how the key is rotated without waiting
for a build, and how a deployment that builds its own agent supplies one. An
agent with neither reports an offer as unchecked rather than treating it as fine,
and status says so before any offer arrives:
Where the packages go#
On the broker, served by the same nginx, from the same address. Put the files
under /var/lib/exordos/exordos_nac/releases/ and the shipped snippet publishes them at
https://<broker>/releases/<file>.
That is not tidiness, it is reachability. Every entry in broker_endpoints is put
into the bootstrap path by the policy compiler, so a device can reach the broker
from every enforced network by construction (§5) — it is the one address in this
product with that property. A separate releases.corp has none of it: it needs a
resource set, a route on each segment, and a line in the list of things this
product requires of networks it does not own (network requirements). Each of
those is a way for a rollout to reach some of the fleet and not the rest, and the
symptom is devices that never arrive with nothing anywhere saying why.
It hands the broker nothing it did not already have. The signature is made off this machine and checked on the device before anything is fetched, and the digest after — so a broker somebody has taken over can still only withhold a release or offer an old one. Serving the bytes is not authorising them, which is the whole reason the release key lives elsewhere.
install -d -m 0755 /var/lib/exordos/exordos_nac/releases
install -m 0644 exordos-nac-agent_0.4.0_amd64.deb /var/lib/exordos/exordos_nac/releases/
The agent trusts the broker's own chain for these downloads in addition to the
system store, so broker_ca_file is all a device needs — which matters, because a
broker answering on more than one network has private addresses in
broker_endpoints, and no public CA signs a certificate for 10.20.0.10. A
deployment that would rather keep its artefacts on a public host still can: an
artefact may name an absolute url instead of a file, and a publicly trusted
one verifies against the system store exactly as before. Neither is signed, so
moving the packages does not mean re-signing the release.
No client certificate is asked for on /releases/. These are signed packages on
a server inside the private network, and requiring mTLS would mean handing the
device key to the code that downloads binaries — a boundary the agent draws
deliberately.
The manifest arrives with the release, signed. The tag build publishes
release.yaml alongside the installers. It lists every package of the release —
deb, rpm and the Arch package (.pkg.tar.zst, installed with pacman -U)
together — and one file serves a fleet that has all three. Each agent reports
which package manager installed it (the package_format signal: deb, rpm or
archlinux) and is offered that format. An agent too old to report it is offered
[nac] default_package_format, which is deb unless you set it. An rpm fleet
upgrading from such an agent needs default_package_format = rpm, and a pacman
one archlinux.
A device offered a format it cannot install says so and stops trying: the update
is reported as unsupported rather than failed, because retrying it every cycle
would claim the next attempt might work. The way into that state is a package
converted from another format — a deb run through debtap onto an Arch machine
keeps reporting deb while nothing on it can install one. Install the Arch
package over it once, by hand, and the machine reports archlinux from then on.
Upgrade the broker before serving a manifest with both formats. An older broker refuses a manifest that lists one platform twice and logs why. It keeps serving the manifest it already loaded, but only until it restarts. After a restart it has no manifest and offers nothing.
Downgrade the schema when you roll the broker back. Once agents report a
format, a broker from before this change cannot read those devices. Migration
0004 removes the format from every device on the way down.
version: "0.4.0"
share: 0 # raise this: the fraction of the fleet the release is open to
enforcer_share: 0 # and of the enforcement points, separately. Default zero.
artifacts:
- os: linux
arch: amd64
format: deb
file: "exordos-nac-agent_0.4.0_amd64.deb"
sha256: "9f2c…"
signature: "MEUCIQ…"
- os: linux
arch: amd64
format: rpm # the same platform again, for machines installed from rpm
file: "exordos-nac-agent-0.4.0-1.x86_64.rpm"
sha256: "c03e…"
signature: "MEYCIQ…"
- os: linux
arch: amd64
format: archlinux # and again, for the machines that install with pacman
file: "exordos-nac-agent-0.4.0-1-x86_64.pkg.tar.zst"
sha256: "7ad9…"
signature: "MEUCIF…"
- os: darwin
arch: universal # one package for Apple silicon and Intel
format: pkg
file: "exordos-nac-agent_0.4.0_macos.pkg"
sha256: "4b71…"
signature: "MEQCIF…"
broker_endpoints has to name this broker for a file to be offered at all.
It is the list the address comes from — the Host of a request only says which
entry, because nginx forwards it as $host and that drops the port. A deployment
that named none has no address to send anybody to and offers no release, which
the broker log says once per proof; a deployment that named one needs nothing
from its proxy at all.
file and not a URL, and that is what makes one signed file work here at all.
The signature covers the version, the platform and the digest — not the address —
so the broker fills the address in per request, with the broker_endpoints
entry that device is talking to. A release built in one place is therefore valid at every
installation, and at every network a broker answers on: while the URL was signed,
a broker with two entries in broker_endpoints could only name one of them, and
the fleet on the other network was offered a download it could not reach.
The signature is this product's own and it is not Apple's. They answer different questions: the notarisation stapled to the pkg says macOS will run it, and this says the fleet was told to by whoever holds the release key — a broker that has been taken over can still only offer artefacts that key signed.
Upgrade the broker before serving one of these. A broker from before file
existed reads the manifest as an artefact with no address and refuses to load it,
saying so once per read in its log and continuing to serve whatever it had — so
the failure is loud and nothing is mis-offered, but the rollout does not start
until the package under it is the one that understands the file. An installation
that has to move the other way round can keep writing url into the manifest by
hand; it is still not signed.
One line to check it before it is the broker's, which is the discipline
policy check already establishes for the other operator-written file here:
A deployment that builds its own agent signs its own manifest, with its own key, the same way the tag build does:
Then point [nac] agent_release_file at it and raise share. share is the rollout control: each
device has a fixed place in [0, 1) derived from its UUID, and it is offered the
release when that place is below share. Raising 0.10 → 0.25 → 1.0 only ever
adds devices, so nobody is offered a release and then un-offered it, and there
is no cohort bookkeeping to keep.
Gateways are upgraded by hand, and are not in this rollout at all. An offer
arrives in the answer to a proof, and an enforcement point runs enforce and
never proves (§3) — so nothing here reaches one. That is the decision rather than
an oversight: the machine every other machine's access passes through is the one
you want moved deliberately, watched, and one at a time. Upgrade it the way any
package is upgraded:
dpkg -i exordos-nac-agent_<version>_amd64.deb # or rpm -U, or pacman -U
systemctl status exordos-nac-agent-enforce # the package restarts it for you
enforcer_share is the same number as share for devices tagged
role:enforcer, and it defaults to zero. What it is for is the machine that
carries the tag and proves — a gateway somebody also gave the prove unit to, or
whatever the next deployment shape turns out to be. It holds that machine out of
a rollout without depending on its config file being right, and release status
prints it even at zero so it is visible before anything is widened. It is a guard,
not the way gateways are moved.
exordos-nac-broker release status
target agent 0.4.0
share 10% of the fleet
enforcers 0% of the enforcement points, 0 in the cohort
fleet 480 enrolled
cohort 47 devices, 44 reachable
arrived 31 of 47
Read cohort against reachable before reading arrived: a cohort full of
machines that are switched off is not a rollout that has stalled. And widen
share on evidence rather than on a timer - a device that proved itself on the
new version has already demonstrated the thing a health check would ask.
A cohort that is reachable and never arrives is a broker with no address to
send it to. release status counts the cohort from share alone, so a
deployment whose broker_endpoints is empty — the shipped configuration has the
line commented out — shows a full cohort, a reachable device, and arrived 0
forever. The devices are offered nothing at all: an artefact named by file has
no address, and the device cannot tell that apart from a cohort that has not
opened. The broker says which it is, once per proof:
A device ahead of the target is offered nothing. The manifest names a
target, not a minimum, so a machine upgraded by hand past the fleet would
otherwise be offered the fleet's version back: a silent downgrade on a deb,
three failed installs on an rpm or an MSI. The broker withholds it, and an agent
that has this change refuses one on its own too - a broker can serve any release
ever signed, including the one with the hole the next one closed. release
status counts those machines apart, so they do not read as stragglers:
Going back on purpose is a rollback, and only the release key can say so. Sign the older release with the newest version it replaces, and that version goes into the manifest and into every signature:
go run ./cmd/release-sign -version 0.4.0 -rollback-from 0.4.1 -dist release # agent repository
exordos-nac-broker release sign release.yaml --key release.key # or: rollback_from: "0.4.1" in the file first
Devices on 0.4.1 or anything between it and 0.4.0 are offered the rollback;
a device past 0.4.1 is offered nothing and its agent refuses it, so a rollback
signed today cannot take back a release that closes the old version's hole
tomorrow. The signed line is present only on a rollback, so every signature made
before it existed still verifies; an agent older than this change refuses a
rollback as a signature that does not verify and stays where it is. rpm is told
--oldpackage, dpkg and pacman need nothing, and Windows reports it as a
rollback it cannot install - the MSI refuses to replace a newer version, so going
back there is an uninstall and an install. Rolling forward - the old code under
a new version number - needs none of this, and is usually the better way to undo
a release.
What the device does with the offer#
It installs it. That is the default, and what a device needs for it is the
release key — which an agent installed from a release already has, and which
configure --release-key overrides where an operator keeps their own.
An offer that cannot be verified is never installed, so a device with no key at
all reports offers and applies none - which is a misconfiguration, not a stance,
and status says so in those words. The stance is spelled out:
which leaves the device checking and reporting offers and installing nothing. It is not what holds a gateway back: a gateway is out of this entirely, because it never proves and an offer only ever arrives in the answer to a proof.
A fleet upgrading into the agent that introduced this default starts installing
offers. Their config files say nothing about updates, and saying nothing means
installing - which is the whole point of a default, and still worth knowing
before the first manifest goes up. Enforcement points are not in it. If a
deployment wants the old behaviour, --apply-updates=false is a line in whatever
already configures those machines.
What the agent then does: fetch the artefact, check it against the digest the
release key signed, hand it to dpkg, rpm or msiexec, and exit so the
service manager starts what is now on disk. It does this immediately after a
proof succeeded, which is what makes it safe - the lease just renewed leaves a
quarter of an hour of access to download and restart inside, and nothing has to
be delivered for that to hold. The exit is also what makes the new version get
reported: an agent that installed and kept running would keep sending the old
number, and that number is this page's arrived column.
Per device, three rails and no more: an unverified offer is never installed, one
version is tried three times at least an hour apart and then left alone, and the
attempt is recorded before the installer runs so an install that kills the agent
is still counted. exordos-nac-agent status says which of those a machine is in.
macOS devices install through a root helper. The agent there runs in the
logged-in user's session, because that is where the keychain holding the device
key is, and installer needs root. So the package also installs a LaunchDaemon,
io.exordos.nac.updater, which launchd starts when the agent queues a verified
offer; it checks the signature, the version and the digest again as root and
installs its own copy of the file (the agent's README says what it trusts). Its
last decision shows in exordos-nac-agent status as update helper.
A Mac on 0.0.7 or earlier has no helper, and moves to the first release that
has one by hand or through its MDM - once. From then on it updates like the rest
of the fleet. Until then its status says the helper is not installed, which is
a different state from a rollout that has not reached it. macOS shows a
"background item added" notice when the helper is installed; an MDM can
pre-approve it with a com.apple.servicemanagement payload.
On Linux, a hand-run dpkg/rpm upgrade restarts one unit and not the other.
exordos-nac-agent-enforce is restarted by the package, because it never exits
on its own and would otherwise go on running the old binary while reporting the
new version. exordos-nac-agent-prove is deliberately left alone — the
agent-driven path runs the very same maintainer script, and restarting the prove
unit from inside its own package transaction tears the agent down mid-install.
So an administrator upgrading a device by hand runs systemctl restart
exordos-nac-agent-prove afterwards; on a gateway there is nothing to do.
On Windows, a hand-run msiexec upgrade leaves the service stopped. The
package stops exordos-nac-agent and starts nothing, deliberately, because a
fresh install has no identity to prove with; Start="auto" only brings it back
at the next boot, and a laptop upgraded at eleven is in quarantine until then. An
administrator upgrading by hand runs sc start exordos-nac-agent afterwards. The
agent-driven path already does this for itself.
Nothing halts a rollout on its own when upgraded devices stop proving, and
nothing rolls a device back - those are the next two rails, and until they exist
the way to stop a rollout is share: 0 or removing the file.
9. Backup, restore, upgrade, retention#
What has to be backed up#
Two things, and only one of them is irreplaceable.
| What | Where | If it is lost |
|---|---|---|
| The device-CA private key | /etc/exordos_nac/device-ca.key (ten years by default, and the broker prints how long is left at every start) |
Nothing can bring it back. Every certificate in the fleet was signed by it, and a new CA does not verify them: every device has to be enrolled again, under a new device_uuid, by an operator, in person where enrollment is in person |
| The database | Postgres | Devices, their pinned keys, tags, tokens and the audit trail. A device whose row is gone is answered device_unknown and has to be enrolled again — same cost as above, minus the CA |
| The policy file | /etc/exordos_nac/policy.yaml |
Keep it in version control with the rest of your configuration; it is the matrix, not state |
pg_dump --format=custom "$DATABASE_URI" > nac-$(date +%F).dump
tar -C /etc/exordos_nac -czf nac-config-$(date +%F).tar.gz device-ca.crt device-ca.key exordos_nac.conf policy.yaml
The key belongs wherever secrets of that class already live in your organisation, and not beside the database dump: together they are the whole system, and the threat model this product is built against (server protection) is exactly somebody who has both.
Restoring#
systemctl stop exordos-nac-broker-api
pg_restore --clean --if-exists --dbname "$DATABASE_URI" nac-2026-08-27.dump
tar -C /etc/exordos_nac -xzf nac-config-2026-08-27.tar.gz
systemctl start exordos-nac-broker-api
Nothing has to be told anything. Devices keep proving themselves on the certificates they already hold, and the first proof after the restore re-opens the attachment and puts the grant back on the enforcement point — a restore that loses the last few minutes loses attachments, which are rebuilt in one lease, rather than identity.
What a restore does bring back is a device you deleted or blocked after the
dump was taken. Re-check device list --status blocked against whatever
prompted the block; the audit trail in the dump is what says what was done and
when.
Upgrading the broker#
The schema is migrated forward, and the migration runs while the service is stopped:
systemctl stop exordos-nac-broker-api
pip install --upgrade './exordos_nac-<version>-py3-none-any.whl[attestation]'
ra-apply-migration --config-dir /etc/exordos_nac/ --path "$(exordos-nac-broker migrations-path)"
systemctl start exordos-nac-broker-api
Take the dump first — a migration is the one operation here that changes the database in a way nothing else undoes.
Policy version 2, and the gateways with it, in one window. The release that
adds up every matching rule refuses a version: 1 policy, and the release before
it refuses version: 2 — so the rewrite belongs inside the stop window, not
before it. A broker started on the old file answers every proof and every
gateway pull with PolicyUnavailable, and the fleet is in quarantine one lease
later. The same release compiles grants into one membership set per resource
set, which only a gateway with membership-set enforcement can load, so the
gateways move in the same window:
systemctl stop exordos-nac-broker-api
pip install --upgrade './exordos_nac-<version>-py3-none-any.whl[attestation]'
# Rewrite the policy: every rule that matches now adds its sets. A rule that
# repeated the base only to sit above it can drop the repeat; one that relied
# on shadowing a later rule now gets that rule's sets too. Then:
sed -i 's/^version: 1$/version: 2/' /etc/exordos_nac/policy.yaml
exordos-nac-broker policy check /etc/exordos_nac/policy.yaml # the gate
ra-apply-migration --config-dir /etc/exordos_nac/ --path "$(exordos-nac-broker migrations-path)"
systemctl start exordos-nac-broker-api
# the gateways, by hand, to the release that enforces membership sets
Rehearse it on a copy of the real policy first: policy check runs the file's
own tests, and a test that starts failing after the rewrite is a rule the
union widened.
Agents first, broker second, while any agent is 0.0.3 or older. Those
versions decode the broker's answers strictly: a field they do not know about is
a decode error rather than something to ignore, so a broker that starts sending
one stops them proving themselves — and a device that cannot prove itself cannot
be updated, which takes the way out with it. Roll the agent release out (§8), let
device versions show no device is left on those versions, and upgrade the
broker after that. Later agents ignore fields they do not know, and the order
stops mattering for anything the broker merely adds.
The downtime is the migration: devices retry with backoff and jitter, and access survives it as long as it is shorter than a lease (fifteen minutes). Longer than that and the fleet is in quarantine until it proves itself again, which it does on its own.
nginx before the gateways, this once. The release that gave the enforce loop
its own poll cadence also splits the rate-limit zone in two (§2), and the order
matters in one direction only: an updated gateway polls every three seconds,
which exhausts nac_enroll's burst in about a minute on the old configuration.
A throttled enforcement point stops rewriting elements, and its segment loses
access one lease later.
# 1. nginx: add the two `map`s and the second `limit_req_zone` from
# etc/nginx/exordos-nac-broker.conf, then
nginx -t && systemctl reload nginx
# 2. the gateways, by hand - they are not in the agent release channel (§8)
# A new agent against the old broker asks in full every time, which is what
# it did before, so this step is safe on its own.
# 3. the broker, as above; the migration adds one index
Neither side needs the other's version: the answer's shape has not changed, so an old agent reads a new broker's answer exactly as before, and a new agent against an old broker simply gets no cursor and keeps asking in full. The only thing that is not safe out of order is a gateway polling through a proxy that still carries one zone.
Retention#
Three questions with three different answers, and none of them has a default —
/etc/default/exordos-nac-broker is where the answers get written down:
NAC_SESSION_RETENTION_DAYS=90 # who held an address, and when
NAC_AUDIT_RETENTION_DAYS=400 # what this system did — evidence, normally kept longer
NAC_TOKEN_RETENTION_DAYS=30 # spent and expired tokens; the trail already says who enrolled
An unset number means "keep everything", and the timer says so in the journal
rather than failing. Rehearse with --dry-run before setting one: every prune
command counts without deleting when asked.
audit prune is the one to think about, because the trail grows with the fleet
rather than with operator activity — every proof is a row, which is one per
device every fifteen minutes.
Removing a device#
device block is almost always the right operation: it stops access, records
why, and leaves the machine able to reach the broker and be told it is blocked.
device delete removes the record entirely — for a test enrollment, a scrapped
machine, a duplicate — and it is not a stronger block:
The certificate the machine holds is not revoked (there is no CRL) and stays
valid until it expires, but with no row to authenticate against it is answered
device_unknown. The certificates and sessions go with the record, by the
cascades in the schema; the audit trail stays, because those rows carry a
snapshot rather than a foreign key — what the device did remains answerable after
the device is gone.