AdminService is the one write surface that changes what config is
running, rather than what an object's state is. It carries four RPCs,
ValidateConfig, ApplyConfig, GetConfig and ListAuditRecords, and
it exists only when both -auth-config is set and the process was not
started with -read-only.
All three RPCs act on the -config document only. Identities live in
the separate document -auth-config loads (see
Security); GetConfig never returns them
and ApplyConfig never accepts them, because they are not part of the
document either RPC reads or writes.
1. Admin-gated, and absent without auth
AdminService requires the admin role: viewer and operator
identities are refused with a permission error, the same way
CommandService refuses an unauthenticated caller. With no
-auth-config set at all, the route is not mounted: there is no
identity to require, so a config write over the API is never offered to
an unauthenticated caller. That is the same posture the config file and
the external command FIFO already have for a local operator. See
Security for what -auth-config looks
like and what the three roles mean.
-read-only unmounts AdminService for the same reason it unmounts
CommandService: a process serving queries and the dashboard only has
no write surface to gate, admin role or not.
2. ValidateConfig: check without touching anything
ValidateConfig(cue) runs a whole CUE document past the same loader a
SIGHUP reload uses, without writing anything to disk and without
touching the running config. Success returns an empty response.
Rejection is a transport error (CodeInvalidArgument) carrying a
ConfigDiagnostics detail: one ConfigDiagnostic per problem the
loader found, each naming the file, line, column and message, exactly
as a rejected SIGHUP logs them.
curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ValidateConfig \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <admin identity token>' \
-d '{"cue":"hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}"}'
3. ApplyConfig: persist, then hot-swap
ApplyConfig(cue) runs the identical check and, on success, rewrites
the served -config file with cue's bytes and swaps the running
config through the same fanout a SIGHUP reload uses. Every consumer
that reacts to a reload today, the scheduler, the sinks, the notifier's
macro map, sees an apply exactly the same way. The file is rewritten
before the swap: a process that stops between the two steps still
converges on restart to the config it would have been running anyway.
Success and rejection follow the same shape as ValidateConfig: an
empty response, or a CodeInvalidArgument error carrying a
ConfigDiagnostics detail. A rejected apply changes nothing: the
served file is left untouched and the running config keeps serving what
it was serving before the call.
curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ApplyConfig \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <admin identity token>' \
-d '{"cue":"hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}"}'
A missing cue field is also a CodeInvalidArgument transport error,
but carries no ConfigDiagnostics detail: that is a malformed
request rather than a rejected document. Any other applier failure,
something other than a rejected CUE document, comes back as
CodeInternal, also with no detail.
4. GetConfig: read the served document
GetConfig() takes no fields and returns the whole CUE document
currently served from the -config file, the same text ValidateConfig
and ApplyConfig would each start from. It requires the admin role,
the same as the other two RPCs.
curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/GetConfig \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <admin identity token>' \
-d '{}'
{"cue": "hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}\n"}
A read failure comes back as CodeInternal, the same code an
ApplyConfig failure other than a rejected document returns.
5. ListAuditRecords: read the audit trail
ListAuditRecords(since, limit) returns audit records newest first.
since is optional: an unset value means everything still retained.
limit of 0 means 100; values above 1000 are clamped to 1000.
It requires the admin role, the same as the rest of AdminService.
curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ListAuditRecords \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <admin identity token>' \
-d '{"limit":2}'
{
"records": [
{
"at": "2026-09-07T10:32:11Z",
"source": "AUDIT_SOURCE_API",
"principal": {"name": "jdoe", "role": "ROLE_OPERATOR"},
"peer": "10.0.4.12:53214",
"rpc": "/jaque.v1.CommandService/AcknowledgeProblem",
"commandJson": "{\"Target\":{\"object\":\"web-01/http\"},\"Sticky\":false,\"Author\":\"jdoe\",\"Comment\":\"investigating\"}",
"outcome": "AUDIT_OUTCOME_ACCEPTED"
},
{
"at": "2026-09-07T10:31:58Z",
"source": "AUDIT_SOURCE_EXTCMD",
"rpc": "SCHEDULE_SVC_DOWNTIME",
"commandJson": "{\"Target\":{\"object\":\"web-01/http\"},\"Start\":\"2026-09-07T10:31:58Z\",\"End\":\"2026-09-07T11:31:58Z\",\"Author\":\"nagios\",\"Comment\":\"maintenance\"}",
"outcome": "AUDIT_OUTCOME_ACCEPTED"
}
]
}
A record with no principal is one nothing authenticated the sender
for, the same as the command file's own audit lines. A read failure
comes back as CodeInternal, the same code a GetConfig failure
returns.
6. Profile demo narrows what ValidateConfig/ApplyConfig accept
Under -config-profile demo, both RPCs validate the submitted document
against the narrowed schema instead of the full one: command checks,
command contacts, exec and file sinks, and any non-empty commands
block are rejected the same way an unsupported check type is:
CodeInvalidArgument carrying a ConfigDiagnostics detail, same shape
as the example in Errors below. See CLI flags
for the full list of what the profile refuses.
7. Errors
Every AdminService failure comes back as a Connect transport error, never
as an ok/error field inside the response body. The codes actually
returned:
invalid_argument: a missingcuefield, or a rejected CUE document (ValidateConfig/ApplyConfigonly;GetConfigtakes no fields and never returns this code, andListAuditRecordsnever returns it either). A rejected document additionally carries aConfigDiagnosticsdetail, oneConfigDiagnosticper problem the loader found.unauthenticated: no session cookie or bearer token resolves to an identity.permission_denied: the resolved identity's role is belowadmin.internal: an applier failure other than a rejected CUE document, aGetConfigread failure, or aListAuditRecordsread failure.
A rejected ValidateConfig response body looks like this:
{
"code": "invalid_argument",
"message": "config: invalid:\nhosts.gw.check: unknown check type \"bogus\", must be one of [command dns http icmp legacy passive snmp tcp tls wasm]:\n config.cue:3:2\n",
"details": [
{
"type": "jaque.v1.ConfigDiagnostics",
"value": "<base64-encoded protobuf>",
"debug": {
"diagnostics": [
{
"message": "hosts.gw.check: unknown check type \"bogus\", must be one of [command dns http icmp legacy passive snmp tcp tls wasm]",
"file": "config.cue",
"line": 3,
"column": 2
}
]
}
}
]
}
ApplyConfig rejects the same way, and leaves the served file and running
config untouched.
8. Config stays code
Every RPC takes or returns a whole document, never a partial shape for
one host or one check. Pipelines that render CUE and commit it remain
the recommended way to manage a fleet's config in a real estate. This
service is the admin convenience and the playground's upload path,
alongside that workflow rather than in place of it. A caller that wants
to add one object resolves the current config with GetConfig, edits
its own copy, and calls ApplyConfig with the whole result.
9. Security considerations
An admin identity that can call ApplyConfig can rewrite the entire
served config, a strictly larger blast radius than anything an
operator identity can do through CommandService. Treat an admin
session or token with the same care as shell access to the host: rotate
-session-secret on any suspicion a session leaked, the same guidance
Security gives for every other role.