Custodian
ReferenceHTTP API

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 an Encrypt call, and unwrapping is a Decrypt call against that KMS key. Anyone who can make that Decrypt call — 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_key you 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.

On this page