diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 299b07385..dd6f2d735 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -2,6 +2,8 @@ namespace App\Actions\Database; +use App\Helpers\SslHelper; +use App\Models\SslCertificate; use App\Models\StandaloneMariadb; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -16,6 +18,8 @@ class StartMariadb public string $configuration_dir; + private ?SslCertificate $ssl_certificate = null; + public function handle(StandaloneMariadb $database) { $this->database = $database; @@ -25,9 +29,57 @@ class StartMariadb $this->commands = [ "echo 'Starting database.'", + "echo 'Creating directories.'", "mkdir -p $this->configuration_dir", + "echo 'Directories created successfully.'", ]; + if (! $this->database->enable_ssl) { + $this->commands[] = "rm -rf $this->configuration_dir/ssl"; + + SslCertificate::where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->delete(); + + $this->database->fileStorages() + ->where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->get() + ->filter(function ($storage) { + return in_array($storage->mount_path, [ + '/etc/mysql/certs/server.crt', + '/etc/mysql/certs/server.key', + ]); + }) + ->each(function ($storage) { + $storage->delete(); + }); + } else { + $this->commands[] = "echo 'Setting up SSL for this database.'"; + $this->commands[] = "rm -rf $this->configuration_dir/ssl"; + $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; + $server = $this->database->destination->server; + $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail(); + + $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->first(); + + if (! $this->ssl_certificate) { + $this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'"; + $this->ssl_certificate = SslHelper::generateSslCertificate( + commonName: $this->database->uuid, + resourceType: $this->database->getMorphClass(), + resourceId: $this->database->id, + serverId: $server->id, + caCert: $caCert->ssl_certificate, + caKey: $caCert->ssl_private_key, + configurationDir: $this->configuration_dir, + mountPath: '/etc/mysql/certs', + ); + } + } + $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->database->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); @@ -67,38 +119,64 @@ class StartMariadb ], ], ]; + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } + if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } + if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; } - if (count($persistent_storages) > 0) { - $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; - } - if (count($persistent_file_volumes) > 0) { - $docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) { - return "$item->fs_path:$item->mount_path"; - })->toArray(); - } + if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } + + $docker_compose['services'][$container_name]['volumes'] ??= []; + + if (count($persistent_storages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = array_merge( + $docker_compose['services'][$container_name]['volumes'], + $persistent_storages + ); + } + if (count($persistent_file_volumes) > 0) { + $docker_compose['services'][$container_name]['volumes'] = array_merge( + $docker_compose['services'][$container_name]['volumes'], + $persistent_file_volumes->map(function ($item) { + return "$item->fs_path:$item->mount_path"; + })->toArray() + ); + } if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) { - $docker_compose['services'][$container_name]['volumes'][] = [ - 'type' => 'bind', - 'source' => $this->configuration_dir.'/custom-config.cnf', - 'target' => '/etc/mysql/conf.d/custom-config.cnf', - 'read_only' => true, - ]; + $docker_compose['services'][$container_name]['volumes'] = array_merge( + $docker_compose['services'][$container_name]['volumes'], + [ + [ + 'type' => 'bind', + 'source' => $this->configuration_dir.'/custom-config.cnf', + 'target' => '/etc/mysql/conf.d/custom-config.cnf', + 'read_only' => true, + ], + ] + ); } // Add custom docker run options $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + if ($this->database->enable_ssl) { + $docker_compose['services'][$container_name]['command'] = [ + 'mysqld', + '--ssl-cert=/etc/mysql/certs/server.crt', + '--ssl-key=/etc/mysql/certs/server.key', + '--require-secure-transport=1', + ]; + } $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); @@ -109,6 +187,9 @@ class StartMariadb $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + if ($this->database->enable_ssl) { + $this->commands[] = executeInDocker($this->database->uuid, 'chown mariadb:mariadb /etc/mysql/certs/server.crt /etc/mysql/certs/server.key'); + } return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index c9d473223..ac6e79140 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Mariadb; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Helpers\SslHelper; use App\Models\Server; +use App\Models\SslCertificate; use App\Models\StandaloneMariadb; use Exception; use Livewire\Component; @@ -21,6 +23,8 @@ class General extends Component public ?string $db_url_public = null; + public $certificateValidUntil = null; + protected $rules = [ 'database.name' => 'required', 'database.description' => 'nullable', @@ -35,6 +39,8 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', 'database.custom_docker_run_options' => 'nullable', + 'database.enable_ssl' => 'boolean', + 'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY', ]; protected $validationAttributes = [ @@ -50,6 +56,8 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', 'database.custom_docker_run_options' => 'Custom Docker Options', + 'database.enable_ssl' => 'Enable SSL', + 'database.ssl_mode' => 'SSL Mode', ]; public function mount() @@ -57,6 +65,14 @@ class General extends Component $this->db_url = $this->database->internal_db_url; $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); + + $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } } public function instantSaveAdvanced() @@ -127,6 +143,52 @@ class General extends Component } } + public function instantSaveSSL() + { + try { + $this->database->enable_ssl = $this->database->enable_ssl; + $this->database->ssl_mode = $this->database->ssl_mode; + $this->database->save(); + $this->dispatch('success', 'SSL configuration updated.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function regenerateSslCertificate() + { + try { + $server = $this->database->destination->server; + + $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->first(); + + if (! $existingCert) { + $this->dispatch('error', 'No existing SSL certificate found for this database.'); + + return; + } + + $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail(); + + SslHelper::generateSslCertificate( + commonName: $existingCert->common_name, + subjectAlternativeNames: $existingCert->subject_alternative_names ?? [], + resourceType: $existingCert->resource_type, + resourceId: $existingCert->resource_id, + serverId: $existingCert->server_id, + caCert: $caCert->ssl_certificate, + caKey: $caCert->ssl_private_key, + configurationDir: $existingCert->configuration_dir, + ); + + $this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 004ead4d9..3708c02a9 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -218,7 +218,17 @@ class StandaloneMariadb extends BaseModel protected function internalDbUrl(): Attribute { return new Attribute( - get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}", + get: function () { + $url = "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; + if ($this->enable_ssl) { + $url .= "?ssl-mode={$this->ssl_mode}"; + if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) { + $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt'; + } + } + + return $url; + }, ); } @@ -227,7 +237,15 @@ class StandaloneMariadb extends BaseModel return new Attribute( get: function () { if ($this->is_public && $this->public_port) { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + $url = "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + if ($this->enable_ssl) { + $url .= "?ssl-mode={$this->ssl_mode}"; + if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) { + $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt'; + } + } + + return $url; } return null; diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php index 4cad81749..673868667 100644 --- a/resources/views/livewire/project/database/mariadb/general.blade.php +++ b/resources/views/livewire/project/database/mariadb/general.blade.php @@ -65,6 +65,50 @@ type="password" readonly wire:model="db_url_public" /> @endif + +