Admin tools
Operator accounts
Section titled “Operator accounts”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:
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).
Operator sessions need a second factor
Section titled “Operator sessions need a second factor”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.
Profiles from the command line
Section titled “Profiles from the command line”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.
opdns-cp admin profile get abc123 --documentopdns-cp admin profile set abc123 settings.block_mode=refused --reason "ticket 812" --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.
Suspend a profile
Section titled “Suspend a profile”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.
opdns-cp admin profile suspend abc123 --reason "abuse ticket 812" --yesopdns-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.
Operator blocks from the command line
Section titled “Operator blocks from the command line”The operator blocks of the dashboard’s Operator pages can also be managed without the API:
opdns-cp admin operator block listopdns-cp admin operator block add --pattern example.com --reason-code legal_order \ --public-ref "ORDER-2026-17" --jurisdictions FR --expires-in 720h --yesopdns-cp admin operator block remove <id> --yesadd 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.
List reports from the command line
Section titled “List reports from the command line”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
Section titled “Operator audit”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.
Dashboard error reports
Section titled “Dashboard error reports”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).
Role admin endpoints
Section titled “Role admin endpoints”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).
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.