{"record":{"id":"b63e81ca828146e6","repo":"coollabsio/coolify","slug":"s3-endpoint-is-not-allowed-error","errorCode":null,"errorMessage":"S3 endpoint is not allowed: {error}","messagePattern":"S3 endpoint is not allowed: (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"app/Models/S3Storage.php","lineNumber":182,"sourceCode":"        );\n    }\n\n    public function testConnection(bool $shouldSave = false)\n    {\n        try {\n            $validator = Validator::make(\n                [\n                    'endpoint' => $this['endpoint'],\n                    'bucket' => $this['bucket'],\n                ],\n                [\n                    'endpoint' => ['required', new SafeWebhookUrl(trustedInternalHosts: $this->trustedInternalHosts())],\n                    'bucket' => ['required', new ValidS3BucketName],\n                ],\n            );\n            $validator->fails();\n            if ($validator->errors()->has('endpoint')) {\n                throw new \\RuntimeException('S3 endpoint is not allowed: '.$validator->errors()->first('endpoint'));\n            }\n            if ($validator->errors()->has('bucket')) {\n                throw new \\RuntimeException('S3 bucket name is not allowed: '.$validator->errors()->first('bucket'));\n            }\n\n            $disk = $this->filesystem();\n            // Test the connection by listing files with ListObjectsV2 (S3)\n            $disk->files();\n\n            $this->unusable_email_sent = false;\n            $this->is_usable = true;\n        } catch (\\Throwable $e) {\n            $exception = $this->toUserFriendlyConnectionException($e);\n            $this->is_usable = false;\n            if ($this->unusable_email_sent === false && is_transactional_emails_enabled()) {\n                try {\n                    $mail = new MailMessage;\n                    $mail->subject('Coolify: S3 Storage Connection Error');","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/coollabsio/coolify/blob/70b9acc42467278373e00de77abb40684e25b395/app/Models/S3Storage.php#L164-L200","documentation":"Thrown by S3Storage::testConnection() (app/Models/S3Storage.php:182). Before attempting any S3 traffic, the endpoint URL is run through the SafeWebhookUrl rule (with the storage's trustedInternalHosts()), which is Coolify's SSRF guard: it rejects non-http(s) schemes, missing/invalid hosts, trailing-dot hostnames, 'localhost'/'.local'/'.internal'/'.cluster.local' names, link-local addresses (169.254.0.0/16 — cloud metadata), and loopback/private/reserved IPs — including IPs resolved via DNS — unless the target is explicitly allowlisted in InstanceSettings (webhook_allowed_internal_hosts) or trusted internal hosts. A failing endpoint aborts the connection test, marks the storage unusable, and (once) emails team admins.","triggerScenarios":"Saving or testing an S3 storage whose endpoint is an internal address: http://192.168.1.10:9000, http://10.x.x.x, http://minio.internal, http://localhost:9000, or a public-looking hostname that DNS-resolves to a private IP. Also https endpoints with a trailing dot or non-http scheme.","commonSituations":"Pointing Coolify backups at a self-hosted MinIO on the LAN; homelab setups where the S3 target is the same Docker network; split-horizon DNS resolving the bucket host to an internal IP; enabling the S3 storage test after tightening instance security settings.","solutions":["Allowlist the internal target: Settings → Advanced → Endpoint section, add the hostname or CIDR (e.g. 192.168.1.0/24 or minio.internal) to webhook_allowed_internal_hosts, then re-test.","Or use a publicly routable endpoint for the S3 target.","Fix URL hygiene: scheme http/https, no trailing dot, a real hostname (not localhost), and make sure DNS resolves the way you expect (dig +short <host>).","Remember DNS-resolved private IPs are blocked too — allowlisting must match the hostname that resolves, not just the literal IP."],"exampleFix":"// before\n$storage->update(['endpoint' => 'http://192.168.1.10:9000']);\n$storage->testConnection(); // S3 endpoint is not allowed: ... private address\n\n// after — allowlist the internal network on the instance, then re-test\n$settings = \\App\\Models\\InstanceSettings::find(0);\n$settings->webhook_allowed_internal_hosts = '192.168.1.0/24';\n$settings->save();\n$storage->testConnection();","handlingStrategy":"validation","validationCode":"// Pre-validate the endpoint with the same rule before saving/testing\nuse App\\Rules\\SafeWebhookUrl;\nValidator::validate([\n    'endpoint' => $endpoint,\n], [\n    'endpoint' => ['required', new SafeWebhookUrl(trustedInternalHosts: $storage->trustedInternalHosts())],\n]);","typeGuard":null,"tryCatchPattern":"try {\n    $storage->testConnection();\n} catch (\\Throwable $e) {\n    // testConnection() does not rethrow; it sets is_usable=false and emails admins.\n    if ($storage->fresh()->is_usable === false) {\n        return 'Endpoint blocked by SSRF guard — allowlist the internal host in Settings → Advanced, or use a public endpoint.';\n    }\n}","preventionTips":["For internal MinIO/S3 targets, pre-allowlist the hostname or CIDR in InstanceSettings webhook_allowed_internal_hosts (Settings → Advanced → Endpoint section).","Prefer real hostnames over raw IPs so the allowlist matches what DNS resolves to (resolved private IPs are also checked).","Avoid localhost/.local/.internal names, trailing dots, and non-http(s) schemes in endpoints."],"tags":["coolify","s3","ssrf","validation","backups","network-security"],"backgroundTag":"ssrf-protection-blocked-url","analyzedSha":"70b9acc42467278373e00de77abb40684e25b395","analyzedAt":"2026-08-17T01:41:01.313Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}