From 065d4825d35ee99630fd20bc6158b9982f27c32d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:29:53 +0200 Subject: [PATCH] fix(backups): pin S3 endpoint DNS for mc cp uploads mc --resolve applies per invocation and is not stored in the alias, so mc cp resolved the S3 host again inside the helper container. On hosts with split DNS this failed with "no such host", and the upload did not use the IP validated by SafeWebhookUrl. Fixes #12129 --- app/Jobs/DatabaseBackupJob.php | 2 +- app/Jobs/VolumeBackupJob.php | 2 +- tests/Feature/Jobs/DatabaseBackupJobTest.php | 2 +- tests/Feature/VolumeBackupTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 92d24bab3d..99e691af86 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -889,7 +889,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $resolveOptions = $resolveOptions === '' ? '' : ' '.$resolveOptions; $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set{$resolveOptions} temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}"; - $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp {$escapedBackupLocation} {$escapedS3Destination}"; + $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp{$resolveOptions} {$escapedBackupLocation} {$escapedS3Destination}"; instant_remote_process($commands, $this->server, true, false, $this->commandTimeout(), disableMultiplexing: true); $this->s3_uploaded = true; diff --git a/app/Jobs/VolumeBackupJob.php b/app/Jobs/VolumeBackupJob.php index d635894b09..7b1406233c 100644 --- a/app/Jobs/VolumeBackupJob.php +++ b/app/Jobs/VolumeBackupJob.php @@ -372,7 +372,7 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue .escapeshellarg($backupLocation.':'.$backupLocation.':ro').' '.escapeshellarg($image), 'docker exec '.escapeshellarg($containerName).' mc alias set'.$resolveOptions.' temporary ' .escapeshellarg($s3->endpoint).' '.escapeshellarg($s3->key).' '.escapeshellarg($s3->secret), - 'docker exec '.escapeshellarg($containerName).' mc cp '.escapeshellarg($backupLocation).' ' + 'docker exec '.escapeshellarg($containerName).' mc cp'.$resolveOptions.' '.escapeshellarg($backupLocation).' ' .escapeshellarg('temporary/'.$s3->bucket.$backupDirectory.'/'), ], $server, timeout: $this->timeout, disableMultiplexing: true); } finally { diff --git a/tests/Feature/Jobs/DatabaseBackupJobTest.php b/tests/Feature/Jobs/DatabaseBackupJobTest.php index a19958608f..a699a60eee 100644 --- a/tests/Feature/Jobs/DatabaseBackupJobTest.php +++ b/tests/Feature/Jobs/DatabaseBackupJobTest.php @@ -330,7 +330,7 @@ test('database backup job escapes the S3 copy destination argument', function () $source = file_get_contents(app_path('Jobs/DatabaseBackupJob.php')); expect($source)->toContain('$escapedS3Destination = escapeshellarg("temporary/{$bucket}{$this->backup_dir}/");') - ->and($source)->toContain('mc cp {$escapedBackupLocation} {$escapedS3Destination}') + ->and($source)->toContain('mc cp{$resolveOptions} {$escapedBackupLocation} {$escapedS3Destination}') ->and($source)->not->toContain('mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/'); }); diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index d247a7a4e6..c254cea724 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -2122,7 +2122,7 @@ it('keeps local-first archive creation and mc copy when retaining a local volume Process::assertRan(fn ($process) => str_contains($process->command, 'tar -I') && str_contains($process->command, '>') && ! str_contains($process->command, 'mc pipe')); - Process::assertRan(fn ($process) => str_contains($process->command, 'mc cp') + Process::assertRan(fn ($process) => str_contains($process->command, 'mc cp --resolve ') && ! str_contains($process->command, 'mc pipe')); });