diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 6c733d318..1e601e689 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -2,6 +2,8 @@ namespace App\Actions\Database; +use App\Helpers\SslHelper; +use App\Models\SslCertificate; use App\Models\StandaloneKeydb; use Illuminate\Support\Facades\Storage; use Lorisleiva\Actions\Concerns\AsAction; @@ -17,26 +19,77 @@ class StartKeydb public string $configuration_dir; + private ?SslCertificate $ssl_certificate = null; + public function handle(StandaloneKeydb $database) { $this->database = $database; - $startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes"; - $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; $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/keydb/certs/server.crt', + '/etc/keydb/certs/server.key', + ]); + }) + ->each(function ($storage) { + $storage->delete(); + }); + } else { + $this->commands[] = "echo 'Setting up SSL for this database.'"; + $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; + + $server = $this->database->destination->server; + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + + $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/keydb/certs', + ); + } + } + + $container_name = $this->database->uuid; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; + $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->database->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $environment_variables = $this->generate_environment_variables(); $this->add_custom_keydb(); + $startCommand = $this->buildStartCommand(); + $docker_compose = [ 'services' => [ $container_name => [ @@ -72,34 +125,67 @@ class StartKeydb ], ], ]; + 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; } + + $docker_compose['services'][$container_name]['volumes'] ??= []; + if (count($persistent_storages) > 0) { - $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; + $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'] = $persistent_file_volumes->map(function ($item) { - return "$item->fs_path:$item->mount_path"; - })->toArray(); + $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 (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } + if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) { - $docker_compose['services'][$container_name]['volumes'][] = [ - 'type' => 'bind', - 'source' => $this->configuration_dir.'/keydb.conf', - 'target' => '/etc/keydb/keydb.conf', - 'read_only' => true, - ]; - $docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes"; + $docker_compose['services'][$container_name]['volumes'] = array_merge( + $docker_compose['services'][$container_name]['volumes'] ?? [], + [ + [ + 'type' => 'bind', + 'source' => $this->configuration_dir.'/keydb.conf', + 'target' => '/etc/keydb/keydb.conf', + 'read_only' => true, + ], + ] + ); + } + + if ($this->database->enable_ssl) { + $docker_compose['services'][$container_name]['volumes'] = array_merge( + $docker_compose['services'][$container_name]['volumes'] ?? [], + [ + [ + 'type' => 'bind', + 'source' => '/data/coolify/ssl/coolify-ca.crt', + 'target' => '/etc/keydb/certs/coolify-ca.crt', + 'read_only' => true, + ], + ] + ); } // Add custom docker run options @@ -112,6 +198,9 @@ class StartKeydb $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + if ($this->database->enable_ssl) { + $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; + } $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; @@ -177,4 +266,34 @@ class StartKeydb instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server); Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}"); } + + private function buildStartCommand(): string + { + $hasKeydbConf = ! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf); + $keydbConfPath = '/etc/keydb/keydb.conf'; + + if ($hasKeydbConf) { + $confContent = $this->database->keydb_conf; + $hasRequirePass = str_contains($confContent, 'requirepass'); + + if ($hasRequirePass) { + $command = "keydb-server $keydbConfPath"; + } else { + $command = "keydb-server $keydbConfPath --requirepass {$this->database->keydb_password}"; + } + } else { + $command = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes"; + } + + if ($this->database->enable_ssl) { + $sslArgs = [ + '--tls-cert-file /etc/keydb/certs/server.crt', + '--tls-key-file /etc/keydb/certs/server.key', + '--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt', + ]; + $command .= ' '.implode(' ', $sslArgs); + } + + return $command; + } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index e768495eb..58db162a8 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Keydb; 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\StandaloneKeydb; use Exception; use Illuminate\Support\Facades\Auth; @@ -53,6 +55,11 @@ class General extends Component #[Validate(['nullable', 'boolean'])] public bool $isLogDrainEnabled = false; + public $certificateValidUntil = null; + + #[Validate(['nullable', 'boolean'])] + public bool $enable_ssl = false; + public function getListeners() { $teamId = Auth::user()->currentTeam()->id; @@ -67,6 +74,14 @@ class General extends Component try { $this->syncData(); $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; + } } catch (\Throwable $e) { return handleError($e, $this); } @@ -86,6 +101,7 @@ class General extends Component $this->database->public_port = $this->publicPort; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->enable_ssl = $this->enable_ssl; $this->database->save(); $this->dbUrl = $this->database->internal_db_url; @@ -101,6 +117,7 @@ class General extends Component $this->publicPort = $this->database->public_port; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->enable_ssl = $this->database->enable_ssl; $this->dbUrl = $this->database->internal_db_url; $this->dbUrlPublic = $this->database->external_db_url; } @@ -179,4 +196,50 @@ class General extends Component } } } + + public function instantSaveSSL() + { + try { + $this->syncData(true); + $this->dispatch('success', 'SSL configuration updated.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function regenerateSslCertificate() + { + try { + $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass()) + ->where('resource_id', $this->database->id) + ->where('server_id', $this->server->id) + ->first(); + + if (! $existingCert) { + $this->dispatch('error', 'No existing SSL certificate found for this database.'); + + return; + } + + $caCert = SslCertificate::where('server_id', $existingCert->server_id) + ->where('is_ca_certificate', true) + ->first(); + + SslHelper::generateSslCertificate( + commonName: $existingCert->commonName, + subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [], + 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, + mountPath: $existingCert->mount_path, + ); + + $this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.'); + } catch (Exception $e) { + handleError($e, $this); + } + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 3e80408ef..af95d58e5 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -223,7 +223,17 @@ class StandaloneKeydb extends BaseModel protected function internalDbUrl(): Attribute { return new Attribute( - get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0", + get: function () { + $scheme = $this->enable_ssl ? 'rediss' : 'redis'; + $port = $this->enable_ssl ? 6380 : 6379; + $url = "{$scheme}://:{$this->keydb_password}@{$this->uuid}:{$port}/0"; + + if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') { + $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt'; + } + + return $url; + } ); } @@ -232,7 +242,14 @@ class StandaloneKeydb extends BaseModel return new Attribute( get: function () { if ($this->is_public && $this->public_port) { - return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + $scheme = $this->enable_ssl ? 'rediss' : 'redis'; + $url = "{$scheme}://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + + if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') { + $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt'; + } + + return $url; } return null; diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index 362b7b363..8939bd00d 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -49,6 +49,40 @@ readonly value="Starting the database will generate this." /> @endif +
+
+
+

SSL Configuration

+ @if($database->enable_ssl && $certificateValidUntil) + + @endif +
+
+ @if($database->enable_ssl && $certificateValidUntil) + Valid until: + @if(now()->gt($certificateValidUntil)) + {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired + @elseif(now()->addDays(30)->gt($certificateValidUntil)) + {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon + @else + {{ $certificateValidUntil->format('d.m.Y H:i:s') }} + @endif + + @endif +
+ +
+