argument('node_id')); if (!$node) { $this->error("Hosting node #{$this->argument('node_id')} not found."); return self::FAILURE; } $this->info("Node: {$node->name} ({$node->ip_address})"); if ($this->option('generate')) { return $this->generateAndInstallKey($node); } if ($this->option('key-path')) { return $this->installExistingKey($node, $this->option('key-path')); } // Try common key locations $commonPaths = [ '/root/.ssh/id_ed25519', '/root/.ssh/id_rsa', getenv('HOME') . '/.ssh/id_ed25519', getenv('HOME') . '/.ssh/id_rsa', ]; foreach ($commonPaths as $path) { if (file_exists($path) && is_readable($path)) { $this->info("Found existing key at: {$path}"); if ($this->confirm("Use this key?", true)) { return $this->installExistingKey($node, $path); } } } $this->warn("No SSH key found. Use --generate to create one, or --key-path to specify an existing key."); $this->line(""); $this->line("Examples:"); $this->line(" php artisan hosting:setup-node-ssh {$node->id} --generate"); $this->line(" php artisan hosting:setup-node-ssh {$node->id} --key-path=/root/.ssh/id_rsa"); return self::FAILURE; } private function generateAndInstallKey(HostingNode $node): int { $keyDir = storage_path('app/ssh-keys'); if (!is_dir($keyDir)) { mkdir($keyDir, 0700, true); } $keyPath = "{$keyDir}/node_{$node->id}_ed25519"; if (file_exists($keyPath)) { if (!$this->confirm("Key already exists at {$keyPath}. Overwrite?", false)) { return self::FAILURE; } unlink($keyPath); if (file_exists("{$keyPath}.pub")) { unlink("{$keyPath}.pub"); } } // Generate Ed25519 key pair $this->info("Generating Ed25519 key pair..."); $result = shell_exec("ssh-keygen -t ed25519 -f " . escapeshellarg($keyPath) . " -N '' -C 'ladill-node-{$node->id}' 2>&1"); if (!file_exists($keyPath) || !file_exists("{$keyPath}.pub")) { $this->error("Failed to generate key pair: {$result}"); return self::FAILURE; } chmod($keyPath, 0600); $privateKey = file_get_contents($keyPath); $publicKey = trim(file_get_contents("{$keyPath}.pub")); // Store private key in the database $node->update(['ssh_private_key' => $privateKey]); $this->info("Private key stored in database for node #{$node->id}."); // Show instructions for installing the public key $this->line(""); $this->warn("=== IMPORTANT: Install the public key on the node ==="); $this->line(""); $isLocal = $node->ip_address === '127.0.0.1' || $node->ip_address === 'localhost' || $node->provider === 'local'; if ($isLocal) { $this->info("This is a local node. Installing public key automatically..."); $authorizedKeysFile = '/root/.ssh/authorized_keys'; if (!is_dir('/root/.ssh')) { mkdir('/root/.ssh', 0700, true); } // Check if key already installed $existingKeys = file_exists($authorizedKeysFile) ? file_get_contents($authorizedKeysFile) : ''; if (str_contains($existingKeys, $publicKey)) { $this->info("Public key already installed in {$authorizedKeysFile}"); } else { file_put_contents($authorizedKeysFile, $publicKey . "\n", FILE_APPEND); chmod($authorizedKeysFile, 0600); $this->info("Public key installed in {$authorizedKeysFile}"); } } else { $this->line("Run this on the node ({$node->ip_address}):"); $this->line(""); $this->line(" mkdir -p /root/.ssh && chmod 700 /root/.ssh"); $this->line(" echo '{$publicKey}' >> /root/.ssh/authorized_keys"); $this->line(" chmod 600 /root/.ssh/authorized_keys"); $this->line(""); } // Test the connection $this->info("Testing SSH connection..."); return $this->testConnection($node); } private function installExistingKey(HostingNode $node, string $keyPath): int { if (!file_exists($keyPath)) { $this->error("Key file not found: {$keyPath}"); return self::FAILURE; } $privateKey = file_get_contents($keyPath); if (!str_contains($privateKey, '-----BEGIN') && !str_contains($privateKey, 'PRIVATE KEY')) { $this->error("File does not appear to be a valid SSH private key."); return self::FAILURE; } $node->update(['ssh_private_key' => $privateKey]); $this->info("Private key stored in database for node #{$node->id}."); // Check if public key needs to be installed $pubKeyPath = "{$keyPath}.pub"; if (file_exists($pubKeyPath)) { $publicKey = trim(file_get_contents($pubKeyPath)); $isLocal = $node->ip_address === '127.0.0.1' || $node->ip_address === 'localhost' || $node->provider === 'local'; if ($isLocal) { $authorizedKeysFile = '/root/.ssh/authorized_keys'; $existingKeys = file_exists($authorizedKeysFile) ? file_get_contents($authorizedKeysFile) : ''; if (!str_contains($existingKeys, $publicKey)) { $this->info("Installing public key on local node..."); if (!is_dir('/root/.ssh')) { mkdir('/root/.ssh', 0700, true); } file_put_contents($authorizedKeysFile, $publicKey . "\n", FILE_APPEND); chmod($authorizedKeysFile, 0600); } } } $this->info("Testing SSH connection..."); return $this->testConnection($node); } private function testConnection(HostingNode $node): int { try { $node->refresh(); $provider = app(\App\Services\Hosting\Providers\SharedNodeProvider::class); // Use reflection to call private connect method for testing $reflection = new \ReflectionClass($provider); $method = $reflection->getMethod('connect'); $method->setAccessible(true); $ssh = $method->invoke($provider, $node); if ($ssh === null) { $this->warn("Node is using local execution (no SSH). This means the SSH key is not configured properly."); return self::FAILURE; } $output = $ssh->exec('whoami'); $this->info("SSH connection successful! Connected as: " . trim($output)); // Verify mysql access $mysqlTest = $ssh->exec('mysql -e "SELECT 1" 2>&1'); if (str_contains($mysqlTest, '1')) { $this->info("MySQL access via auth_socket: OK"); } else { $this->warn("MySQL access via auth_socket: FAILED ({$mysqlTest})"); $this->warn("MySQL may not be installed or auth_socket may not be configured for root."); } // Verify runuser access $runuserTest = $ssh->exec('runuser -u nobody -- whoami 2>&1'); if (trim($runuserTest) === 'nobody') { $this->info("runuser access: OK"); } else { $this->warn("runuser access: FAILED"); } $ssh->disconnect(); $this->info(""); $this->info("Node #{$node->id} is fully configured for SSH access."); return self::SUCCESS; } catch (\Throwable $e) { $this->error("SSH connection failed: " . $e->getMessage()); $this->line(""); $this->line("Ensure:"); $this->line(" 1. SSH is running on the node ({$node->ip_address}:{$node->ssh_port})"); $this->line(" 2. Root login is permitted (PermitRootLogin yes in sshd_config)"); $this->line(" 3. The public key is installed in /root/.ssh/authorized_keys"); return self::FAILURE; } } }