Share SSH access (no API key)
If you want someone else to SSH to a specific Morph instance, you do not need to share your Morph API key. Instead, use a two-phase handoff: a trusted operator mints instance-scoped SSH credentials once (using MORPH_API_KEY), then shares only an instance-scoped access bundle with the recipient.
Two-phase handoff
- Phase 1 (trusted operator, API domain): Use
MORPH_API_KEYonce to fetch the instance’s SSH credentials and write an access bundle file. - Phase 2 (recipient, SSH domain): Use only the access bundle to SSH to
ssh.cloud.morph.so. No Morph API key is needed.
What to share (and what not to share)
- Share: the instance-scoped access bundle for the specific instance.
- Do not share:
MORPH_API_KEYor any org/user automation credentials used to mint bundles.
The access bundle contains a private key and a password. Treat it like a secret (equivalent to SSH credentials): don’t paste it into tickets or chat, don’t commit it, and store/share it only via approved secure channels.
Option A: Mint a bundle with the SDK (one-time API key use)
Write a bundle to disk with restrictive permissions (e.g., 0600) and share only that file.
- Python
- TypeScript
import json
import os
import stat
from pathlib import Path
from morphcloud.api import MorphCloudClient
instance_id = "morphvm_abc123" # Replace with your instance ID
bundle_path = Path("./ssh-access-bundle.json")
client = MorphCloudClient(api_key=os.environ["MORPH_API_KEY"])
instance = client.instances.get(instance_id=instance_id)
ssh_key = instance.ssh_key() # one-time API call for instance-scoped credentials
bundle = {
"hostname": "ssh.cloud.morph.so",
"port": 22,
"username": instance_id,
"private_key": ssh_key.private_key,
"password": getattr(ssh_key, "password", None),
}
bundle_path.write_text(json.dumps(bundle, indent=2) + "\n")
bundle_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
import fs from "node:fs";
import { MorphCloudClient } from "morphcloud";
const instance_id = "morphvm_abc123"; // Replace with your instance ID
const bundlePath = "./ssh-access-bundle.json";
const client = new MorphCloudClient({ apiKey: process.env.MORPH_API_KEY! });
const instance = await client.instances.get({ instance_id });
const sshKey = await instance.ssh_key(); // one-time API call for instance-scoped credentials
const bundle = {
hostname: "ssh.cloud.morph.so",
port: 22,
username: instance_id,
private_key: sshKey.privateKey,
password: (sshKey as any).password ?? null,
};
fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + "\n", { mode: 0o600 });
Option B: Use the bundle to SSH (no API key)
Once you have an instance-scoped bundle file, you can connect through the Morph SSH gateway without MORPH_API_KEY.
The simplest approach is to treat the bundle as an input to a small script/tool (copy/paste the example below into your own tooling).
Copy/paste Python example: exec via bundle (no API key)
This example reads ssh-access-bundle.json and runs a command via Paramiko. It does not call the Morph API.
#!/usr/bin/env python3
import argparse
import json
import os
import stat
import tempfile
import paramiko
def main() -> None:
parser = argparse.ArgumentParser(description="Run a command using a Morph SSH access bundle (no API key required).")
parser.add_argument("--bundle", required=True, help="Path to ssh-access-bundle.json")
parser.add_argument("--cmd", required=True, help="Command to run on the instance")
args = parser.parse_args()
if os.environ.get("MORPH_API_KEY"):
raise SystemExit("MORPH_API_KEY should not be set for this step (bundle-only SSH).")
with open(args.bundle, "r", encoding="utf-8") as f:
bundle = json.load(f)
hostname = bundle["hostname"]
port = int(bundle.get("port", 22))
username = bundle["username"]
private_key = bundle["private_key"]
password = bundle.get("password") # may be null
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
with tempfile.TemporaryDirectory() as td:
key_path = os.path.join(td, "id_morph_instance")
with open(key_path, "w", encoding="utf-8") as kf:
kf.write(private_key)
os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
try:
pkey = paramiko.RSAKey.from_private_key_file(key_path, password=password)
client.connect(hostname=hostname, port=port, username=username, pkey=pkey, password=None, timeout=30)
except Exception:
# If key auth fails, fall back to password auth (bundle includes a password).
client.connect(hostname=hostname, port=port, username=username, pkey=None, password=password, timeout=30)
stdin, stdout, stderr = client.exec_command(args.cmd)
exit_code = stdout.channel.recv_exit_status()
out = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace")
if out:
print(out, end="" if out.endswith("\n") else "\n")
if err:
print(err, end="" if err.endswith("\n") else "\n")
raise SystemExit(exit_code)
if __name__ == "__main__":
main()
Example usage:
python3 -m pip install --upgrade morphcloud paramiko
env -u MORPH_API_KEY python3 ./ssh_bundle_exec.py \
--bundle ./ssh-access-bundle.json \
--cmd 'uname -a'
Rotation / revocation
To end access (or if the bundle is ever exposed), rotate the instance SSH key and re-mint a new bundle for the next recipient.
- Rotate using the API/SDK (see SSH Key Management).
- After rotation, the old bundle should no longer be considered valid; delete it wherever it may have been stored or shared.
Safe sharing checklist
- Store and share the bundle only via a secrets manager or approved secure file sharing.
- Time-box access: rotate the instance SSH key when the engagement ends.
- Prefer passing the bundle as a file (not copy/paste); avoid tickets, logs, CI output, and chat.