ssh_setup.py
1 #!/usr/bin/env python3 2 """ 3 SSH Key Setup - Auto-detect and configure SSH keys for Sovereign OS nodes. 4 5 Usage: 6 python3 scripts/ssh_setup.py # Check status 7 python3 scripts/ssh_setup.py --setup # Setup keys and authorize 8 python3 scripts/ssh_setup.py --test # Test connectivity 9 """ 10 11 import os 12 import sys 13 import subprocess 14 from pathlib import Path 15 16 # Known sovereign nodes 17 SOVEREIGN_NODES = { 18 "nodebox": { 19 "host": "nodebox.local", 20 "user": "satoshi", 21 "description": "Local dev/deployment node" 22 } 23 } 24 25 # SSH key paths (in priority order) 26 SSH_KEY_TYPES = [ 27 ("id_ed25519", "Ed25519 (recommended)"), 28 ("id_rsa", "RSA"), 29 ("id_ecdsa", "ECDSA"), 30 ] 31 32 33 def get_ssh_dir() -> Path: 34 """Get SSH directory path.""" 35 return Path.home() / ".ssh" 36 37 38 def find_existing_keys() -> list: 39 """Find existing SSH keys.""" 40 ssh_dir = get_ssh_dir() 41 found_keys = [] 42 43 for key_name, key_type in SSH_KEY_TYPES: 44 private_key = ssh_dir / key_name 45 public_key = ssh_dir / f"{key_name}.pub" 46 47 if private_key.exists() and public_key.exists(): 48 found_keys.append({ 49 "name": key_name, 50 "type": key_type, 51 "private": private_key, 52 "public": public_key, 53 "public_content": public_key.read_text().strip() 54 }) 55 56 return found_keys 57 58 59 def generate_key(key_type: str = "ed25519") -> dict: 60 """Generate a new SSH key pair.""" 61 ssh_dir = get_ssh_dir() 62 ssh_dir.mkdir(mode=0o700, exist_ok=True) 63 64 key_path = ssh_dir / f"id_{key_type}" 65 66 if key_path.exists(): 67 print(f"Key already exists: {key_path}") 68 return None 69 70 print(f"Generating new {key_type} key...") 71 72 result = subprocess.run([ 73 "ssh-keygen", 74 "-t", key_type, 75 "-f", str(key_path), 76 "-N", "", # No passphrase 77 "-C", f"sovereign-os@{os.uname().nodename}" 78 ], capture_output=True, text=True) 79 80 if result.returncode == 0: 81 public_key = key_path.with_suffix(".pub") 82 return { 83 "name": f"id_{key_type}", 84 "type": key_type, 85 "private": key_path, 86 "public": public_key, 87 "public_content": public_key.read_text().strip() 88 } 89 else: 90 print(f"Error generating key: {result.stderr}") 91 return None 92 93 94 def test_ssh_connection(host: str, user: str, timeout: int = 5) -> dict: 95 """Test SSH connection to a host.""" 96 result = { 97 "host": host, 98 "user": user, 99 "reachable": False, 100 "key_auth": False, 101 "error": None 102 } 103 104 # Test with key auth (no password prompt) 105 try: 106 proc = subprocess.run([ 107 "ssh", 108 "-o", "BatchMode=yes", 109 "-o", "ConnectTimeout=5", 110 "-o", "StrictHostKeyChecking=no", 111 f"{user}@{host}", 112 "echo ok" 113 ], capture_output=True, text=True, timeout=timeout + 2) 114 115 if proc.returncode == 0: 116 result["reachable"] = True 117 result["key_auth"] = True 118 elif "Permission denied" in proc.stderr: 119 result["reachable"] = True 120 result["key_auth"] = False 121 result["error"] = "Key not authorized" 122 elif "Host key verification failed" in proc.stderr: 123 result["reachable"] = True 124 result["key_auth"] = False 125 result["error"] = "Host key changed" 126 else: 127 result["error"] = proc.stderr.strip()[:100] 128 except subprocess.TimeoutExpired: 129 result["error"] = "Connection timeout" 130 except Exception as e: 131 result["error"] = str(e) 132 133 return result 134 135 136 def copy_key_to_host(public_key: str, host: str, user: str) -> bool: 137 """ 138 Copy public key to remote host's authorized_keys. 139 140 Note: This requires password auth to work initially. 141 """ 142 print(f"\nTo authorize your key on {host}, run:") 143 print(f" ssh-copy-id {user}@{host}") 144 print(f"\nOr manually add this to {user}@{host}:~/.ssh/authorized_keys:") 145 print(f" {public_key}") 146 return False 147 148 149 def check_status() -> dict: 150 """Check overall SSH setup status.""" 151 status = { 152 "keys": find_existing_keys(), 153 "nodes": {} 154 } 155 156 # Test each known node 157 for node_name, node_info in SOVEREIGN_NODES.items(): 158 status["nodes"][node_name] = test_ssh_connection( 159 node_info["host"], 160 node_info["user"] 161 ) 162 status["nodes"][node_name]["description"] = node_info["description"] 163 164 return status 165 166 167 def print_status(status: dict): 168 """Print status in human-readable format.""" 169 print("═" * 60) 170 print("SSH KEY STATUS") 171 print("═" * 60) 172 print() 173 174 # Keys 175 if status["keys"]: 176 print("Local SSH Keys:") 177 for key in status["keys"]: 178 print(f" ✓ {key['name']} ({key['type']})") 179 else: 180 print("⚠ No SSH keys found!") 181 print(" Run: python3 scripts/ssh_setup.py --setup") 182 print() 183 184 # Nodes 185 print("Sovereign Nodes:") 186 all_good = True 187 188 for node_name, node_status in status["nodes"].items(): 189 desc = node_status.get("description", "") 190 191 if node_status["key_auth"]: 192 print(f" ✓ {node_name}: Key auth working") 193 elif node_status["reachable"]: 194 all_good = False 195 print(f" ⚠ {node_name}: Reachable but key not authorized") 196 if node_status["error"]: 197 print(f" Error: {node_status['error']}") 198 else: 199 print(f" ✗ {node_name}: Not reachable") 200 if node_status["error"]: 201 print(f" Error: {node_status['error']}") 202 203 print() 204 205 if not all_good and status["keys"]: 206 print("To authorize your key on a node:") 207 print(" ssh-copy-id satoshi@nodebox.local") 208 print() 209 210 211 def setup(): 212 """Interactive setup flow.""" 213 print("═" * 60) 214 print("SSH KEY SETUP") 215 print("═" * 60) 216 print() 217 218 # Check for existing keys 219 keys = find_existing_keys() 220 221 if keys: 222 print("Existing SSH keys found:") 223 for key in keys: 224 print(f" ✓ {key['name']} ({key['type']})") 225 print() 226 key = keys[0] # Use first key 227 else: 228 print("No SSH keys found. Generating Ed25519 key...") 229 key = generate_key("ed25519") 230 if not key: 231 print("Failed to generate key.") 232 return False 233 234 print(f"\nYour public key ({key['name']}):") 235 print(f" {key['public_content'][:60]}...") 236 print() 237 238 # Test nodes and offer to authorize 239 for node_name, node_info in SOVEREIGN_NODES.items(): 240 print(f"\nChecking {node_name} ({node_info['host']})...") 241 242 result = test_ssh_connection(node_info["host"], node_info["user"]) 243 244 if result["key_auth"]: 245 print(f" ✓ Key already authorized on {node_name}") 246 elif result["reachable"]: 247 print(f" ⚠ {node_name} reachable but key not authorized") 248 copy_key_to_host(key["public_content"], node_info["host"], node_info["user"]) 249 else: 250 print(f" ✗ {node_name} not reachable: {result.get('error', 'unknown error')}") 251 252 print() 253 return True 254 255 256 def main(): 257 args = sys.argv[1:] 258 259 if "--setup" in args: 260 setup() 261 elif "--test" in args: 262 status = check_status() 263 print_status(status) 264 # Exit with error if any node has key auth issues 265 for node_status in status["nodes"].values(): 266 if not node_status["key_auth"]: 267 sys.exit(1) 268 elif "--json" in args: 269 import json 270 status = check_status() 271 # Convert Path objects to strings 272 for key in status["keys"]: 273 key["private"] = str(key["private"]) 274 key["public"] = str(key["public"]) 275 print(json.dumps(status, indent=2)) 276 else: 277 status = check_status() 278 print_status(status) 279 280 281 if __name__ == "__main__": 282 main()