diff --git a/README.md b/README.md index 13635cd659..66114586e8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Your configurations stay on your servers. If you stop using Coolify, your runnin ## What can Coolify do? - **Deploy any application:** Build from GitHub, GitLab, Bitbucket, or Gitea with Nixpacks, Railpack, Dockerfile, Docker Compose, or a prebuilt Docker image. -- **Run databases and services:** Deploy managed databases and more than 300 one-click services with persistent storage and generated credentials. +- **Run databases and services:** Deploy managed databases and more than 360 one-click services with persistent storage and generated credentials. - **Automate deployments:** Deploy on every Git push, create pull-request previews, call deployment webhooks, and roll back to retained application images. - **Manage networking:** Configure custom domains, automatic HTTPS certificates, reverse proxies, health checks, and container networks. - **Operate your infrastructure:** Manage multiple servers, inspect deployment and runtime logs, open container terminals, and monitor resource status. diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 1f0cf18da4..ac78a5c8de 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,17 +2,12 @@ namespace App\Exceptions; -use App\Models\InstanceSettings; -use App\Models\User; use App\Providers\RouteServiceProvider; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Session\TokenMismatchException; use Psr\Log\LogLevel; -use RuntimeException; -use Sentry\Laravel\Integration; -use Sentry\State\Scope; use Throwable; class Handler extends ExceptionHandler @@ -49,8 +44,6 @@ class Handler extends ExceptionHandler 'password_confirmation', ]; - private InstanceSettings $settings; - protected function unauthenticated($request, AuthenticationException $exception) { if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) { @@ -108,44 +101,4 @@ class Handler extends ExceptionHandler return parent::render($request, $e); } - - /** - * Register the exception handling callbacks for the application. - */ - public function register(): void - { - $this->reportable(function (Throwable $e) { - if (isDev()) { - return; - } - if ($e instanceof RuntimeException) { - return; - } - $this->settings = instanceSettings(); - if ($this->settings->do_not_track) { - return; - } - app('sentry')->configureScope( - function (Scope $scope) { - $email = auth()?->user() ? auth()->user()->email : 'guest'; - $instanceAdmin = User::find(0)->email ?? 'admin@localhost'; - $scope->setUser( - [ - 'email' => $email, - 'instanceAdmin' => $instanceAdmin, - ] - ); - } - ); - // Check for errors that should not be reported to Sentry - if (str($e->getMessage())->contains('No space left on device')) { - // Log locally but don't send to Sentry - logger()->warning('Disk space error: '.$e->getMessage()); - - return; - } - - Integration::captureUnhandledException($e); - }); - } } diff --git a/app/Exceptions/NonReportableException.php b/app/Exceptions/NonReportableException.php index 4c4672127f..b0846b5b66 100644 --- a/app/Exceptions/NonReportableException.php +++ b/app/Exceptions/NonReportableException.php @@ -5,7 +5,7 @@ namespace App\Exceptions; use Exception; /** - * Exception that should not be reported to Sentry or other error tracking services. + * Exception that should not be reported to error tracking services. * Use this for known, expected errors that don't require external tracking. */ class NonReportableException extends Exception diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 03e227ef5e..6de5989747 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -50,6 +50,8 @@ class GithubApp extends BaseModel 'type' => 'string', 'webhook_events' => 'array', 'runner_group_id' => 'integer', + 'client_secret' => 'encrypted', + 'webhook_secret' => 'encrypted', ]; protected $hidden = [ diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index 727ec77cd1..0f6de87930 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -51,6 +51,7 @@ class GitlabApp extends BaseModel 'access_token' => 'encrypted', 'refresh_token' => 'encrypted', 'client_secret' => 'encrypted', + 'app_secret' => 'encrypted', 'is_system_wide' => 'boolean', 'is_public' => 'boolean', ]; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 25b430b851..8a9c599331 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -200,6 +200,7 @@ class ServerSetting extends Model 'is_geoip_enabled' => 'boolean', 'geoip_refresh_days' => 'integer', 'geoip_maxmind_license_key' => 'encrypted', + 'logdrain_custom_config' => 'encrypted', 'traffic_ip_mode' => TrafficIpMode::class, 'docker_version_checked_at' => 'datetime', 'compose_version_checked_at' => 'datetime', diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index be5c464e0d..90d39c4f8e 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -80,6 +80,7 @@ class StandaloneMariadb extends BaseModel 'health_check_retries' => 'integer', 'health_check_start_period' => 'integer', 'mariadb_password' => 'encrypted', + 'mariadb_root_password' => 'encrypted', 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 208b5e917a..a018bae7c2 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -74,6 +74,7 @@ class StandaloneMongodb extends BaseModel protected $casts = [ 'include_coolify_default_variables' => 'boolean', 'health_check_enabled' => 'boolean', + 'mongo_initdb_root_password' => 'encrypted', 'health_check_interval' => 'integer', 'health_check_timeout' => 'integer', 'health_check_retries' => 'integer', @@ -283,22 +284,6 @@ class StandaloneMongodb extends BaseModel return null; } - public function mongoInitdbRootPassword(): Attribute - { - return Attribute::make( - get: function ($value) { - try { - return decrypt($value); - } catch (\Throwable $th) { - $this->mongo_initdb_root_password = encrypt($value); - $this->save(); - - return $value; - } - } - ); - } - public function portsMappings(): Attribute { return Attribute::make( diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index 4258a82b8a..f2a906e915 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -15,6 +15,10 @@ class TeamInvitation extends Model 'via', ]; + protected $casts = [ + 'link' => 'encrypted', + ]; + /** * Set the email attribute to lowercase. */ diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 07853f139e..b4c70f2f79 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -157,7 +157,7 @@ class EmailChannel json_encode(app(SensitiveDataRedactor::class)->redactValue($emailSettings), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) )); - // Don't report expected errors (invalid keys, validation) to Sentry + // Don't report expected errors (invalid keys, validation) to error tracking if (in_array($statusCode, [403, 401, 400, 422], true)) { throw NonReportableException::fromException(new Exception($userMessage, $e->getCode(), $e)); } @@ -178,7 +178,7 @@ class EmailChannel } catch (\Throwable $e) { // Check if this is a Resend domain verification error on cloud instances if (isCloud() && str_contains($e->getMessage(), 'domain is not verified')) { - // Throw as NonReportableException so it won't go to Sentry + // Throw as NonReportableException so it won't go to error tracking throw NonReportableException::fromException($e); } throw $e; diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 00652f46b5..877d790266 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -253,8 +253,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool } /** - * Throws the remote command error. Handler::register() never sends RuntimeException to Sentry, - * so SSH key, DNS, and timeout failures on user servers are not reported. + * Throws the remote command error. */ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null): never { diff --git a/composer.json b/composer.json index 7e92f92cb3..29d24ed495 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,6 @@ "purplepixie/phpdns": "^2.4.0", "pusher/pusher-php-server": "^7.3.0", "resend/resend-laravel": "^1.6.0", - "sentry/sentry-laravel": "^4.28.0", "socialiteproviders/authentik": "^5.3.0", "socialiteproviders/clerk": "^5.1.0", "socialiteproviders/discord": "^4.2.0", diff --git a/composer.lock b/composer.lock index 43835e7622..02f3e6112f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "664589c32e86e8e6fc2a0fb17feab410", + "content-hash": "f97af22f1628cec49170f1d40c582a9d", "packages": [ { "name": "aws/aws-crt-php", @@ -1837,66 +1837,6 @@ ], "time": "2026-08-24T09:15:32+00:00" }, - { - "name": "jean85/pretty-package-versions", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.1.0", - "php": "^7.4|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^7.5|^8.5|^9.6", - "rector/rector": "^2.0", - "vimeo/psalm": "^4.3 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" - }, - "time": "2025-03-19T14:43:43+00:00" - }, { "name": "laravel/fortify", "version": "v1.40.0", @@ -4746,84 +4686,6 @@ ], "time": "2026-02-16T23:10:27+00:00" }, - { - "name": "nyholm/psr7", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/psr7.git", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0", - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "php-http/message-factory": "^1.0", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", - "symfony/error-handler": "^4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Nyholm\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.2" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2024-09-09T07:06:30+00:00" - }, { "name": "paragonie/constant_time_encoding", "version": "v3.1.3", @@ -7161,194 +7023,6 @@ }, "time": "2026-09-25T18:18:55+00:00" }, - { - "name": "sentry/sentry", - "version": "4.32.0", - "source": { - "type": "git", - "url": "https://github.com/getsentry/sentry-php.git", - "reference": "c345642bdc0b678965a35ab61fc6323808eb3d2c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/c345642bdc0b678965a35ab61fc6323808eb3d2c", - "reference": "c345642bdc0b678965a35ab61fc6323808eb3d2c", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "guzzlehttp/psr7": "^1.8.4|^2.1.1|^3.0.0", - "jean85/pretty-package-versions": "^1.5|^2.0.4", - "php": "^7.2|^8.0", - "psr/log": "^1.0|^2.0|^3.0", - "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0|^8.0" - }, - "conflict": { - "raven/raven": "*" - }, - "require-dev": { - "carthage-software/mago": "1.30.0", - "friendsofphp/php-cs-fixer": "^3.4", - "guzzlehttp/guzzle": "^7.0|^8.0", - "guzzlehttp/promises": "^2.0.3|^3.0.0", - "monolog/monolog": "^1.6|^2.0|^3.0", - "nyholm/psr7": "^1.8", - "open-telemetry/api": "^1.1", - "open-telemetry/context": "^1.1", - "open-telemetry/exporter-otlp": "^1.1", - "open-telemetry/sdk": "^1.1", - "open-telemetry/sem-conv": "^1.27", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^8.5.52|^9.6.34", - "spiral/roadrunner-http": "^3.6", - "spiral/roadrunner-worker": "^3.6" - }, - "suggest": { - "ext-excimer": "Enable Sentry profiling with the Excimer PHP extension.", - "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Sentry\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sentry", - "email": "accounts@sentry.io" - } - ], - "description": "PHP SDK for Sentry (http://sentry.io)", - "homepage": "http://sentry.io", - "keywords": [ - "crash-reporting", - "crash-reports", - "error-handler", - "error-monitoring", - "log", - "logging", - "profiling", - "sentry", - "tracing" - ], - "support": { - "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.32.0" - }, - "funding": [ - { - "url": "https://sentry.io/", - "type": "custom" - }, - { - "url": "https://sentry.io/pricing/", - "type": "custom" - } - ], - "time": "2026-09-24T13:32:44+00:00" - }, - { - "name": "sentry/sentry-laravel", - "version": "4.28.0", - "source": { - "type": "git", - "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "34bf18fbf99b884a9bfbb46957d0178d2d0b988e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/34bf18fbf99b884a9bfbb46957d0178d2d0b988e", - "reference": "34bf18fbf99b884a9bfbb46957d0178d2d0b988e", - "shasum": "" - }, - "require": { - "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0 | ^13.0", - "nyholm/psr7": "^1.0", - "php": "^7.2 | ^8.0", - "sentry/sentry": "^4.31.0", - "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.11", - "guzzlehttp/guzzle": "^7.2", - "laravel/ai": "^0.1.5", - "laravel/folio": "^1.1", - "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0 | ^13.0", - "laravel/octane": "^2.15", - "laravel/pennant": "^1.0", - "livewire/livewire": "^2.0 | ^3.0 | ^4.0", - "mockery/mockery": "^1.3", - "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5 | ^9.6 | ^10.4 | ^11.5" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Sentry": "Sentry\\Laravel\\Facade" - }, - "providers": [ - "Sentry\\Laravel\\ServiceProvider", - "Sentry\\Laravel\\Tracing\\ServiceProvider" - ] - } - }, - "autoload": { - "psr-0": { - "Sentry\\Laravel\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sentry", - "email": "accounts@sentry.io" - } - ], - "description": "Laravel SDK for Sentry (https://sentry.io)", - "homepage": "https://sentry.io", - "keywords": [ - "crash-reporting", - "crash-reports", - "error-handler", - "error-monitoring", - "laravel", - "log", - "logging", - "profiling", - "sentry", - "tracing" - ], - "support": { - "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.28.0" - }, - "funding": [ - { - "url": "https://sentry.io/", - "type": "custom" - }, - { - "url": "https://sentry.io/pricing/", - "type": "custom" - } - ], - "time": "2026-09-23T14:29:19+00:00" - }, { "name": "socialiteproviders/authentik", "version": "5.3.0", @@ -9853,77 +9527,6 @@ ], "time": "2026-09-04T10:45:44+00:00" }, - { - "name": "symfony/options-resolver", - "version": "v8.1.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "88f9c561f678a02d54b897014049fa839e33ff82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", - "reference": "88f9c561f678a02d54b897014049fa839e33ff82", - "shasum": "" - }, - "require": { - "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-29T05:06:50+00:00" - }, { "name": "symfony/polyfill-ctype", "version": "v1.37.0", @@ -11065,93 +10668,6 @@ ], "time": "2026-09-28T19:13:15+00:00" }, - { - "name": "symfony/psr-http-message-bridge", - "version": "v8.1.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "67fd34de15ded1763aa1e330fe345f080a94022c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/67fd34de15ded1763aa1e330fe345f080a94022c", - "reference": "67fd34de15ded1763aa1e330fe345f080a94022c", - "shasum": "" - }, - "require": { - "php": ">=8.4.1", - "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^7.4|^8.0" - }, - "conflict": { - "php-http/discovery": "<1.15" - }, - "require-dev": { - "nyholm/psr7": "^1.1", - "php-http/discovery": "^1.15", - "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^7.4|^8.0", - "symfony/config": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/runtime": "^7.4|^8.0" - }, - "type": "symfony-bridge", - "autoload": { - "psr-4": { - "Symfony\\Bridge\\PsrHttpMessage\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "PSR HTTP message bridge", - "homepage": "https://symfony.com", - "keywords": [ - "http", - "http-message", - "psr-17", - "psr-7" - ], - "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.1.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-29T05:06:50+00:00" - }, { "name": "symfony/routing", "version": "v7.4.20", @@ -14616,6 +14132,66 @@ }, "time": "2026-03-17T11:56:53+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "kelunik/certificate", "version": "v1.1.3", diff --git a/config/constants.php b/config/constants.php index 728ba9c8ee..3f696774d2 100644 --- a/config/constants.php +++ b/config/constants.php @@ -105,10 +105,6 @@ return [ 'verification_code_expiry_minutes' => 10, ], - 'sentry' => [ - 'sentry_dsn' => env('SENTRY_DSN'), - ], - 'sentinel' => [ 'dev_url' => env('DEV_SENTINEL_URL'), // How often (seconds) PushServerUpdateJob is force-dispatched even when diff --git a/config/sentry.php b/config/sentry.php deleted file mode 100644 index 0efb4a0e2b..0000000000 --- a/config/sentry.php +++ /dev/null @@ -1,85 +0,0 @@ - config('constants.sentry.sentry_dsn'), - - // The release version of your application - // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => config('constants.coolify.version'), - - // When left empty or `null` the Laravel environment will be used - 'environment' => config('app.env'), - - 'breadcrumbs' => [ - // Capture Laravel logs in breadcrumbs - 'logs' => true, - - // Capture Laravel cache events in breadcrumbs - 'cache' => true, - - // Capture Livewire components in breadcrumbs - 'livewire' => true, - - // Capture SQL queries in breadcrumbs - 'sql_queries' => true, - - // Capture bindings on SQL queries logged in breadcrumbs - 'sql_bindings' => true, - - // Capture queue job information in breadcrumbs - 'queue_info' => true, - - // Capture command information in breadcrumbs - 'command_info' => true, - - // Capture HTTP client requests information in breadcrumbs - 'http_client_requests' => true, - ], - - 'tracing' => [ - // Trace queue jobs as their own transactions - 'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false), - - // Capture queue jobs as spans when executed on the sync driver - 'queue_jobs' => true, - - // Capture SQL queries as spans - 'sql_queries' => true, - - // Try to find out where the SQL query originated from and add it to the query spans - 'sql_origin' => true, - - // Capture views as spans - 'views' => true, - - // Capture Livewire components as spans - 'livewire' => true, - - // Capture HTTP client requests as spans - 'http_client_requests' => true, - - // Capture Redis operations as spans (this enables Redis events in Laravel) - 'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false), - - // Try to find out where the Redis command originated from and add it to the command spans - 'redis_origin' => true, - - // Indicates if the tracing integrations supplied by Sentry should be loaded - 'default_integrations' => true, - - // Indicates that requests without a matching route should be traced - 'missing_routes' => false, - ], - - // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii - 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), - - // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate - 'enable_tracing' => env('SENTRY_ENABLE_TRACING', false), - 'traces_sample_rate' => 0.2, - - 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'), - -]; diff --git a/database/migrations/2026_10_19_083521_encrypt_plaintext_secrets.php b/database/migrations/2026_10_19_083521_encrypt_plaintext_secrets.php new file mode 100644 index 0000000000..5de956657e --- /dev/null +++ b/database/migrations/2026_10_19_083521_encrypt_plaintext_secrets.php @@ -0,0 +1,88 @@ +text('app_secret')->nullable()->change(); + }); + + $this->encryptColumns('github_apps', ['client_secret', 'webhook_secret']); + $this->encryptColumns('gitlab_apps', ['app_secret']); + $this->encryptColumns('standalone_mariadbs', ['mariadb_root_password']); + // The old accessor stored encrypt($value), which serializes the value before encryption. + $this->encryptColumns('standalone_mongodbs', ['mongo_initdb_root_password'], legacySerialized: true); + $this->encryptColumns('server_settings', ['logdrain_custom_config']); + $this->encryptColumns('team_invitations', ['link']); + } + + /** + * @param array $columns + */ + private function encryptColumns(string $table, array $columns, bool $legacySerialized = false): void + { + try { + DB::table($table)->select(['id', ...$columns])->chunkById(100, function ($rows) use ($table, $columns, $legacySerialized) { + foreach ($rows as $row) { + $updates = []; + + foreach ($columns as $column) { + $value = $row->{$column}; + + if (blank($value)) { + continue; + } + + $plaintext = $this->plaintextToEncrypt($value, $legacySerialized); + + if ($plaintext !== null) { + $updates[$column] = Crypt::encryptString($plaintext); + } + } + + if ($updates !== []) { + DB::table($table)->where('id', $row->id)->update($updates); + } + } + }); + } catch (Exception $e) { + echo "Encrypting secrets in {$table} failed."; + echo $e->getMessage(); + } + } + + /** + * Return the plaintext that still needs encryption, or null when the value is already stored as encryptString() output. + */ + private function plaintextToEncrypt(string $value, bool $legacySerialized): ?string + { + try { + $decrypted = Crypt::decryptString($value); + } catch (DecryptException) { + return $value; + } + + if ($legacySerialized) { + $unserialized = @unserialize($decrypted, ['allowed_classes' => false]); + + if (is_string($unserialized) && serialize($unserialized) === $decrypted) { + return $unserialized; + } + } + + return null; + } +}; diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 2f8ea18e56..4ff572da81 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -191,7 +191,6 @@ @if (config('app.name') == 'Coolify Cloud') - @endif @auth diff --git a/tests/Feature/PlaintextSecretEncryptionTest.php b/tests/Feature/PlaintextSecretEncryptionTest.php new file mode 100644 index 0000000000..29edf2de47 --- /dev/null +++ b/tests/Feature/PlaintextSecretEncryptionTest.php @@ -0,0 +1,123 @@ +create(); + $server = Server::factory()->create(['team_id' => $team->id]); + $destination = $server->standaloneDockers()->firstOrFail(); + $environment = Environment::factory()->create([ + 'project_id' => Project::factory()->create(['team_id' => $team->id])->id, + ]); + $databaseAttributes = [ + 'destination_type' => $destination->getMorphClass(), + 'destination_id' => $destination->id, + 'environment_id' => $environment->id, + ]; + $gitAppAttributes = [ + 'html_url' => 'https://example.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'team_id' => $team->id, + 'is_system_wide' => false, + ]; + + $githubApp = GithubApp::create([ + ...$gitAppAttributes, + 'name' => 'github', + 'api_url' => 'https://api.github.com', + 'client_secret' => 'github-client-secret', + 'webhook_secret' => 'github-webhook-secret', + ]); + $gitlabApp = GitlabApp::create([ + ...$gitAppAttributes, + 'name' => 'gitlab', + 'api_url' => 'https://gitlab.com/api/v4', + 'app_secret' => str_repeat('g', 64), + ]); + $mariadb = StandaloneMariadb::create([ + ...$databaseAttributes, + 'name' => 'mariadb', + 'mariadb_root_password' => 'mariadb-root-password', + 'mariadb_password' => 'mariadb-password', + ]); + $mongodb = StandaloneMongodb::create([ + ...$databaseAttributes, + 'name' => 'mongodb', + 'mongo_initdb_root_password' => 'mongo-root-password', + ]); + $server->settings->update(['logdrain_custom_config' => "[OUTPUT]\n Name http\n http_Passwd secret"]); + $invitation = TeamInvitation::create([ + 'team_id' => $team->id, + 'uuid' => 'invite-uuid', + 'email' => 'new@example.com', + 'role' => 'member', + 'link' => 'https://coolify.test/auth/link?token=secret-token', + 'via' => 'link', + ]); + + $this->secrets = [ + ['github_apps', $githubApp, 'client_secret', 'github-client-secret'], + ['github_apps', $githubApp, 'webhook_secret', 'github-webhook-secret'], + ['gitlab_apps', $gitlabApp, 'app_secret', str_repeat('g', 64)], + ['standalone_mariadbs', $mariadb, 'mariadb_root_password', 'mariadb-root-password'], + ['standalone_mongodbs', $mongodb, 'mongo_initdb_root_password', 'mongo-root-password'], + ['server_settings', $server->settings, 'logdrain_custom_config', "[OUTPUT]\n Name http\n http_Passwd secret"], + ['team_invitations', $invitation, 'link', 'https://coolify.test/auth/link?token=secret-token'], + ]; +}); + +function rawSecret(string $table, $model, string $column): ?string +{ + return DB::table($table)->where('id', $model->id)->value($column); +} + +it('encrypts secrets at rest when they are saved through the model', function () { + foreach ($this->secrets as [$table, $model, $column, $plaintext]) { + expect(Crypt::decryptString(rawSecret($table, $model, $column)))->toBe($plaintext, "{$table}.{$column}") + ->and($model->fresh()->{$column})->toBe($plaintext, "{$table}.{$column}"); + } +}); + +it('stores the GitLab app secret in a text column so the ciphertext fits', function () { + expect(Schema::getColumnType('gitlab_apps', 'app_secret'))->toBe('text'); +}); + +it('migrates plaintext and legacy serialized secrets without encrypting them twice', function () { + foreach ($this->secrets as [$table, $model, $column, $plaintext]) { + $legacyValue = $column === 'mongo_initdb_root_password' ? encrypt($plaintext) : $plaintext; + DB::table($table)->where('id', $model->id)->update([$column => $legacyValue]); + } + $emptyGithubApp = GithubApp::create([ + 'name' => 'empty', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'team_id' => $this->secrets[0][1]->team_id, + 'is_system_wide' => false, + ]); + + $migration = require database_path('migrations/2026_10_19_083521_encrypt_plaintext_secrets.php'); + $migration->up(); + $migration->up(); + + foreach ($this->secrets as [$table, $model, $column, $plaintext]) { + expect(Crypt::decryptString(rawSecret($table, $model, $column)))->toBe($plaintext, "{$table}.{$column}") + ->and($model->fresh()->{$column})->toBe($plaintext, "{$table}.{$column}"); + } + expect(rawSecret('github_apps', $emptyGithubApp, 'client_secret'))->toBeNull() + ->and(rawSecret('github_apps', $emptyGithubApp, 'webhook_secret'))->toBeNull(); +});