Extract Ladill Servers as standalone app at servers.ladill.com.

VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 19:18:30 +00:00
co-authored by Cursor
commit b6c8ac343f
382 changed files with 67315 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
<?php
namespace App\Console\Commands;
use App\Models\HostingNode;
use Illuminate\Console\Command;
class SetupNodeSshKey extends Command
{
protected $signature = 'hosting:setup-node-ssh {node_id} {--key-path= : Path to existing private key} {--generate : Generate a new key pair}';
protected $description = 'Configure SSH key authentication for a hosting node (required for all node operations)';
public function handle(): int
{
$node = HostingNode::find($this->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;
}
}
}