From 86722939cd56b6bda69b99f0abcda1b4efe38142 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:34:27 +0200 Subject: [PATCH] Fix. Remove write to SSH key on every remote command execution --- app/Jobs/ApplicationDeploymentJob.php | 17 +-- app/Livewire/Boarding/Index.php | 27 +++-- app/Models/Server.php | 32 ++++-- bootstrap/helpers/remoteProcess.php | 144 +++++++++++--------------- routes/web.php | 2 +- 5 files changed, 107 insertions(+), 115 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 718cea639..205fdce15 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -210,7 +210,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } ray('New container name: ', $this->container_name)->green(); - savePrivateKeyToFs($this->server); $this->saved_outputs = collect(); // Set preview fqdn @@ -969,7 +968,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); + $url = str($this->application->fqdn)->replace('http://', '').replace('https://', ''); if ($this->application->compose_parsing_version === '3') { $envs->push("COOLIFY_FQDN={$url}"); } else { @@ -1442,21 +1441,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->pull_request_id !== 0) { $local_branch = "pull/{$this->pull_request_id}/head"; } - $private_key = data_get($this->application, 'private_key.private_key'); + $private_key = $this->application->privateKey->getKeyLocation(); if ($private_key) { - $private_key = base64_encode($private_key); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), - ], - [ - executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), - ], - [ - executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), - ], - [ - executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$private_key}\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), 'hidden' => true, 'save' => 'git_commit_sha', ], diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 0ded72d7e..21dcda50f 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function savePrivateKey() { $this->validate([ - 'privateKeyName' => 'required', - 'privateKey' => 'required', + 'privateKeyName' => 'required|string|max:255', + 'privateKeyDescription' => 'nullable|string|max:255', + 'privateKey' => 'required|string', ]); - $this->createdPrivateKey = PrivateKey::create([ - 'name' => $this->privateKeyName, - 'description' => $this->privateKeyDescription, - 'private_key' => $this->privateKey, - 'team_id' => currentTeam()->id, - ]); - $this->createdPrivateKey->save(); - $this->currentState = 'create-server'; + + try { + $privateKey = PrivateKey::createAndStore([ + 'name' => $this->privateKeyName, + 'description' => $this->privateKeyDescription, + 'private_key' => $this->privateKey, + 'team_id' => currentTeam()->id, + ]); + + $this->createdPrivateKey = $privateKey; + $this->currentState = 'create-server'; + } catch (\Exception $e) { + $this->addError('privateKey', 'Failed to save private key: ' . $e->getMessage()); + } } public function saveServer() diff --git a/app/Models/Server.php b/app/Models/Server.php index 65d70083f..cae910257 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -909,13 +909,15 @@ $schema://$host { public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; - ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this); - if (! $isFunctional) { - Storage::disk('ssh-keys')->delete($private_key_filename); - Storage::disk('ssh-mux')->delete($mux_filename); + $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled; + + if (!$isFunctional) { + if ($this->privateKey) { + PrivateKey::deleteFromStorage($this->privateKey); + } + Storage::disk('ssh-mux')->delete($this->muxFilename()); } - + return $isFunctional; } @@ -1115,4 +1117,22 @@ $schema://$host { { return $this->settings->is_build_server; } + + public static function createWithPrivateKey(array $data, PrivateKey $privateKey) + { + $server = new self($data); + $server->privateKey()->associate($privateKey); + $server->save(); + return $server; + } + + public function updateWithPrivateKey(array $data, PrivateKey $privateKey = null) + { + $this->update($data); + if ($privateKey) { + $this->privateKey()->associate($privateKey); + $this->save(); + } + return $this; + } } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 4ba378e67..0d21c20a0 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -16,6 +16,7 @@ use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Spatie\Activitylog\Contracts\Activity; + function remote_process( Collection|array $command, Server $server, @@ -26,19 +27,18 @@ function remote_process( $callEventOnFinish = null, $callEventData = null ): Activity { - if (is_null($type)) { - $type = ActivityTypes::INLINE->value; - } - if ($command instanceof Collection) { - $command = $command->toArray(); - } + $type = $type ?? ActivityTypes::INLINE->value; + $command = $command instanceof Collection ? $command->toArray() : $command; + if ($server->isNonRoot()) { $command = parseCommandsByLineForSudo(collect($command), $server); } + $command_string = implode("\n", $command); + if (auth()->user()) { $teams = auth()->user()->teams->pluck('id'); - if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { + if (!$teams->contains($server->team_id) && !$teams->contains(0)) { throw new \Exception('User is not part of the team that owns this server'); } } @@ -46,9 +46,7 @@ function remote_process( return resolve(PrepareCoolifyTask::class, [ 'remoteProcessArgs' => new CoolifyTaskArgs( server_uuid: $server->uuid, - command: <<privateKey->getKeyLocation(); + ray($sshKeyLocation); $mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename(); return [ - 'location' => $location, - 'mux_filename' => $mux_filename, - 'private_key_filename' => $private_key_filename, + 'sshKeyLocation' => $sshKeyLocation, + 'muxFilename' => $mux_filename, ]; } -function savePrivateKeyToFs(Server $server) -{ - if (data_get($server, 'privateKey.private_key') === null) { - throw new \Exception("Server {$server->name} does not have a private key"); - } - ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server); - Storage::disk('ssh-keys')->makeDirectory('.'); - Storage::disk('ssh-mux')->makeDirectory('.'); - Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key); - - return $location; -} function generateScpCommand(Server $server, string $source, string $dest) { + $sshConfig = server_ssh_configuration($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; + $muxSocket = $sshConfig['muxFilename']; + $user = $server->user; $port = $server->port; - $privateKeyLocation = savePrivateKeyToFs($server); $timeout = config('constants.ssh.command_timeout'); $connectionTimeout = config('constants.ssh.connection_timeout'); $serverInterval = config('constants.ssh.server_interval'); @@ -99,21 +84,20 @@ function generateScpCommand(Server $server, string $source, string $dest) $scp_command = "timeout $timeout scp "; $muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false; - // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); + ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); if ($muxEnabled) { - $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; ensureMultiplexedConnection($server); - // ray('Using SSH Multiplexing')->green(); + ray('Using SSH Multiplexing')->green(); } else { - // ray('Not using SSH Multiplexing')->red(); + ray('Not using SSH Multiplexing')->red(); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { $scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; } - $scp_command .= "-i {$privateKeyLocation} " + $scp_command .= "-i {$sshKeyLocation} " .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' .'-o PasswordAuthentication=no ' ."-o ConnectTimeout=$connectionTimeout " @@ -126,6 +110,7 @@ function generateScpCommand(Server $server, string $source, string $dest) return $scp_command; } + function instant_scp(string $source, string $dest, Server $server, $throwError = true) { $timeout = config('constants.ssh.command_timeout'); @@ -146,48 +131,52 @@ function instant_scp(string $source, string $dest, Server $server, $throwError = return $output; } + function generateSshCommand(Server $server, string $command) { if ($server->settings->force_disabled) { throw new \RuntimeException('Server is disabled.'); } - $user = $server->user; - $port = $server->port; - $privateKeyLocation = savePrivateKeyToFs($server); + + $sshConfig = server_ssh_configuration($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; + $muxSocket = $sshConfig['muxFilename']; + $timeout = config('constants.ssh.command_timeout'); $connectionTimeout = config('constants.ssh.connection_timeout'); $serverInterval = config('constants.ssh.server_interval'); $muxPersistTime = config('constants.ssh.mux_persist_time'); + $muxEnabled = config('constants.ssh.mux_enabled') && !config('coolify.is_windows_docker_desktop'); $ssh_command = "timeout $timeout ssh "; - $muxEnabled = config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false; - // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); + ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); + if ($muxEnabled) { - // Always use multiplexing when enabled - $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; ensureMultiplexedConnection($server); - // ray('Using SSH Multiplexing')->green(); + ray('Using SSH Multiplexing')->green(); } else { - // ray('Not using SSH Multiplexing')->red(); + ray('Not using SSH Multiplexing')->red(); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; } + $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $delimiter = Hash::make($command); $command = str_replace($delimiter, '', $command); - $ssh_command .= "-i {$privateKeyLocation} " + + $ssh_command .= "-i {$sshKeyLocation} " .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' .'-o PasswordAuthentication=no ' ."-o ConnectTimeout=$connectionTimeout " ."-o ServerAliveInterval=$serverInterval " .'-o RequestTTY=no ' .'-o LogLevel=ERROR ' - ."-p {$port} " - ."{$user}@{$server->ip} " + ."-p {$server->port} " + ."{$server->user}@{$server->ip} " ." 'bash -se' << \\$delimiter".PHP_EOL .$command.PHP_EOL .$delimiter; @@ -197,52 +186,33 @@ function generateSshCommand(Server $server, string $command) function ensureMultiplexedConnection(Server $server) { - if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) { + static $ensuredConnections = []; + + $sshConfig = server_ssh_configuration($server); + $muxSocket = $sshConfig['muxFilename']; + + if (isset($ensuredConnections[$server->id]) && !shouldResetMultiplexedConnection($server)) { return; } - static $ensuredConnections = []; - - if (isset($ensuredConnections[$server->id])) { - if (! shouldResetMultiplexedConnection($server)) { - // ray('Using Existing Multiplexed Connection')->green(); - - return; - } - } - - $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; - $checkCommand = "ssh -O check -o ControlPath=$muxSocket "; - if (data_get($server, 'settings.is_cloudflare_tunnel')) { - $checkCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; - } - $checkCommand .= " {$server->user}@{$server->ip}"; - + $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; $process = Process::run($checkCommand); if ($process->exitCode() === 0) { - // ray('Existing Multiplexed Connection is Valid')->green(); $ensuredConnections[$server->id] = [ 'timestamp' => now(), 'muxSocket' => $muxSocket, ]; - return; } - // ray('Establishing New Multiplexed Connection')->orange(); - - $privateKeyLocation = savePrivateKeyToFs($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; $connectionTimeout = config('constants.ssh.connection_timeout'); $serverInterval = config('constants.ssh.server_interval'); $muxPersistTime = config('constants.ssh.mux_persist_time'); - $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; - - if (data_get($server, 'settings.is_cloudflare_tunnel')) { - $establishCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; - } - $establishCommand .= "-i {$privateKeyLocation} " + $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} " + ."-i {$sshKeyLocation} " .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' .'-o PasswordAuthentication=no ' ."-o ConnectTimeout=$connectionTimeout " @@ -262,8 +232,6 @@ function ensureMultiplexedConnection(Server $server) 'timestamp' => now(), 'muxSocket' => $muxSocket, ]; - - // ray('Established New Multiplexed Connection')->green(); } function shouldResetMultiplexedConnection(Server $server) @@ -320,7 +288,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool $process = Process::timeout($timeout)->run($sshCommand); $end_time = microtime(true); - $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds + // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds // ray('SSH command execution time:', $execution_time.' ms')->orange(); $output = trim($process->output()); @@ -339,6 +307,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool return $output; } + function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) { $ignoredErrors = collect([ @@ -358,6 +327,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) } throw new \RuntimeException($errorOutput, $exitCode); } + function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection { $application = Application::find(data_get($application_deployment_queue, 'application_id')); @@ -424,16 +394,20 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d return $formatted; } + function remove_iip($text) { $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text); return preg_replace('/\x1b\[[0-9;]*m/', '', $text); } + function remove_mux_and_private_key(Server $server) { - $muxFilename = $server->muxFilename(); - $privateKeyLocation = savePrivateKeyToFs($server); + $sshConfig = server_ssh_configuration($server); + $privateKeyLocation = $sshConfig['sshKeyLocation']; + $muxFilename = basename($sshConfig['muxFilename']); + $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}"; Process::run($closeCommand); @@ -441,13 +415,15 @@ function remove_mux_and_private_key(Server $server) Storage::disk('ssh-mux')->delete($muxFilename); Storage::disk('ssh-keys')->delete($privateKeyLocation); } + function refresh_server_connection(?PrivateKey $private_key = null) { if (is_null($private_key)) { return; } foreach ($private_key->servers as $server) { - $muxFilename = $server->muxFilename(); + $sshConfig = server_ssh_configuration($server); + $muxFilename = basename($sshConfig['muxFilename']); $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}"; Process::run($closeCommand); Storage::disk('ssh-mux')->delete($muxFilename); diff --git a/routes/web.php b/routes/web.php index e2ccfc704..98995dd99 100644 --- a/routes/web.php +++ b/routes/web.php @@ -264,7 +264,7 @@ Route::middleware(['auth'])->group(function () { } else { $server = $execution->scheduledDatabaseBackup->database->destination->server; } - $privateKeyLocation = savePrivateKeyToFs($server); + $privateKeyLocation = $server->privateKey->getKeyLocation(); $disk = Storage::build([ 'driver' => 'sftp', 'host' => $server->ip,