Files
coolify/app/Livewire/Server/Delete.php
T
Andras BacsaiandClaude Opus 5.5 cf9f254507 fix(server-transfer): close security holes in import, claim, and archives
Import:
- Check every uuid in a bundle against a strict id format before any
  write, and quote the SSH mux socket and key paths in local shell
  commands. A bundle uuid could run commands in the Coolify container.
- Validate bundle fqdns, git source URLs, paths, networks, cron
  expressions, and database ports; cap the number of items.
- Do not confirm that another team has a uuid.
- Refuse a project icon path with a uuid that is not a plain id.

Claim, finish, and migrate:
- Pass checklist step keys to wire:click with Js::from (stored XSS).
- Link only to https target server URLs.
- Validate target_instance_url in /transfer/complete; never prefill an
  unverified URL for a transfer back; scope the transfer-back token to
  write+deploy for 1 hour.
- Release on an owner marker only when it is newer than the own claim.
- Do not touch apps that only use the server as an additional
  destination when an archive is released or deleted.
- Give Sentinel a new token on claim.
- Add a checklist step to remove the previous instance's SSH key.
- Block a transfer while backups or scheduled tasks run.
- Authorize refreshMigration and PrivateKey checkConnection, clear
  tokens from Livewire state, hide migration log/error from members.

Archives:
- Refuse ip, user, port, private key, and Cloudflare tunnel changes.
- Throttle Enable management and require a free server slot for it and
  for a transfer back.
- Skip cloud provider calls.
- Re-read force_disabled before each SSH command, so running jobs stop.
- Probe the import check target at the resolved address only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:02:32 +02:00

160 lines
6.1 KiB
PHP

<?php
namespace App\Livewire\Server;
use App\Actions\Server\DeleteServer;
use App\Jobs\DeleteResourceJob;
use App\Models\Server;
use App\Services\ServerTransfer\ServerTransferClaimer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Delete extends Component
{
use AuthorizesRequests;
public Server $server;
public bool $delete_from_hetzner = false;
public bool $delete_from_vultr = false;
public bool $delete_from_digitalocean = false;
public bool $delete_from_hostinger = false;
public bool $force_delete_resources = false;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete($password, $selectedActions = [])
{
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
}
if (! empty($selectedActions)) {
$this->delete_from_hetzner = in_array('delete_from_hetzner', $selectedActions);
$this->delete_from_vultr = in_array('delete_from_vultr', $selectedActions);
$this->delete_from_digitalocean = in_array('delete_from_digitalocean', $selectedActions);
$this->delete_from_hostinger = in_array('delete_from_hostinger', $selectedActions);
$this->force_delete_resources = in_array('force_delete_resources', $selectedActions);
}
// Another Coolify instance manages a transferred server. This instance deletes only its own records:
// never the VM, and never the containers that the other instance runs.
$isArchive = $this->server->isTransferredAway();
if ($isArchive) {
$this->delete_from_hetzner = false;
$this->delete_from_vultr = false;
$this->delete_from_digitalocean = false;
$this->delete_from_hostinger = false;
$this->force_delete_resources = true;
}
try {
$this->authorize('delete', $this->server);
if ($this->server->is_coolify_host) {
$this->dispatch('error', 'The Coolify host server cannot be deleted.');
return;
}
if ($this->server->hasDefinedResources() && ! $this->force_delete_resources) {
$this->dispatch('error', 'Server has defined resources. Please delete them first or select "Delete all resources".');
return;
}
$resources = $this->server->definedResources();
if ($isArchive) {
$claimer = app(ServerTransferClaimer::class);
$claimer->forgetDnsRecords($this->server);
// Applications that use the server only as an additional destination stay; they lose this server.
$claimer->detachAdditionalApplications($this->server);
$resources = $claimer->archiveResources($this->server);
}
if ($this->force_delete_resources) {
foreach ($resources as $resource) {
DeleteResourceJob::dispatch($resource, deleteFromCoolifyOnly: $isArchive);
}
}
$this->server->delete();
DeleteServer::dispatch(
$this->server->id,
$this->delete_from_hetzner,
$this->server->hetzner_server_id,
$this->server->cloud_provider_token_id,
$this->server->team_id,
$this->delete_from_vultr,
$this->server->vultr_instance_id,
$this->delete_from_digitalocean,
$this->server->digitalocean_droplet_id,
$this->delete_from_hostinger,
$this->server->hostinger_virtual_machine_id
);
return redirectRoute($this, 'server.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
$checkboxes = [];
if ($this->server->hasDefinedResources() && ! $this->server->isTransferredAway()) {
$resourceCount = $this->server->definedResources()->count();
$checkboxes[] = [
'id' => 'force_delete_resources',
'label' => "Delete all resources ({$resourceCount} total)",
'default_warning' => 'Server cannot be deleted while it has resources.',
];
}
$canDeleteFromProvider = ! $this->server->isTransferredAway();
if ($canDeleteFromProvider && $this->server->hetzner_server_id) {
$checkboxes[] = [
'id' => 'delete_from_hetzner',
'label' => 'Also delete server from Hetzner Cloud',
'default_warning' => 'The actual server on Hetzner Cloud will NOT be deleted.',
];
}
if ($canDeleteFromProvider && $this->server->vultr_instance_id) {
$checkboxes[] = [
'id' => 'delete_from_vultr',
'label' => 'Also delete server from Vultr',
'default_warning' => 'The actual server on Vultr will NOT be deleted.',
];
}
if ($canDeleteFromProvider && $this->server->digitalocean_droplet_id) {
$checkboxes[] = [
'id' => 'delete_from_digitalocean',
'label' => 'Also delete droplet from DigitalOcean',
'default_warning' => 'The actual droplet on DigitalOcean will NOT be deleted.',
];
}
if ($canDeleteFromProvider && $this->server->hostinger_virtual_machine_id) {
$checkboxes[] = [
'id' => 'delete_from_hostinger',
'label' => 'Also delete VPS from Hostinger (at the end of the paid period)',
'default_warning' => 'The VPS on Hostinger will NOT be deleted, and Hostinger will keep billing for it.',
];
}
return view('livewire.server.delete', [
'checkboxes' => $checkboxes,
]);
}
}