Skip to content

Enrol a node

Enrolment turns a fresh node into a managed one: it exchanges a one-time code for a long-lived node token, stores it in its data directory, and opens its link to opdns.

  1. In the dashboard, open the profile’s Nodes page and click Add a node. Name it and click Create enrolment code.

    You get a code that starts with opdnsenrol_. It works once and expires after 15 minutes. The dialog also shows the commands below with the code filled in.

  2. On the node, use the code:

    Pass it in the environment on first start; the node enrols itself:

    Terminal window
    docker run ... -e OPDNS_ENROL_TOKEN=opdnsenrol_... ghcr.io/operomtl/opdns-node:<version>

    The full command is on Install with Docker.

  3. Start (or restart) the node. Within seconds the dashboard’s Nodes page shows it online, with its versions.

  4. Point your network’s DNS at the node’s LAN address.

The data directory gets node.json (mode 0600): the node id, the node token (opdnsnode_…), the profile id and the link and profile URLs the cloud handed out. The one-time code is not stored, only its hash, so enrolling again with the same code is a no-op instead of an error. It also gets profile_keys.json: the keys the cloud signs your profile with, which the node checks every profile against from then on (Profile signing keys).

The node token is the node’s only credential, so it does not live forever. A connected node rotates it by itself every 90 days (link.token_rotation; 0 turns this off): over its link it asks the cloud for a new token, writes it to node.json (atomically, so a power cut leaves either the old or the new token), switches to it, and confirms. The previous token keeps working for 10 minutes after that, so nothing in flight breaks. Each rotation is audited (node.token_rotated, shown as Node token rotated in the dashboard’s activity lists), and the node’s record carries the time of the last one (token_rotated_at in the API).

To rotate now, for example after the data directory was copied somewhere it should not be:

Terminal window
sudo -u opdns opdns-node rotate-token
rotated the token of node 7c9e…; the new token is saved in /var/lib/opdns/node.json
the previous token keeps working for 10m0s; a running node switches to the new one within seconds

It works whether or not the node is running: a running node picks up the new token from node.json within about 10 seconds and reconnects with it. Only the current token can rotate; the previous one, during its 10 minutes, is refused with node_token_superseded. The local page shows when the token was issued, how often it rotates and the last failed attempt, if any. A token you think has leaked is better handled by Revoke on the Nodes page, which stops it at once.

To move a node to new hardware or a new container without a new code, keep the data directory (it has the token, the last profile and the logs), or enrol the new install with the old node token from node.json:

Terminal window
opdns-node enrol --token opdnsnode_...

The node asks the cloud which node the token belongs to. With --node-id <id> it skips that and enrols offline.

A node that already runs in standalone mode (a profile_file, or an inline profile: in its config) sends that local profile along with the enrolment code, so your dashboard profile starts out as a copy of what the node was doing:

Terminal window
sudo -u opdns opdns-node enrol --token opdnsenrol_...

Before sending anything it shows what it will upload and asks:

enrolling uploads the local profile (profile_file /etc/opdns/profile.yaml) and replaces the dashboard profile of this code with it:
"Home": 3 deny and 1 allow rules, 1 rewrites, 2 lists, block mode nxdomain, logs on
the cloud refuses it when that profile already has settings or rules; --keep-cloud-profile enrols without uploading
replace the dashboard profile with the local profile? [y/N]

Answering anything but y cancels: nothing is sent and the code stays valid. Off a terminal (a script, a provisioning tool) the question cannot be asked, so the command refuses unless you pass --yes. Scripts that enrolled a standalone node without it need the flag added.

After the upload:

the local profile (profile_file /etc/opdns/profile.yaml) is now the cloud profile: edit it in the dashboard from here on
the local profile is archived in /var/lib/opdns/archive/standalone-profile-<time>.json; the node now pulls the cloud profile
note: profile_file is no longer read (the node follows the cloud profile); remove it from the config

The uploaded profile is kept in <data_dir>/archive/, and node.json records where it came from.

The cloud takes the local profile only while its own profile is untouched (never edited, no rules). Otherwise the enrolment is refused with profile_not_empty: nothing is enrolled and the code stays valid. Then either

  • enrol with --keep-cloud-profile to leave the local profile out, so the node follows the dashboard’s profile as it is; or
  • reset the profile in the dashboard and enrol again.

Only a fresh enrolment code uploads the profile: re-installing with a node token (opdnsnode_…) and enrolling from OPDNS_ENROL_TOKEN never do.

After enrolling, remove profile_file (and mode: standalone, if set) from the config; the enrol command reminds you. A profile_file whose profile was uploaded is no longer read: the node follows the dashboard. One that was not uploaded (with --keep-cloud-profile, or when re-installing with a node token) still wins over the dashboard’s profile for as long as it is set.

Stop the node first (sudo systemctl stop opdns-node, or stop the container), then:

Terminal window
sudo -u opdns opdns-node unenrol
unenrolled node 7c9e…: revoked in the cloud; token, log cursor and learnt list keys removed
query logs, the last profile and lists are kept (--purge-logs deletes the logs)

It first revokes the node in the cloud with its own token, which also closes its link session, as Revoke on the Nodes page would. Then it removes the node token (node.json), the log cursor and the signing keys learnt from the cloud (lists_keys.json for lists, profile_keys.json for profiles). Logs, the last profile and the list artifact stay on disk; --purge-logs deletes the logs too.

The cloud call gives up after a few seconds. If opdns cannot be reached, unenrol goes on locally and prints a note asking you to revoke the node on the Nodes page, so its token stops working. A node the dashboard already revoked or deleted counts as done. --local-only skips the cloud call.

After unenrolling, the node does not serve DNS on its next start: like a fresh install, it prints how to enrol and exits with status 2. Enrol it again with a new code, or give it a profile for standalone mode. It never falls back to resolving unfiltered. If OPDNS_ENROL_TOKEN (enrol.token) is still set, the next start enrols again with it; unenrol says so. Remove it to stay unenrolled.

A node with no identity, no enrolment code and no standalone profile does not serve DNS. opdns-node run prints:

opdns-node: this node is not set up yet, so it is not serving DNS.
1. In the opdns dashboard, open Nodes → Add node and copy the enrolment code.
2. Enrol: opdns-node enrol --token <code>
(containers: set OPDNS_ENROL_TOKEN=<code> instead; it enrols on start)
3. Start: opdns-node run

and exits with status 2, which the systemd unit does not restart.

An enrolled node opens two kinds of outbound connection: HTTPS to cloud.api_url (https://api.opdns.io: enrolment, profile and list pulls) and the WebSocket link to cloud.link_url (wss://link.opdns.net/link). Both use TLS 1.3 only, verified against the system’s certificate authorities (plus cloud.ca_file), and so do list artifact downloads. cloud.proxy sends them through an HTTP proxy instead of HTTPS_PROXY; its password is never printed or logged.

cloud.spki_pins additionally pins the keys of those two hosts: a list of sha256/<base64> hashes of a certificate’s SubjectPublicKeyInfo, of which at least one must appear somewhere in the verified chain. It is off by default, because a TLS-inspecting proxy or a change of certificate authority breaks a pinned node until you update the pins. List artifacts are never pinned: their Ed25519 signature protects them wherever they come from.

/etc/opdns/node.yaml
cloud:
spki_pins:
- sha256/<base64 of the SPKI hash>
Symptom Cause
invalid_token the code was used already, mistyped, or older than 15 minutes; create a new one
node stays offline in the dashboard outbound HTTPS to api.opdns.io and link.opdns.net blocked, or a TLS-inspecting proxy in the way (connections to opdns); set cloud.proxy if you need a proxy
status says “revoked” the node was revoked in the dashboard; it keeps resolving with its last profile. Enrol again or switch to standalone
exits with status 2 and “this node is not set up yet” no identity and no enrolment code (also with mode: enrolled set): enrol it
exits with status 1 right after start with OPDNS_ENROL_TOKEN set the code was refused (see invalid_token above) or the cloud was unreachable; the log says which
pass --yes to upload the local profile when enrolling from a script the node has a local profile and no terminal to confirm the upload: add --yes, or --keep-cloud-profile to leave it out (Enrol a standalone node)
node_token_superseded a request used the previous node token after a rotation (a copy of an old node.json, or a second install): use the current token, or rotate again from the running node
profile_not_empty when enrolling a standalone node the dashboard profile already has settings or rules; enrol with --keep-cloud-profile or reset the profile first
unenrol prints a note about the dashboard the cloud could not be reached; revoke the node on the Nodes page