mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(s3): add a setting to skip TLS verification for S3 storages
S3 storages with a self-signed or private CA certificate failed with "cURL error 60", because the Coolify container and the backup helper do not use the CA certificates of the host. The new opt-in setting turns off the certificate check in the PHP S3 clients and sets MC_INSECURE for mc. Closes #9762 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a487d7c9d3
commit
7f521e496d
23 files changed
+196
-16
No files matched your search
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- Add an opt-in setting to also stop an older webhook deployment that is still building when a newer push arrives. A deployment that already started to replace containers always finishes. The applications API sets it with `is_auto_cancel_building_deployments_enabled`
|
||||
- Add `cancel_previous` (`none`, `queued`, or `building`) to `/api/v1/deploy` and the MCP `deploy` tool, so a deployment from the API or MCP can cancel the older webhook, API, and MCP deployments of the same application, pull request, and server. The default is `none`. Manual, restart, and rollback deployments are never cancelled
|
||||
- Show "Not used" on the TLS certificates of the Traefik proxy page when no route and no resource on the server uses the domain, and add a **Delete unused** button that deletes all of them in one step. Traefik renews every certificate in `acme.json`, also after you delete the resource, until you delete the certificate and restart the proxy ([#12447](https://github.com/coollabsio/coolify/issues/12447))
|
||||
- Add a **Skip TLS certificate verification** setting to S3 storages, for endpoints with a self-signed or private CA certificate, for example a MinIO server on an internal network. Before, the connection test failed with "cURL error 60", because the Coolify container and the backup helper do not use the CA certificates of the host. The setting applies to the connection test, backups, restores, and the deletion of old backups. The S3 storages API sets it with `skip_tls_verification`. Browsers that download files from the storage must still trust the certificate ([#9762](https://github.com/coollabsio/coolify/issues/9762), [#4402](https://github.com/coollabsio/coolify/issues/4402))
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ class StartDatabaseImport
|
||||
'docker rm -f '.escapeshellarg($helper).' 2>/dev/null || true',
|
||||
'docker run -d --network '.escapeshellarg($network)
|
||||
.' --name '.escapeshellarg($helper)
|
||||
.' '.$storage->minioClientRegionOption()
|
||||
.' '.$storage->minioClientEnvOptions()
|
||||
.' '.escapeshellarg($image).' sleep 86400',
|
||||
], $server);
|
||||
$resolveOptions = $storage->minioClientResolveOptions($server, $image, $network);
|
||||
|
||||
@@ -98,6 +98,7 @@ class S3StoragesController extends Controller
|
||||
'bucket' => ['type' => 'string'],
|
||||
'region' => ['type' => 'string'],
|
||||
'use_path_style_endpoint' => ['type' => 'boolean'],
|
||||
'skip_tls_verification' => ['type' => 'boolean'],
|
||||
'is_usable' => ['type' => 'boolean'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
@@ -162,6 +163,7 @@ class S3StoragesController extends Controller
|
||||
'bucket' => ['type' => 'string'],
|
||||
'region' => ['type' => 'string'],
|
||||
'use_path_style_endpoint' => ['type' => 'boolean'],
|
||||
'skip_tls_verification' => ['type' => 'boolean'],
|
||||
'is_usable' => ['type' => 'boolean'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
@@ -223,6 +225,7 @@ class S3StoragesController extends Controller
|
||||
'bucket' => ['type' => 'string', 'example' => 'my-bucket', 'description' => 'S3 bucket name.'],
|
||||
'region' => ['type' => 'string', 'example' => 'us-east-1', 'description' => 'S3 region.'],
|
||||
'use_path_style_endpoint' => ['type' => 'boolean', 'description' => 'Use path-style URLs (endpoint/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint). Default: false for Alibaba Cloud OSS, Tencent Cloud COS, and Volcengine TOS endpoints, true for all other endpoints.'],
|
||||
'skip_tls_verification' => ['type' => 'boolean', 'description' => 'Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false.'],
|
||||
'key' => ['type' => 'string', 'description' => 'Access key.'],
|
||||
'secret' => ['type' => 'string', 'description' => 'Secret key.'],
|
||||
'is_usable' => ['type' => 'boolean', 'description' => 'Whether the storage is marked usable.'],
|
||||
@@ -261,7 +264,7 @@ class S3StoragesController extends Controller
|
||||
)]
|
||||
public function store(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'endpoint', 'bucket', 'region', 'key', 'secret', 'use_path_style_endpoint', 'is_usable'];
|
||||
$allowedFields = ['name', 'description', 'endpoint', 'bucket', 'region', 'key', 'secret', 'use_path_style_endpoint', 'skip_tls_verification', 'is_usable'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -285,6 +288,7 @@ class S3StoragesController extends Controller
|
||||
'key' => 'required|string|max:255',
|
||||
'secret' => 'required|string|max:255',
|
||||
'use_path_style_endpoint' => 'sometimes|boolean',
|
||||
'skip_tls_verification' => 'sometimes|boolean',
|
||||
'is_usable' => 'sometimes|boolean',
|
||||
]);
|
||||
if ($validationError instanceof JsonResponse) {
|
||||
@@ -301,6 +305,7 @@ class S3StoragesController extends Controller
|
||||
'key' => $body['key'],
|
||||
'secret' => $body['secret'],
|
||||
'use_path_style_endpoint' => $body['use_path_style_endpoint'] ?? S3Storage::detectPathStyleEndpoint($body['endpoint']),
|
||||
'skip_tls_verification' => $body['skip_tls_verification'] ?? false,
|
||||
'is_usable' => $body['is_usable'] ?? false,
|
||||
]);
|
||||
|
||||
@@ -341,6 +346,7 @@ class S3StoragesController extends Controller
|
||||
'bucket' => ['type' => 'string', 'description' => 'S3 bucket name.'],
|
||||
'region' => ['type' => 'string', 'description' => 'S3 region.'],
|
||||
'use_path_style_endpoint' => ['type' => 'boolean', 'description' => 'Use path-style URLs (endpoint/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint), such as Alibaba Cloud OSS.'],
|
||||
'skip_tls_verification' => ['type' => 'boolean', 'description' => 'Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false.'],
|
||||
'key' => ['type' => 'string', 'description' => 'Access key.'],
|
||||
'secret' => ['type' => 'string', 'description' => 'Secret key.'],
|
||||
'is_usable' => ['type' => 'boolean', 'description' => 'Whether the storage is marked usable.'],
|
||||
@@ -379,7 +385,7 @@ class S3StoragesController extends Controller
|
||||
)]
|
||||
public function update(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'endpoint', 'bucket', 'region', 'key', 'secret', 'use_path_style_endpoint', 'is_usable'];
|
||||
$allowedFields = ['name', 'description', 'endpoint', 'bucket', 'region', 'key', 'secret', 'use_path_style_endpoint', 'skip_tls_verification', 'is_usable'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -402,6 +408,7 @@ class S3StoragesController extends Controller
|
||||
'key' => 'sometimes|string|max:255',
|
||||
'secret' => 'sometimes|string|max:255',
|
||||
'use_path_style_endpoint' => 'sometimes|boolean',
|
||||
'skip_tls_verification' => 'sometimes|boolean',
|
||||
'is_usable' => 'sometimes|boolean',
|
||||
]);
|
||||
if ($validationError instanceof JsonResponse) {
|
||||
|
||||
@@ -929,7 +929,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Remove a leftover helper in the same batch, so a replayed batch does not fail with a name conflict.
|
||||
$commands[] = "docker rm -f backup-of-{$this->backup_log_uuid} >/dev/null 2>&1 || true";
|
||||
$mount = escapeshellarg($this->backupMountSource().':'.$this->backup_location.':ro');
|
||||
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm {$s3->minioClientRegionOption()} -v {$mount} {$fullImageName}";
|
||||
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm {$s3->minioClientEnvOptions()} -v {$mount} {$fullImageName}";
|
||||
|
||||
$escapedBackupLocation = escapeshellarg($this->backup_location);
|
||||
$escapedS3Destination = escapeshellarg("temporary/{$bucket}{$this->backup_dir}/");
|
||||
|
||||
@@ -447,7 +447,7 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
instant_remote_process([
|
||||
'docker rm -f '.escapeshellarg($containerName).' >/dev/null 2>&1 || true',
|
||||
'docker run -d --name '.escapeshellarg($containerName).' --rm '.$s3->minioClientRegionOption().' -v '
|
||||
'docker run -d --name '.escapeshellarg($containerName).' --rm '.$s3->minioClientEnvOptions().' -v '
|
||||
.escapeshellarg($backupLocation.':'.$backupLocation.':ro').' '.escapeshellarg($image),
|
||||
], $server, timeout: $this->timeout, disableMultiplexing: true);
|
||||
$this->setMinioClientAlias($server, $containerName, $s3, $resolveOptions);
|
||||
@@ -475,7 +475,7 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$verifySourceCommand,
|
||||
'docker rm -f '.escapeshellarg($containerName).' >/dev/null 2>&1 || true',
|
||||
'docker run -d --rm --name '.escapeshellarg($containerName)
|
||||
.' '.$s3->minioClientRegionOption()
|
||||
.' '.$s3->minioClientEnvOptions()
|
||||
.' -v '.escapeshellarg($source.':/volume:ro')
|
||||
.' '.escapeshellarg($image).' >/dev/null',
|
||||
], $server, timeout: $this->timeout, disableMultiplexing: true);
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\StandaloneSqlite;
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use App\Support\DatabaseImport\DatabaseImportCommandBuilder;
|
||||
use App\Support\DatabaseImport\DatabaseImportException;
|
||||
use App\Support\DatabaseImport\DatabaseImportSource;
|
||||
@@ -625,7 +624,7 @@ class ImportForm extends Component
|
||||
'bucket' => $s3Storage->bucket,
|
||||
'endpoint' => $s3Storage->endpoint,
|
||||
'use_path_style_endpoint' => $s3Storage->usesPathStyleEndpoint(),
|
||||
'http' => SafeWebhookUrl::httpClientOptions($s3Storage->endpoint),
|
||||
'http' => $s3Storage->httpClientOptions(),
|
||||
]);
|
||||
|
||||
// Check if file exists
|
||||
|
||||
@@ -33,6 +33,8 @@ class Create extends Component
|
||||
|
||||
public bool $usePathStyleEndpointChanged = false;
|
||||
|
||||
public bool $skipTlsVerification = false;
|
||||
|
||||
public array $endpointParts = ['scheme' => 'https', 'host' => '', 'port' => '', 'path' => ''];
|
||||
|
||||
public bool $endpointPartsChanged = false;
|
||||
@@ -50,6 +52,7 @@ class Create extends Component
|
||||
'bucket' => ['required', new ValidS3BucketName],
|
||||
'endpoint' => ['required', 'max:255', new SafeWebhookUrl],
|
||||
'usePathStyleEndpoint' => 'boolean',
|
||||
'skipTlsVerification' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,6 +83,7 @@ class Create extends Component
|
||||
'bucket' => 'Bucket',
|
||||
'endpoint' => 'Endpoint',
|
||||
'usePathStyleEndpoint' => 'Use path-style URLs',
|
||||
'skipTlsVerification' => 'Skip TLS certificate verification',
|
||||
];
|
||||
|
||||
public function submit()
|
||||
@@ -100,6 +104,7 @@ class Create extends Component
|
||||
$this->storage->secret = $this->secret;
|
||||
$this->storage->bucket = $this->bucket;
|
||||
$this->storage->use_path_style_endpoint = $this->usePathStyleEndpoint;
|
||||
$this->storage->skip_tls_verification = $this->skipTlsVerification;
|
||||
if (empty($this->endpoint)) {
|
||||
$this->storage->endpoint = "https://s3.{$this->region}.amazonaws.com";
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,8 @@ class Form extends Component
|
||||
|
||||
public bool $usePathStyleEndpointChanged = false;
|
||||
|
||||
public bool $skipTlsVerification = false;
|
||||
|
||||
public string $key;
|
||||
|
||||
public string $secret;
|
||||
@@ -57,6 +59,7 @@ class Form extends Component
|
||||
'bucket' => ['required', new ValidS3BucketName],
|
||||
'endpoint' => ['required', 'max:255', new SafeWebhookUrl],
|
||||
'usePathStyleEndpoint' => 'boolean',
|
||||
'skipTlsVerification' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ class Form extends Component
|
||||
'bucket' => 'Bucket',
|
||||
'endpoint' => 'Endpoint',
|
||||
'usePathStyleEndpoint' => 'Use path-style URLs',
|
||||
'skipTlsVerification' => 'Skip TLS certificate verification',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -105,6 +109,7 @@ class Form extends Component
|
||||
$this->storage->bucket = $this->bucket;
|
||||
$this->storage->region = $this->region;
|
||||
$this->storage->use_path_style_endpoint = $this->usePathStyleEndpoint;
|
||||
$this->storage->skip_tls_verification = $this->skipTlsVerification;
|
||||
$this->storage->key = $this->key;
|
||||
$this->storage->secret = $this->secret;
|
||||
$this->storage->is_usable = $this->isUsable;
|
||||
@@ -120,6 +125,7 @@ class Form extends Component
|
||||
$this->usePathStyleEndpoint = $this->storage->usesPathStyleEndpoint();
|
||||
// A saved style that detection would not pick is a user choice, so keep it when the endpoint changes.
|
||||
$this->usePathStyleEndpointChanged = $this->usePathStyleEndpoint !== S3Storage::detectPathStyleEndpoint($this->endpoint);
|
||||
$this->skipTlsVerification = (bool) $this->storage->skip_tls_verification;
|
||||
$canSeeCredentials = auth()->user()?->can('update', $this->storage) ?? false;
|
||||
$this->key = $canSeeCredentials ? $this->storage->key : '';
|
||||
$this->secret = $canSeeCredentials ? $this->storage->secret : '';
|
||||
@@ -153,6 +159,7 @@ class Form extends Component
|
||||
$testedStorage->bucket = $this->bucket;
|
||||
$testedStorage->region = $this->region;
|
||||
$testedStorage->use_path_style_endpoint = $this->usePathStyleEndpoint;
|
||||
$testedStorage->skip_tls_verification = $this->skipTlsVerification;
|
||||
$testedStorage->key = $this->key;
|
||||
$testedStorage->secret = $this->secret;
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class S3Storage extends BaseModel
|
||||
'bucket',
|
||||
'endpoint',
|
||||
'use_path_style_endpoint',
|
||||
'skip_tls_verification',
|
||||
'is_usable',
|
||||
'unusable_email_sent',
|
||||
];
|
||||
@@ -71,6 +72,7 @@ class S3Storage extends BaseModel
|
||||
protected $casts = [
|
||||
'is_usable' => 'boolean',
|
||||
'use_path_style_endpoint' => 'boolean',
|
||||
'skip_tls_verification' => 'boolean',
|
||||
'key' => 'encrypted',
|
||||
'secret' => 'encrypted',
|
||||
];
|
||||
@@ -292,7 +294,7 @@ class S3Storage extends BaseModel
|
||||
'bucket' => $this['bucket'],
|
||||
'endpoint' => $this['endpoint'],
|
||||
'use_path_style_endpoint' => $this->usesPathStyleEndpoint(),
|
||||
'http' => array_merge(SafeWebhookUrl::httpClientOptions($this['endpoint'], $this->trustedInternalHosts()), [
|
||||
'http' => array_merge($this->httpClientOptions(), [
|
||||
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
|
||||
]),
|
||||
@@ -300,12 +302,27 @@ class S3Storage extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* The `docker run` option that makes the MinIO client sign requests for the storage region, as filesystem() does.
|
||||
* Without it, mc signs for us-east-1, and providers that enforce the region reject the requests.
|
||||
* The Guzzle options of every request from Coolify to the endpoint: the DNS pin of SafeWebhookUrl, and the TLS
|
||||
* certificate check, which a storage with a self-signed or private CA certificate can turn off.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function minioClientRegionOption(): string
|
||||
public function httpClientOptions(): array
|
||||
{
|
||||
return '-e '.escapeshellarg('MC_REGION='.$this->region);
|
||||
return array_merge(SafeWebhookUrl::httpClientOptions((string) $this->endpoint, $this->trustedInternalHosts()), [
|
||||
'verify' => ! $this->skip_tls_verification,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The `docker run` options of the MinIO client helper. `MC_REGION` makes mc sign requests for the storage region,
|
||||
* as filesystem() does. Without it, mc signs for us-east-1, and providers that enforce the region reject the
|
||||
* requests. `MC_INSECURE` turns off the TLS certificate check, as httpClientOptions() does.
|
||||
*/
|
||||
public function minioClientEnvOptions(): string
|
||||
{
|
||||
return '-e '.escapeshellarg('MC_REGION='.$this->region)
|
||||
.($this->skip_tls_verification ? ' -e MC_INSECURE=true' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Services;
|
||||
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use Aws\S3\S3Client;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -122,7 +121,7 @@ class FileDownloadStorageService
|
||||
'use_path_style_endpoint' => $storage->usesPathStyleEndpoint(),
|
||||
'credentials' => false,
|
||||
'retries' => 0,
|
||||
'http' => array_merge(SafeWebhookUrl::httpClientOptions($storage->endpoint, $storage->trustedInternalHosts()), [
|
||||
'http' => array_merge($storage->httpClientOptions(), [
|
||||
'connect_timeout' => 15,
|
||||
'timeout' => 15,
|
||||
]),
|
||||
|
||||
@@ -1490,6 +1490,7 @@ class ServerTransferExporter
|
||||
'bucket' => $storage->bucket,
|
||||
'endpoint' => $storage->endpoint,
|
||||
'use_path_style_endpoint' => $storage->usesPathStyleEndpoint(),
|
||||
'skip_tls_verification' => (bool) $storage->skip_tls_verification,
|
||||
'is_usable' => (bool) $storage->is_usable,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2008,6 +2008,7 @@ class ServerTransferImporter
|
||||
'bucket' => data_get($payload, 'bucket'),
|
||||
'endpoint' => data_get($payload, 'endpoint'),
|
||||
'use_path_style_endpoint' => (bool) data_get($payload, 'use_path_style_endpoint', true),
|
||||
'skip_tls_verification' => (bool) data_get($payload, 'skip_tls_verification', false),
|
||||
'is_usable' => (bool) data_get($payload, 'is_usable', true),
|
||||
]);
|
||||
$storage->uuid = $uuid;
|
||||
|
||||
@@ -285,6 +285,7 @@ function deleteBackupsS3(string|array|null $filenames, S3Storage $s3): void
|
||||
'endpoint' => $s3->endpoint,
|
||||
'use_path_style_endpoint' => $s3->usesPathStyleEndpoint(),
|
||||
'aws_url' => $s3->awsUrl(),
|
||||
'http' => ['verify' => ! $s3->skip_tls_verification],
|
||||
'throw' => true,
|
||||
]);
|
||||
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('s3_storages', function (Blueprint $table) {
|
||||
$table->boolean('skip_tls_verification')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('s3_storages', function (Blueprint $table) {
|
||||
$table->dropColumn('skip_tls_verification');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -15737,6 +15737,9 @@
|
||||
"use_path_style_endpoint": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"skip_tls_verification": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_usable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -15820,6 +15823,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Use path-style URLs (endpoint\/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint). Default: false for Alibaba Cloud OSS, Tencent Cloud COS, and Volcengine TOS endpoints, true for all other endpoints."
|
||||
},
|
||||
"skip_tls_verification": {
|
||||
"type": "boolean",
|
||||
"description": "Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false."
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Access key."
|
||||
@@ -15921,6 +15928,9 @@
|
||||
"use_path_style_endpoint": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"skip_tls_verification": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_usable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -16050,6 +16060,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Use path-style URLs (endpoint\/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint), such as Alibaba Cloud OSS."
|
||||
},
|
||||
"skip_tls_verification": {
|
||||
"type": "boolean",
|
||||
"description": "Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false."
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Access key."
|
||||
|
||||
+8
-1
@@ -10045,7 +10045,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
properties: { uuid: { type: string }, name: { type: string }, description: { type: string, nullable: true }, endpoint: { type: string }, bucket: { type: string }, region: { type: string }, use_path_style_endpoint: { type: boolean }, is_usable: { type: boolean }, team_id: { type: integer }, created_at: { type: string }, updated_at: { type: string } }
|
||||
properties: { uuid: { type: string }, name: { type: string }, description: { type: string, nullable: true }, endpoint: { type: string }, bucket: { type: string }, region: { type: string }, use_path_style_endpoint: { type: boolean }, skip_tls_verification: { type: boolean }, is_usable: { type: boolean }, team_id: { type: integer }, created_at: { type: string }, updated_at: { type: string } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
@@ -10097,6 +10097,9 @@ paths:
|
||||
use_path_style_endpoint:
|
||||
type: boolean
|
||||
description: 'Use path-style URLs (endpoint/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint). Default: false for Alibaba Cloud OSS, Tencent Cloud COS, and Volcengine TOS endpoints, true for all other endpoints.'
|
||||
skip_tls_verification:
|
||||
type: boolean
|
||||
description: 'Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false.'
|
||||
key:
|
||||
type: string
|
||||
description: 'Access key.'
|
||||
@@ -10154,6 +10157,7 @@ paths:
|
||||
bucket: { type: string }
|
||||
region: { type: string }
|
||||
use_path_style_endpoint: { type: boolean }
|
||||
skip_tls_verification: { type: boolean }
|
||||
is_usable: { type: boolean }
|
||||
team_id: { type: integer }
|
||||
created_at: { type: string }
|
||||
@@ -10236,6 +10240,9 @@ paths:
|
||||
use_path_style_endpoint:
|
||||
type: boolean
|
||||
description: 'Use path-style URLs (endpoint/bucket). Set to false for providers that only accept virtual-hosted-style URLs (bucket.endpoint), such as Alibaba Cloud OSS.'
|
||||
skip_tls_verification:
|
||||
type: boolean
|
||||
description: 'Do not check the TLS certificate of the endpoint. Use only for endpoints with a self-signed or private CA certificate. Default: false.'
|
||||
key:
|
||||
type: string
|
||||
description: 'Access key.'
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox id="usePathStyleEndpoint" label="Use path-style URLs"
|
||||
helper="Turn this off for providers that only accept virtual-hosted-style URLs (bucket.endpoint). Coolify turns it off for Alibaba Cloud OSS, Tencent Cloud COS, and Volcengine TOS, and when the provider rejects path-style URLs." />
|
||||
<x-forms.checkbox id="skipTlsVerification" label="Skip TLS certificate verification"
|
||||
helper="Turn this on only for an endpoint with a self-signed or private CA certificate. Coolify and the backup helper then accept any certificate, so an attacker on the network path can read or change your backups. Browsers that download files from this storage must still trust the certificate." />
|
||||
</div>
|
||||
|
||||
<x-forms.button class="mt-4" type="submit" wire:target="submit">
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
<x-forms.checkbox canGate="update" :canResource="$storage" id="usePathStyleEndpoint"
|
||||
label="Use path-style URLs"
|
||||
helper="Turn this off for providers that only accept virtual-hosted-style URLs (bucket.endpoint). Coolify turns it off for Alibaba Cloud OSS, Tencent Cloud COS, and Volcengine TOS, and when the provider rejects path-style URLs." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$storage" id="skipTlsVerification"
|
||||
label="Skip TLS certificate verification"
|
||||
helper="Turn this on only for an endpoint with a self-signed or private CA certificate. Coolify and the backup helper then accept any certificate, so an attacker on the network path can read or change your backups. Browsers that download files from this storage must still trust the certificate." />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
</form>
|
||||
@@ -280,6 +280,19 @@ describe('POST /api/v1/s3-storages', function () {
|
||||
->and(S3Storage::where('name', 'Detected')->sole()->use_path_style_endpoint)->toBeFalse();
|
||||
});
|
||||
|
||||
test('stores the TLS verification setting, which checks the certificate by default', function () {
|
||||
$headers = ['Authorization' => 'Bearer '.$this->bearerToken, 'Content-Type' => 'application/json'];
|
||||
|
||||
$this->withHeaders($headers)->postJson('/api/v1/s3-storages', validS3StoragePayload(['name' => 'Default']))->assertStatus(201);
|
||||
$this->withHeaders($headers)->postJson('/api/v1/s3-storages', validS3StoragePayload([
|
||||
'name' => 'Self-signed',
|
||||
'skip_tls_verification' => true,
|
||||
]))->assertStatus(201);
|
||||
|
||||
expect(S3Storage::where('name', 'Default')->sole()->skip_tls_verification)->toBeFalse()
|
||||
->and(S3Storage::where('name', 'Self-signed')->sole()->skip_tls_verification)->toBeTrue();
|
||||
});
|
||||
|
||||
test('validates name is required', function () {
|
||||
$payload = validS3StoragePayload();
|
||||
unset($payload['name']);
|
||||
@@ -409,6 +422,20 @@ describe('PATCH /api/v1/s3-storages/{uuid}', function () {
|
||||
expect($storage->fresh()->use_path_style_endpoint)->toBeFalse();
|
||||
});
|
||||
|
||||
test('updates the TLS verification setting', function () {
|
||||
$storage = createS3StorageForTeam($this->team);
|
||||
expect($storage->fresh()->skip_tls_verification)->toBeFalse();
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->patchJson("/api/v1/s3-storages/{$storage->uuid}", [
|
||||
'skip_tls_verification' => true,
|
||||
])->assertStatus(200);
|
||||
|
||||
expect($storage->fresh()->skip_tls_verification)->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects empty body', function () {
|
||||
$storage = createS3StorageForTeam($this->team);
|
||||
|
||||
|
||||
@@ -205,6 +205,36 @@ it('makes mc use virtual-hosted-style URLs when the storage turns path-style URL
|
||||
'virtual-hosted-style' => [false, 'mc alias set --api S3v4 --path off temporary https://8.8.8.8'],
|
||||
]);
|
||||
|
||||
it('makes mc skip the TLS certificate check only when the storage skips it', function (bool $skipTlsVerification) {
|
||||
[$team, $server, $storage] = s3ArgvTestSetup('root');
|
||||
$storage->update(['skip_tls_verification' => $skipTlsVerification]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $team->id])->id]);
|
||||
$database = create_standalone_postgresql($environment->id, $destination, ['status' => 'running:healthy']);
|
||||
$backup = new ScheduledDatabaseBackup;
|
||||
$backup->forceFill([
|
||||
'enabled' => true,
|
||||
'save_s3' => true,
|
||||
'disable_local_backup' => true,
|
||||
's3_storage_id' => $storage->id,
|
||||
'frequency' => 'daily',
|
||||
'databases_to_backup' => 'app',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => $database->getMorphClass(),
|
||||
'team_id' => $team->id,
|
||||
])->save();
|
||||
$backup->syncS3Storages([$storage->id]);
|
||||
$processes = s3ArgvTestFakeSsh();
|
||||
|
||||
(new DatabaseBackupJob($backup->fresh()))->handle();
|
||||
|
||||
$helperStart = $processes->sole(fn (array $process) => str_contains($process['command']."\n".$process['input'], '--name backup-of-'));
|
||||
expect(str_contains($helperStart['command']."\n".$helperStart['input'], '-e MC_INSECURE=true'))->toBe($skipTlsVerification);
|
||||
})->with([
|
||||
'checks the certificate' => [false],
|
||||
'skips the check' => [true],
|
||||
]);
|
||||
|
||||
it('passes the S3 keys of a volume backup to mc on stdin, not in a command line', function (string $sshUser, bool $streamToS3) {
|
||||
[$team, $server, $storage] = s3ArgvTestSetup($sshUser);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
|
||||
@@ -56,6 +56,7 @@ it('tests the S3 connection with the values currently entered in the form', func
|
||||
$form->key = 'new-key';
|
||||
$form->secret = 'new-secret';
|
||||
$form->usePathStyleEndpoint = false;
|
||||
$form->skipTlsVerification = true;
|
||||
$form->isUsable = false;
|
||||
|
||||
$form->testConnection();
|
||||
@@ -70,5 +71,6 @@ it('tests the S3 connection with the values currently entered in the form', func
|
||||
'secret' => 'new-secret',
|
||||
'use_path_style_endpoint' => false,
|
||||
])
|
||||
->and($testedConfig['http']['verify'])->toBeFalse()
|
||||
->and($form->isUsable)->toBeTrue();
|
||||
});
|
||||
@@ -64,6 +64,7 @@ test('S3Storage model fillable attributes are configured correctly', function ()
|
||||
'bucket',
|
||||
'endpoint',
|
||||
'use_path_style_endpoint',
|
||||
'skip_tls_verification',
|
||||
'is_usable',
|
||||
'unusable_email_sent',
|
||||
]);
|
||||
@@ -122,6 +123,30 @@ test('S3Storage uses path-style URLs unless the storage turns them off', functio
|
||||
'virtual-hosted-style' => [false, false, ' --api S3v4 --path off'],
|
||||
]);
|
||||
|
||||
test('S3Storage checks the TLS certificate unless the storage skips the check', function (?bool $setting, bool $verify, string $mcOptions) {
|
||||
$disk = Mockery::mock(FilesystemAdapter::class);
|
||||
Storage::expects('build')
|
||||
->once()
|
||||
->with(Mockery::on(fn (array $config) => $config['http']['verify'] === $verify))
|
||||
->andReturn($disk);
|
||||
|
||||
$s3Storage = new S3Storage;
|
||||
$s3Storage->setRawAttributes([
|
||||
'region' => 'us-east-1',
|
||||
'bucket' => 'backups',
|
||||
'endpoint' => 'https://93.184.216.34:9000',
|
||||
'skip_tls_verification' => $setting,
|
||||
]);
|
||||
|
||||
expect($s3Storage->filesystem())->toBe($disk)
|
||||
->and($s3Storage->httpClientOptions()['verify'])->toBe($verify)
|
||||
->and($s3Storage->minioClientEnvOptions())->toBe($mcOptions);
|
||||
})->with([
|
||||
'not saved yet' => [null, true, "-e 'MC_REGION=us-east-1'"],
|
||||
'checks the certificate' => [false, true, "-e 'MC_REGION=us-east-1'"],
|
||||
'skips the check' => [true, false, "-e 'MC_REGION=us-east-1' -e MC_INSECURE=true"],
|
||||
]);
|
||||
|
||||
test('S3Storage detects the URL style from the endpoint host', function (?string $endpoint, bool $usesPathStyle) {
|
||||
expect(S3Storage::detectPathStyleEndpoint($endpoint))->toBe($usesPathStyle);
|
||||
})->with([
|
||||
|
||||
@@ -151,3 +151,13 @@ test('transfer imports bundles that only have the primary S3 destination', funct
|
||||
expect(transferredBackupBuckets(ScheduledDatabaseBackup::where('team_id', $targetTeam->id)->sole()))->toBe(['second-bucket'])
|
||||
->and(transferredBackupBuckets(ScheduledVolumeBackup::where('team_id', $targetTeam->id)->sole()))->toBe(['second-bucket']);
|
||||
});
|
||||
|
||||
test('transfer keeps the TLS verification setting of S3 storages', function () {
|
||||
$this->secondS3->update(['skip_tls_verification' => true]);
|
||||
$bundle = app(ServerTransferExporter::class)->export($this->server);
|
||||
|
||||
$targetTeam = importTransferBundleIntoNewTeam($bundle, $this);
|
||||
|
||||
expect(S3Storage::where('team_id', $targetTeam->id)->pluck('skip_tls_verification', 'bucket')->all())
|
||||
->toBe(['first-bucket' => false, 'second-bucket' => true]);
|
||||
});
|
||||
Reference in new issue
Block a user