Adding a new developer or sysadmin to your team usually means updating SSH access across dozens or hundreds of servers.

Doing this manually takes time and invites errors.

In this post, you’ll learn how to use the RunCloud API to automate this process with a Python script that adds an SSH public key to all servers linked to your RunCloud account.

This approach is ideal for bulk operations. If you’re only working with a single server, the RunCloud dashboard is faster and easier.

Why You Might Want To Do This

Manually adding an SSH key to multiple servers can be tedious and error-prone if you are processing a large number of servers. Using the following script offers several advantages:

  1. Temporary Access: You can grant temporary access by setting the temporary flag to ‘true’ when adding the key. This feature ensures that access is revoked automatically after 12 hours.
  2. Key Rotation & Security: If an SSH key is compromised or an employee leaves, you’d typically remove their old key. While this script focuses on adding keys, the same API principles can be used to remove them. Regularly rotating keys and ensuring only necessary keys are present enhances server security. If a key is misplaced, and you’ve already rotated it out or it was temporary, the potential security impact is minimized.
  3. Automation & Integration: This script can be a standalone tool or integrated into larger automation workflows, such as Ansible playbooks or CI/CD pipelines, for managing server access systematically.

Prerequisites

  • RunCloud API Key: You must generate an API Key from your RunCloud account settings. This script uses a Bearer Token.
  • Existing SSH Key Pair: You need an SSH key pair (private and public keys).
    • The private key must be stored securely on the computer from which the new administrator will access the servers.
    • The public key string (e.g., ssh-rsa AAAA... user@host) will be embedded into this Python script to be distributed to the servers.
  • Python Environment: You’ll need Python installed to run the script.

Important Note on Permissions and Security

The RunCloud API grants you a great deal of power to manage your servers. If your API credentials are leaked, an attacker could potentially gain access to all your servers. Keep the following things in mind:

  • Principle of Least Privilege: Before adding SSH keys to your server, we strongly recommend reading Patchstack’s post on the Principle of Least Privilege (POLP).
  • Secure Storage: Store your API Key and Secret (or Bearer Token) very securely. Do not hardcode them directly into scripts committed to version control without appropriate secret management (e.g., environment variables, a secrets vault). We’ll use a placeholder for this example, but manage it securely in production.
  • Audit Regularly: Regularly review who has API access and what SSH keys are deployed on your servers.

Building the Python Script

The following script uses Python to make API requests, but the RunCloud API is flexible. You can interact with it using cURL, PowerShell, Node.js, Ruby, Go, or any language that can make HTTP requests.

Always refer to the Official RunCloud API v3 Documentation for comprehensive details on API endpoints, authentication, and other capabilities.

Now, let’s write the Python script:

import http.client
import json
# --- Configuration ---
RUNCLOUD_API_HOST = "manage.runcloud.io"

# IMPORTANT: Replace with your actual Bearer Token.
AUTH_BEARER_TOKEN = "PLEASE_REPLACE_WITH_YOUR_RUNCLOUD_API_BEARER_TOKEN"

AUTH_HEADER = {"Authorization": f"Bearer {AUTH_BEARER_TOKEN}",
               "user-agent": "RunCloud SSH Key Management Script v1.0"}

# --- SSH Key Configuration ---
# IMPORTANT: Replace with the public key you want to add.
# It should look like: "ssh-rsa AAAA..." or "ecdsa-sha2-nistp256 AAAA..."
NEW_SSH_PUBLIC_KEY = "ssh-rsa AAAA...your_username@your_machine"
# It is STRONGLY recommended to provide a unique and descriptive label.
SSH_KEY_LABEL = "my_new_key" 
# The username on the server for which this key will be authorized.
SSH_USERNAME = "runcloud"
# Set to True if this key is for temporary access.
IS_TEMPORARY_KEY = True

# --- Helper Function for API Requests ---
def make_api_request(method, path, payload_dict=None, extra_headers=None):
    """
    Makes an API request to the RunCloud API.
    Args:
        method (str): HTTP method (e.g., "GET", "POST", "PATCH").
        path (str): API endpoint path (e.g., "/api/v3/servers/...").
        payload_dict (dict, optional): Data payload for POST/PATCH requests. Defaults to None.
        extra_headers (dict, optional): Extra headers to add/override. Defaults to None.
    Returns:
        tuple: (status_code, response_body_dict) or (None, error_message_dict) on failure.
    """
    conn = None
    try:
        conn = http.client.HTTPSConnection(RUNCLOUD_API_HOST)
        
        headers = AUTH_HEADER.copy()
        if extra_headers:
            headers.update(extra_headers)
        
        body_bytes = None
        if payload_dict:
            if 'Content-Type' not in headers: # Default to JSON if payload is present
                headers['Content-Type'] = 'application/json'
            body_bytes = json.dumps(payload_dict).encode('utf-8')
        else:
            # For GET or methods without body, Content-Type is not strictly needed
            # but ensure body_bytes is None or empty if method expects it
            body_bytes = b''


        conn.request(method, path, body=body_bytes, headers=headers)
        
        res = conn.getresponse()
        status = res.status
        response_body_str = res.read().decode("utf-8")
        
        try:
            response_json = json.loads(response_body_str)
            return status, response_json
        except json.JSONDecodeError:
            # Handle cases where response isn't valid JSON (e.g., unexpected HTML error page from proxy)
            print(f"     Warning: Response from {method} {path} was not valid JSON (Status: {status}).")
            print(f"     Raw Response Body (first 200 chars): {response_body_str[:200]}...")
            if 200 <= status < 300: # Success, but not JSON
                 return status, {"raw_response": response_body_str, "message": "Successful but non-JSON response"}
            else: # Error and not JSON
                return status, {"error": "Invalid JSON response", "details": response_body_str, "message": "Error with non-JSON response"}

    except http.client.HTTPException as e:
        print(f"[Error] HTTP Exception during {method} {path}: {e}")
        return None, {"error": "HTTP Exception", "details": str(e)}
    except ConnectionError as e: # More specific than just Exception for network issues
        print(f"[Error] Connection Error during {method} {path}: {e}")
        return None, {"error": "Connection Error", "details": str(e)}
    except Exception as e:
        print(f"[Error] Unexpected error during {method} {path}: {e}")
        return None, {"error": "Unexpected Error", "details": str(e)}
    finally:
        if conn:
            conn.close()
# --- Main Logic ---
def add_ssh_key_to_all_servers():
    print("Attempting to add SSH key to all RunCloud servers...")
    print(f"  Key Label: {SSH_KEY_LABEL}")
    print(f"  Target Username: {SSH_USERNAME}")
    print(f"  Public Key: {NEW_SSH_PUBLIC_KEY[:30]}...{NEW_SSH_PUBLIC_KEY[-20:]}") # Print snippet
    print("-" * 30)

    successful_servers = []
    failed_servers = []
    # 1. Get list of all servers
    print("Fetching list of servers...")
    status, response = make_api_request("GET", "/api/v3/servers")
    if status is None or not (200 <= status < 300):
        print(f"Failed to fetch server list. Status: {status}")
        print(f"Response: {json.dumps(response, indent=2)}")
        return
    servers = response.get("data")
    if not servers:
        print("No servers found in your RunCloud account.")
        return
    print(f"Found {len(servers)} server(s).")

    # 2. For each server, try to add the SSH key
    ssh_key_payload = {
        "label": SSH_KEY_LABEL,
        "username": SSH_USERNAME,
        "publicKey": NEW_SSH_PUBLIC_KEY,
        "temporary": IS_TEMPORARY_KEY
    }

    for server in servers:
        server_id = server.get("id")
        server_name = server.get("name", "N/A")
        server_ip = server.get("ipAddress", "N/A")

        if not server_id:
            print(f"Skipping server with missing ID: {server_name} ({server_ip})")
            failed_servers.append({"name": server_name, "ip": server_ip, "reason": "Missing server ID in API response"})
            continue

        print(f"\nProcessing server: {server_name} (ID: {server_id}, IP: {server_ip})")
        
        add_key_path = f"/api/v3/servers/{server_id}/ssh/credentials"
        
        # The make_api_request function handles json.dumps for the payload
        post_status, post_response = make_api_request("POST", add_key_path, payload_dict=ssh_key_payload)

        if post_status is not None and (post_status == 200 or post_status == 201): 
            print(f"  SUCCESS: SSH key added to {server_name} ({server_ip}).")
            # The response might contain the details of the added key, e.g., post_response.get('data', {}).get('id')
            successful_servers.append({"name": server_name, "ip": server_ip})
        else:
            error_message = "Unknown error"
            if post_response:
                error_message = post_response.get("message", json.dumps(post_response.get("errors", post_response)))
            
            print(f"  FAILURE: Could not add SSH key to {server_name} ({server_ip}). Status: {post_status}")
            print(f"  Reason: {error_message}")
            failed_servers.append({"name": server_name, "ip": server_ip, "status": post_status, "reason": error_message})


    # 3. Log success and failure
    print("\n--- Summary ---")
    if successful_servers:
        print("\nSuccessfully added SSH key to the following servers:")
        for s in successful_servers:
            print(f"  - {s['name']} ({s['ip']})")
    
    if failed_servers:
        print("\nFailed to add SSH key to the following servers:")
        for s in failed_servers:
            print(f"  - {s['name']} ({s['ip']}) - Status: {s.get('status', 'N/A')}, Reason: {s['reason']}")
            
    if not servers:
        print("\nNo servers were processed.")


if __name__ == "__main__":
    # Before running, ensure you have:
    # 1. Updated AUTH_BEARER_TOKEN with your actual RunCloud API Bearer Token.
    # 2. Updated NEW_SSH_PUBLIC_KEY with the public key string.
    # 3. Optionally, updated SSH_KEY_LABEL, SSH_USERNAME, and IS_TEMPORARY_KEY.        
    add_ssh_key_to_all_servers()

The add_ssh_key_to_all_servers() function in the above script performs the following actions:

  1. Initializes successful_servers and failed_servers lists to keep track of outcomes.
  2. Fetch Servers: The script first calls make_api_request(“GET”, “/api/v3/servers“) to get a list of all servers.
  3. Prepare SSH Key Payload: The ssh_key_payload dictionary is defined once with the details of the SSH key you want to add (label, username, public key, temporary status).
  4. Iterate and Add Key:
    • The script loops through each server object obtained from the server list.
    • It then extracts the server_id, server_name, and server_ip for logging and API calls.
    • Next, it constructs the correct API endpoint for adding an SSH key to a specific server: /api/v3/servers/{server_id}/ssh/credentials.
    • The script then calls the make_api_request() function to send the request.
    • If the post_status is 200 (OK) or 201 (Created), it logs success and adds the server to successful_servers. Otherwise, it logs failure, tries to extract an error message from the API response, and adds the server to failed_servers.
  5. Summary: Finally, this script summarizes which servers had the key added successfully and which failed, along with reasons for failure.

How to Use This Script

  1. Save: Save the code as a Python file named main.py.
  2. Configure:
    • Replace PLEASE_REPLACE_WITH_YOUR_RUNCLOUD_API_BEARER_TOKEN with your actual RunCloud API Bearer Token.
    • Replace ssh-rsa AAAA...your_username@your_machine in NEW_SSH_PUBLIC_KEY with the public SSH key string you want to add.
    • Optionally, customize SSH_KEY_LABEL, SSH_USERNAME, and IS_TEMPORARY_KEY.
  3. Run: Execute the script from your terminal by executing the following command: python main.py

This script is truly just a glimpse into the power and flexibility of the RunCloud API. We have provided a practical solution to a common administrative task, but the API offers vast automation and custom server management possibilities.

The RunCloud API is a versatile tool, and its true potential is only limited by your creativity and imagination.

Join the RunCloud community to explore the RunCloud API further and see how others use it to speed up server management.

If you have any other questions or need help – please feel free to get in touch with our 24/7 support team. We’re here to help!