Add an SSH connection
Adding a connection is three things: pin the host, seal the key, and declare the capabilities. The perma CLI does all of it in one command; the REST API does it in a few calls.
1. Pin the host's fingerprint
The connection refuses to connect unless the server presents this exact key, so grab it first:
FP=$(ssh-keyscan -t ed25519 db1.example.com | ssh-keygen -lf - | awk '{print $2}')
echo "$FP" # SHA256:9Ije0/h/UFCjrsPr+a6o/dPtQhd0p9AbIQOW10Q7wJo
2. Create the connection
- perma CLI
- REST API
One command creates the connection, seals the key, and grants it to an agent:
perma connection add-ssh db1 \
--host db1.example.com --user deploy \
--host-key-sha256 "$FP" \
--key-file ~/.ssh/db1_ed25519 \
--sftp-root /var/data \
--exec 'uptime:uptime' \
--exec 'restart:systemctl,restart,{service}' \
--agent codex-cli
--exec 'action:program,arg,arg'defines one exec capability. A{name}argument becomes an allow-listed placeholder the agent may fill.--sftp-rootexposessftp.get/sftp.put/sftp.list, all scoped under that directory.- Add
--require-approvalto gate the exec commands on a human. (Writes andsftp.putare higher-risk and escalate automatically regardless.) - For a local or dev host with no fingerprint,
--insecureskips verification. Do not use it against a host reached over an untrusted network.
The same thing over REST v1 ($TOKEN is your operator token):
BASE=http://127.0.0.1:8787
# a) the connection: host/user/host-key pinned, capabilities declared
curl -X POST $BASE/v1/connections -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
"id": "db1", "connector": "ssh", "name": "DB box",
"ssh": {
"host": "db1.example.com", "port": 22, "user": "deploy",
"host_key_sha256": "SHA256:9Ije…",
"sftp_root": "/var/data",
"exec": [
{ "action": "uptime", "args_template": ["uptime"] },
{ "action": "restart", "args_template": ["systemctl","restart","{service}"],
"allowed_placeholders": ["service"], "risk": "high", "requires_approval": true }
]
}
}'
# b) seal the private key (never returned)
curl -X POST $BASE/v1/connections/db1/credential -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d "{\"value\": $(jq -Rs . < ~/.ssh/db1_ed25519)}"
# c) allow the actions and grant them to an agent
curl -X POST $BASE/v1/policies -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
"connection_id": "db1", "allow": ["uptime","restart","sftp.list","sftp.get"] }'
curl -X POST $BASE/v1/grants -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
"policy_ids": ["<the pol_… id returned above>"], "agents": [{ "agent_id": "codex-cli" }] }'
What you get
The connection now exposes these capabilities (namespaced under its id):
| Capability | What it does |
|---|---|
db1.uptime | runs uptime on the host |
db1.restart | runs systemctl restart <service> (approval-gated) |
db1.sftp.list | lists a directory under /var/data |
db1.sftp.get | reads a file under /var/data |
db1.sftp.put | writes a file under /var/data (higher-risk) |
Use it from an agent
Point an MCP client (Claude, ChatGPT, Cursor) at your gateway and the capabilities appear as tools named db1.uptime, db1.sftp.get, and so on. The agent calls them like any other tool:
db1.uptimewith{}returns the realuptimeline from the host.db1.sftp.getwith{ "path": "reports/q3.csv" }returns the file's bytes (base64), scoped under the root.db1.restartwith{ "service": "nginx" }returns awaiting approval and prompts you on your device; it runs only once you approve.
The key never leaves the gateway, and every call lands in the signed audit log.
Notes
- The key you seal must be unencrypted (this build authenticates with no passphrase). Generate a dedicated one with
ssh-keygen -t ed25519 -f ~/.ssh/db1_ed25519 -N ''and authorise it on the host. - Password auth is supported with
--password-env VAR(the value is read from that environment variable and sealed, never placed on a command line). - Check what is configured any time with
perma connection list.