SSH Keys — API v1 and MCP | Nodea Documentation
The SSH Keys module manages the user's public SSH keys — the ones deployed to servers during provisioning. It covers listing, adding a key, renaming it and deleting it. This document covers the REST API v1 surface (/api/v1/ssh-keys) and the MCP tools in this module.
Authentication and scopes
API v1 authenticates with a Passport personal access token via the Authorization: Bearer <token> header. Access tiers:
infra:read— list keys.infra:write— add a key and rename it.infra:delete— delete a key.
The whole module is additionally gated by the Pennant flag features:servers — disabling the servers module hides SSH keys on both web UI and API/MCP. Rate limit: throttle:api-v1 = 240/min per token user.
Ownership and security
Row-level access is enforced by UserSshKeyPolicy (owner-only for update/delete). Listing is owner-scoped by construction. A foreign id on edit/delete yields 403. There is no sharing model for SSH keys.
Important: the content column holds only the public key (encrypted at rest), so returning it is safe — private keys never enter this table. Never submit a private key. Key material is immutable: editing only changes the display name (replacing a key = delete + create).
API v1 — endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/ssh-keys | infra:read | The user's keys, paginated (newest first). |
| POST | /api/v1/ssh-keys | infra:write | Add an OpenSSH public key. Returns 201. |
| PUT/PATCH | /api/v1/ssh-keys/{sshKey} | infra:write | Rename a key's display name (owner-only). |
| DELETE | /api/v1/ssh-keys/{sshKey} | infra:delete | Delete a key (owner-only). Returns 204. |
GET /ssh-keys — parameters and response
per_page(int 1–100, default 20),page(int ≥1).
Response: a UserSshKeyResource collection — fields: id, name, for_root (bool), public_key (the key content), fingerprint (SHA256, ssh-keygen -lf style — SHA256:... without padding; null when the content is not parseable as <type> <base64> [comment]), created_at, updated_at.
POST — body parameters
name(string, required, min 3, max 255) — display name.content(string, required, min 3, max 10000,ValidPublicSshKeyrule) — public key material in OpenSSH format. The rule round-trips the key throughssh-keygen -l, so invalid material yields422.for_root(bool, optional) — whether the key should be installed for the root user during provisioning.
PATCH — body parameters
name(string, required, min 3, max 255) — the only mutable field.
Error codes
401— missing/invalid token.403— missing required scope or a foreign key (owner-only policy).404— unknown id.422— validation (invalid public key, name too short/long).
Examples — API
List keys
curl -s https://app.nodea.io/api/v1/ssh-keys \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
Add a public key (for root)
curl -s -X POST https://app.nodea.io/api/v1/ssh-keys \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "laptop-ed25519",
"content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host",
"for_root": true
}'
Rename a key
curl -s -X PATCH https://app.nodea.io/api/v1/ssh-keys/17 \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "work-laptop" }'
MCP — tools
The MCP tools share the authentication and scope model with API v1 (Bearer + scope checked in ensureScope) and the features:servers flag (a tool on an inactive flag disappears from tools/list). Keys are addressed by integer id; a missing or foreign id raises the same error (no cross-tenant enumeration). The output uses the same shape as the API (UserSshKeyResource).
| Tool | Scope | Mode | Description |
|---|---|---|---|
| ssh-keys-list | infra:read | read-only | List keys; page, per_page (max 50). Returns id/name/for_root/public_key/fingerprint/dates. |
| ssh-key-create | infra:write | write | Add an OpenSSH public key; name (min 3), content, for_root. Validated via ssh-keygen. Never pass a private key. |
| ssh-key-delete | infra:delete | destructive | Delete a key by id; requires confirm: true. Keys already deployed to servers are not removed from them. |
Example — MCP (JSON-RPC)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ssh-key-create",
"arguments": {
"name": "laptop-ed25519",
"content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host",
"for_root": true
}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "ssh-key-delete",
"arguments": { "id": 17, "confirm": true }
}
}