Devices
Enrol, list and revoke the machines that hold a device key, fetch a device's wrapped master key, and find your own device over HTTP.
A device is a machine that holds one half of a key pair and, through it, a
wrapped copy of the vault's master key. Enrolling, listing and revoking devices are
reserved for operator principals; fetching a wrapped key and finding your own
device are open to operator and service principals. No agent reaches any of
these — an agent reaches no /v1 route at all.
A device's public_key is one of two things, depending on its provider: for a
hardware provider (secure_enclave, tpm, dpapi, secret_service,
onepassword) it is a P-256 public key; for kms_task_role it is an AWS Key
Management Service key identifier, which holds no signing key of its own.
POST /v1/devices — enrol a device
A new device is enrolled only when an already-enrolled device approves it, by signing an approval the server verifies. The very first device in a vault arrives with no approver.
The wrapped_master_key in the body is the vault's master key, wrapped for this
device. Who can unwrap it depends on the device's provider:
- For a P-256 device-key provider (
secure_enclave,tpm,dpapi,secret_service,onepassword), the copy is wrapped to the device's own P-256 public key, and only the holder of that device's private key can unwrap it. - For
kms_task_role, there is no device key: the copy is wrapped under an AWS symmetric KMS key with anEncryptcall, and unwrapping is aDecryptcall against that KMS key. Anyone who can make thatDecryptcall — the headless machine's cloud role, not the device alone — can unwrap it.
Where the wrapped copy comes from differs for the first device and every one after:
- The first device generates the vault's master key and wraps it for itself —
so the
wrapped_master_keyyou send is that fresh key, wrapped. It has no approver because there is no earlier device to approve it. - A later device is given a wrapped copy by an already-enrolled device, which
holds the master key and wraps it for the new device. That same enrolled device
also signs the
approval.
You do not assemble these bytes by hand. They are produced on an enrolled device by
the custodian command line — enrolling the first device, and
approving each later one — as described in Using the HTTP API.
POST /v1/devices
Authorization: Bearer <operator token>
Content-Type: application/json
{
"device": {
"label": "ops-laptop",
"provider": "secure_enclave",
"public_key": "<base64>"
},
"wrapped_master_key": {
"ciphertext": "<base64>",
"algorithm": "ECIES-P256-HKDF-SHA256-XCHACHA20-POLY1305"
},
"approval": {
"approver_device_id": 4242,
"issued_at": "2026-08-28T10:00:00Z",
"signature": "<base64>"
}
}The approval is only fresh for a short, configured window — the ceremony is two
machines side by side and takes seconds. An approval older than the window is
refused with a plain 422; the window is set by
CUSTODIAN_ENROLMENT_APPROVAL_MAX_AGE_SECONDS (see
Deployment).
On success the response is 201 Created:
{
"device": {
"id": 4243,
"label": "ops-laptop",
"provider": "secure_enclave",
"enrolled_by": 4242,
"enrolled_at": "2026-08-28T10:00:05Z",
"revoked": false
}
}Every failure of the approval itself — an approver that does not exist, belongs to
another vault or is revoked; a missing or invalid signature; a first-device
enrolment attempted while a live device already exists — takes the one shared
not_found, so the route cannot be used to discover whether a device exists.
GET /v1/devices — list devices
GET /v1/devices
Authorization: Bearer <operator token>{
"devices": [
{
"id": 4242,
"label": "founder-laptop",
"provider": "secure_enclave",
"enrolled_by": null,
"enrolled_at": "2026-08-12T09:02:00Z",
"last_seen_at": "2026-08-27T14:11:07Z",
"revoked_at": null
}
]
}Revoked devices are listed too. The list never carries the wrapped ciphertext or the
raw public key. enrolled_by is the approving device's identifier, or null for a
first device.
DELETE /v1/devices/:id — revoke a device
Deletes the device's wrapped copy of the master key and stamps revoked_at. The
devices row stays, so its place in the enrolment chain survives.
DELETE /v1/devices/4243
Authorization: Bearer <operator token>{
"device": { "id": 4243, "revoked_at": "2026-08-28T10:05:00Z" },
"message": "Removed this device's wrapped copy of the master key. From its next run this device cannot decrypt client-encrypted values. Its API credential still works and still reads plain, masked and tokenised values — revoke the principal to stop that. Past access is not undone: a device that already unwrapped and retained the master key still holds it. After an actual compromise, run `custodian key rotate`."
}Read that message: revoking a device is not revoking access. The machine's
credential still authenticates and still reads every value that is not
client_encrypted, and a master key already unwrapped and retained is not recalled
by deleting a row.
When the revocation leaves the vault with no live device at all, the response
carries a warning naming what that means — that the master key is now reachable only
through the recovery envelope, or, if none is stored, that
every client_encrypted value in the vault is now unreadable.
GET /v1/devices/:id/wrapped-key — fetch a wrapped master key
Returns the master key wrapped to one live device, and stamps its last_seen_at.
Open to operator and service principals — a headless machine drives this as a
service to collect its own copy.
GET /v1/devices/4243/wrapped-key
Authorization: Bearer <token>{
"wrapped_key": {
"device_id": 4243,
"ciphertext": "<base64>",
"algorithm": "ECIES-P256-HKDF-SHA256-XCHACHA20-POLY1305"
}
}A revoked device, another vault's device and an unknown identifier all take the same
not_found, so the route cannot be turned into a device-existence oracle. What it
hands back is a wrapped copy, not the master key itself: for a P-256 device-key
provider it is inert to anyone who does not hold that device's private key, and for a
kms_task_role device it is an AWS KMS ciphertext that a Decrypt call against that
KMS key can unwrap.
A principal that is neither an operator nor a service is refused with the
caller_not_operator code, its message naming that this route is open to operator
and service principals and stating the caller's own kind.
POST /v1/devices/self — find your own device
A machine needs its own device identifier before it can fetch its wrapped key, and no other route gives it one. It submits the key identity it holds — base64-encoded — and receives that device's identifier, and nothing else.
POST /v1/devices/self
Authorization: Bearer <token>
Content-Type: application/json
{ "device": { "public_key": "<base64 of the key identity>" } }{ "device": { "id": 4243 } }The bytes are matched whole and exactly against the enrolled public_key, over live
devices only. A key naming no live device in your vault — a revoked device, another
vault's, an unknown or a partial key — takes the same not_found, so the route
cannot be used to discover whether a key is enrolled. It writes nothing and stamps no
timestamp.