Skip to content

Admin tools

The admin scope, which the Operator pages of the dashboard and the /v1/admin/... routes need, is never granted through the API. It is a flag on the account, set from a machine that can reach the control plane’s database:

Terminal window
opdns-cp admin grant --email [email protected] --reason "on-call rota" --yes
opdns-cp admin revoke --email [email protected] --yes
opdns-cp admin list
Subcommand Does
grant --email X flags the account as an operator
revoke --email X removes the flag
list prints every operator account: email, account id, organisation, creation date

The account must exist (sign up first); an unknown address exits 1. Outside env=dev, grant and revoke refuse to run without --yes. Granting an account that already is an operator changes nothing and says so. Each change is audited as admin.granted or admin.revoked, with the actor cli:<user>@<host> (the operating-system user who ran it) and the optional --reason. The command takes the same database settings as the other opdns-cp roles and refuses to run against a database whose migrations are behind (opdns-cp migrate first).

Outside env=dev, the admin scope needs more than the flag: every /v1/admin/... route, and creating an API token with the admin scope, needs a session that signed in with a passkey, or with a password and a second factor (an authenticator code or a recovery code). A session that signed in with the password alone (an account without an authenticator app can, even when it has a passkey) gets 403 mfa_required, and the dashboard’s Operator pages say Operator pages need a sign-in with a passkey or authenticator app with a link to Security. An admin-scoped token, once created, is not checked again: it can only have been minted from such a session. The command line below talks to the database directly and is not affected.

opdns-cp admin profile reads and changes customer profiles, for support and incidents. Writes go through the same write path as the API (version bump and outbox row in one transaction, so the change is published to the PoPs like any other) and are audited as profile.create or profile.update, with the before and after values, the actor cli:<user>@<host> and the optional --reason.

Terminal window
opdns-cp admin profile list --email [email protected]
opdns-cp admin profile get abc123 --document
opdns-cp admin profile set abc123 settings.block_mode=refused --reason "ticket 812" --yes
opdns-cp admin profile create --email [email protected] --name Home --yes
Subcommand Does
list [--email X] [--all] [--limit 50] profiles across accounts, newest first (--all includes deleted ones)
get ID [--document] a profile as JSON, or with --document the document the publisher sends to the PoPs (rules, lists, linked IPs)
create --email X [--name N] a new profile for that account
set ID [--version N] [--json BODY | --file F | KEY=VALUE...] a partial update, the body of PATCH /v1/profiles/{id}; KEY is name or section.field (security, parental, settings), VALUE JSON or a bare string; --version fails the write if the profile changed since you read it

The profile pipeline itself (stream, replay, reconcile) has its own commands: Profile propagation.

Suspending a profile stops its filtering and logging without breaking the customer’s network: it keeps answering, unfiltered and unlogged, until it is reinstated. What the customer sees is on Profiles.

Terminal window
opdns-cp admin profile suspend abc123 --reason "abuse ticket 812" --yes
opdns-cp admin profile unsuspend abc123 --yes
Subcommand Does
suspend ID --reason R suspends the profile; the reason is required
unsuspend ID [--reason R] reinstates it

The same from the API, with an admin session or token: GET /v1/admin/profiles/{id} returns the operator view (suspended, suspended_reason, suspended_at, suspended_by), and PATCH /v1/admin/profiles/{id} with {"suspended": true, "reason": "…"} (the reason is required to suspend, at most 200 characters, and ignored when reinstating) changes it.

Either way the change goes through the profile write path (new version, outbox row), so every PoP and enrolled node applies it within seconds like any profile change. On them the profile compiles to no lists, rules, rewrites, security or parental controls, with logs off, no client subnet and no cache boost; it keeps its id, linked IPs and retention, and the edge writes neither a log record nor a counter for it. Operator blocks still apply. A request that changes nothing (suspending a suspended profile) writes nothing and says so.

Each change is audited as profile.suspended or profile.unsuspended in the profile’s organisation. The reason (and the previous one) is in the operator audit only: the customer’s Activity shows the entry without it, and the reason never reaches the profile API, the node’s document or the customer’s data export. While suspended, the customer’s writes to the profile get 409 profile_suspended; admin profile set, DDNS updates by token and a node’s own calls still work. A self-hosted node older than this feature refuses the document with the new fields and keeps its last one, so it goes on filtering.

The operator blocks of the dashboard’s Operator pages can also be managed without the API:

Terminal window
opdns-cp admin operator block list
opdns-cp admin operator block add --pattern example.com --reason-code legal_order \
--public-ref "ORDER-2026-17" --jurisdictions FR --expires-in 720h --yes
opdns-cp admin operator block remove <id> --yes

add takes --pattern, --reason-code (legal_order, abuse or csam), and optionally --public-ref, --note, --pops, --jurisdictions and --expires-in; list --all includes removed and expired entries, --json prints JSON. A change writes the row, operator/blocks.json and the audit entry (admin.operator_block_created or admin.operator_block_removed) in one transaction, the same function the admin API uses, then announces it to the PoPs over NATS (--nats-url; without NATS they pick the file up by polling). The file goes to the --s3-* bucket, or --export-dir without one, as for the api role.

Outside env=dev, every admin write (grant, revoke, profile create|set|suspend|unsuspend, operator block add|remove, profiles republish, profiles reconcile --fix) refuses to run without --yes.

opdns-cp admin list-reports export --accepted turns the list reports operators accepted into candidate lines for the list repository; it is read-only. See List reports.

Operator → Audit in the dashboard is every organisation’s audit log, newest first, read-only: sign-ins, credential and token changes, profiles, nodes, exports, deletions and operator actions. Filter it by actor (exact, such as account:<id> or cli:alice@ops1), by action (exact, such as auth.login_failed, or a prefix ending in ., such as admin.), and by a time range; the filters are kept in the address, so a filtered view can be shared. Load more fetches older entries.

The API is GET /v1/admin/audit with actor, action, from, to and the usual limit and cursor (scope admin). An organisation’s own members see only their organisation’s entries, on Account → Activity.

Uncaught dashboard errors reported by browsers (scrubbed of names, addresses, emails and ids before sending) are kept 30 days. There is no page for them yet; list them with GET /v1/admin/client-errors (scope admin, paginated, newest first).

Every opdns-cp role serves /metrics, /healthz, /readyz and /livez on its admin port (--admin-addr, default :9090). Two roles add routes that act:

Role Route Does
certs POST /admin/rotate start a forced certificate rotation on the leader; waits up to ?wait= (default 45 s, at most 55 s): 200 done, 202 still running, 500 failed, 409 not the leader, 503 busy
certs GET /admin/rotate the last rotation since the instance started (404 when none)
listc POST /admin/build run a list build cycle now; waits up to ?wait= (default 50 s, at most 55 s): 200 done, 500 failed, 202 still running, 409 another on-demand build running, 503 the compiler not running
listc GET /admin/build the last on-demand build (404 when none)
listc GET /admin/rollout, POST /admin/rollback, POST and DELETE /admin/pin list rollouts

These routes are guarded the same way everywhere: with OPDNS_ADMIN_TOKEN set on the role, a request needs Authorization: Bearer <token> (401 otherwise); without it, only loopback clients are served (403 otherwise). Outside env=dev, every role refuses to start with a secret the repository publishes for the simulation: the dev admin token, the dev S3 secret, or a secrets keyring holding the simulation’s key (or any key named dev).

Terminal window
curl -X POST -H "Authorization: Bearer $OPDNS_ADMIN_TOKEN" \
"http://127.0.0.1:9090/admin/build?wait=30s"

opdns-cp certs --once --force, run while another instance leads, asks that leader to rotate through POST /admin/rotate (--leader-admin-url, default http://127.0.0.1:<admin-addr port>, with OPDNS_ADMIN_TOKEN) and follows the rotation. Sending the leader SIGUSR1 also rotates.