Nodea — logo

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

MethodPathScopeDescription
GET/api/v1/ssh-keysinfra:readThe user's keys, paginated (newest first).
POST/api/v1/ssh-keysinfra:writeAdd an OpenSSH public key. Returns 201.
PUT/PATCH/api/v1/ssh-keys/{sshKey}infra:writeRename a key's display name (owner-only).
DELETE/api/v1/ssh-keys/{sshKey}infra:deleteDelete 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, ValidPublicSshKey rule) — public key material in OpenSSH format. The rule round-trips the key through ssh-keygen -l, so invalid material yields 422.
  • 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).

ToolScopeModeDescription
ssh-keys-listinfra:readread-onlyList keys; page, per_page (max 50). Returns id/name/for_root/public_key/fingerprint/dates.
ssh-key-createinfra:writewriteAdd an OpenSSH public key; name (min 3), content, for_root. Validated via ssh-keygen. Never pass a private key.
ssh-key-deleteinfra:deletedestructiveDelete 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 }
  }
}