Skip to content

Rotate this device's own tunnel credential

POST
/api/v1/tunnel-credential/rotate
curl --request POST \
--url https://example.com/api/v1/tunnel-credential/rotate \
--header 'X-API-Key: example' \
--header 'X-Device-ID: example'

Revoke the calling device’s active sotc_ tunnel credential(s) and return a new one — authenticated by the device’s OWN API key.

Why this exists (BUG-367): an already-provisioned board whose tunnel credential is stale used to re-mint it only through the operator route POST /api/devices/{id}/tunnel-credential/rotate, which needs an admin JWT. otapulse-auto-provision therefore logged in as an admin from the device, which can never work against an MFA-mandatory admin, needs admin credentials on the device, and the only MFA-free alternative (re-provisioning with a provisioning token) rotates the device’s API key as a side effect. This route heals ONLY the tunnel credential.

Authorization — a key can only ever mint a credential for its own device:

  • X-API-Key must be an active, unexpired key (401 otherwise).
  • The key must be DEVICE-BOUND (api_keys.device_id set). Org-scoped service keys are refused (403) even though other device routes accept them: minting a remote-shell credential is not something a fleet-wide key may do for an arbitrary device.
  • X-Device-ID resolves only within the key’s organization (404 for a device of another org) and must be the key’s bound device (403 for any other device) — the GAP-BE-014 rule get_device_from_header already enforces, re-asserted here.
  • The key needs devices:write and the device must be active (403).

Rate limited per source IP (per minute) and per device (per hour), and every successful rotation writes an audit-log row.

X-API-Key
required
X-Api-Key
string
X-Device-ID
required
X-Device-Id
string

Successful Response

Media type application/json
DeviceTunnelCredentialRotateResponse

The operator rotate response plus the caller’s own backend device UUID, which otapulse-auto-provision records as device_uuid= in its tunnel marker (engine/provision.py cross-checks that value).

object
device_id
required
Device Id
string
rotated_at
required
Rotated At
string format: date-time
rotation_count
required
Rotation Count
integer
tunnel_token
required
Tunnel Token
string
tunnel_token_prefix
required
Tunnel Token Prefix
string
Example generated
{
"device_id": "example",
"rotated_at": "2026-04-15T12:00:00Z",
"rotation_count": 1,
"tunnel_token": "example",
"tunnel_token_prefix": "example"
}

Validation Error

Media type application/json
HTTPValidationError
object
detail
Detail
Array<object>
ValidationError
object
ctx
Context
object
input
Input
loc
required
Location
Array
msg
required
Message
string
type
required
Error Type
string
Example generated
{
"detail": [
{
"ctx": {},
"input": "example",
"loc": [
"example"
],
"msg": "example",
"type": "example"
}
]
}