private_key_id); return [ 'sshKeyLocation' => $privateKey->getKeyLocation(), 'muxFilename' => self::muxSocket($server), ]; } public static function ensureMultiplexedConnection(Server $server): bool { return self::isMultiplexingEnabled(); } public static function removeMuxFile(Server $server): void { $closeCommand = self::muxControlCommand($server, 'exit'); Process::run($closeCommand); } private static function muxControlCommand(Server $server, string $operation): string { $command = "ssh -O {$operation} -o ControlPath=".self::muxSocket($server).' '; if (data_get($server, 'settings.is_cloudflare_tunnel')) { $command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; } return $command.self::escapedUserAtHost($server); } public static function generateScpCommand(Server $server, string $source, string $dest): string { $sshConfig = self::serverSshConfiguration($server); $sshKeyLocation = $sshConfig['sshKeyLocation']; $scpCommand = 'timeout '.config('constants.ssh.command_timeout').' scp '; if ($server->isIpv6()) { $scpCommand .= '-6 '; } if (self::isMultiplexingEnabled()) { $scpCommand .= self::multiplexingOptions($server); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { $scpCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; } $scpCommand .= self::getCommonSshOptions($server, $sshKeyLocation, self::getConnectionTimeout($server), config('constants.ssh.server_interval'), isScp: true); if ($server->isIpv6()) { return $scpCommand."{$source} ".escapeshellarg($server->user).'@['.escapeshellarg($server->ip)."]:{$dest}"; } return $scpCommand."{$source} ".self::escapedUserAtHost($server).":{$dest}"; } public static function generateSshCommand(Server $server, string $command, bool $disableMultiplexing = false): string { if ($server->settings->force_disabled) { throw new \RuntimeException('Server is disabled.'); } $sshConfig = self::serverSshConfiguration($server); $sshKeyLocation = $sshConfig['sshKeyLocation']; self::validateSshKey($server->privateKey); $sshCommand = 'timeout '.config('constants.ssh.command_timeout').' ssh '; if (! $disableMultiplexing && self::isMultiplexingEnabled()) { $sshCommand .= self::multiplexingOptions($server); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { $sshCommand .= "-o ProxyCommand='cloudflared access ssh --hostname %h' "; } $sshCommand .= self::getCommonSshOptions($server, $sshKeyLocation, self::getConnectionTimeout($server), config('constants.ssh.server_interval')); $delimiter = base64_encode(Hash::make($command)); $command = str_replace($delimiter, '', $command); return $sshCommand.self::escapedUserAtHost($server)." 'bash -se' << \\$delimiter".PHP_EOL .$command.PHP_EOL .$delimiter; } private static function multiplexingOptions(Server $server): string { return '-o ControlMaster=auto ' .'-o ControlPath='.self::muxSocket($server).' ' .'-o ControlPersist='.config('constants.ssh.mux_persist_time').' '; } private static function muxSocket(Server $server): string { return '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid; } private static function escapedUserAtHost(Server $server): string { return escapeshellarg($server->user).'@'.escapeshellarg($server->ip); } private static function isMultiplexingEnabled(): bool { return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop'); } private static function validateSshKey(PrivateKey $privateKey): void { $keyLocation = $privateKey->getKeyLocation(); $filename = "ssh_key@{$privateKey->uuid}"; $disk = Storage::disk('ssh-keys'); $needsRewrite = false; if (! $disk->exists($filename)) { $needsRewrite = true; } else { $diskContent = $disk->get($filename); if ($diskContent !== $privateKey->private_key) { Log::warning('SSH key file content does not match database, resyncing', [ 'key_uuid' => $privateKey->uuid, ]); $needsRewrite = true; } } if ($needsRewrite) { $privateKey->storeInFileSystem(); } if (file_exists($keyLocation)) { $currentPerms = fileperms($keyLocation) & 0777; if ($currentPerms !== 0600 && ! chmod($keyLocation, 0600)) { Log::warning('Failed to set SSH key file permissions to 0600', [ 'key_uuid' => $privateKey->uuid, 'path' => $keyLocation, ]); } } } public static function getConnectionTimeout(Server $server): int { $timeout = data_get($server, 'settings.connection_timeout'); return is_numeric($timeout) && (int) $timeout > 0 ? (int) $timeout : (int) config('constants.ssh.connection_timeout'); } private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string { $options = "-i {$sshKeyLocation} " .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' .'-o PasswordAuthentication=no ' ."-o ConnectTimeout=$connectionTimeout " ."-o ServerAliveInterval=$serverInterval " .'-o RequestTTY=no ' .'-o LogLevel=ERROR '; if ($isScp) { return $options.'-P '.escapeshellarg((string) $server->port).' '; } return $options.'-p '.escapeshellarg((string) $server->port).' '; } }