feat(ca-certificate): add CA certificate management functionality with UI integration and routing

This commit is contained in:
Andras Bacsai
2025-05-29 14:27:28 +02:00
parent 9d71900952
commit 261a2fe564
7 changed files with 223 additions and 181 deletions

View File

@@ -43,7 +43,7 @@ class Terminal extends Component
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->where('settings.is_terminal_enabled', true)->firstOrFail();
if ($isContainer) {
// Validate container identifier format (alphanumeric, dashes, and underscores only)

View File

@@ -2,12 +2,8 @@
namespace App\Livewire\Server;
use App\Helpers\SslHelper;
use App\Jobs\RegenerateSslCertJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\SslCertificate;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate;
@@ -17,14 +13,6 @@ class Advanced extends Component
{
public Server $server;
public ?SslCertificate $caCertificate = null;
public $showCertificate = false;
public $certificateContent = '';
public ?Carbon $certificateValidUntil = null;
public array $parameters = [];
#[Validate(['string'])]
@@ -48,27 +36,12 @@ class Advanced extends Component
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->parameters = get_route_parameters();
$this->syncData();
$this->loadCaCertificate();
} catch (\Throwable) {
return redirect()->route('server.index');
}
}
public function loadCaCertificate()
{
$this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first();
if ($this->caCertificate) {
$this->certificateContent = $this->caCertificate->ssl_certificate;
$this->certificateValidUntil = $this->caCertificate->valid_until;
}
}
public function toggleCertificate()
{
$this->showCertificate = ! $this->showCertificate;
}
public function toggleTerminal($password)
{
try {
@@ -100,78 +73,6 @@ class Advanced extends Component
}
}
public function saveCaCertificate()
{
try {
if (! $this->certificateContent) {
throw new \Exception('Certificate content cannot be empty.');
}
if (! openssl_x509_read($this->certificateContent)) {
throw new \Exception('Invalid certificate format.');
}
if ($this->caCertificate) {
$this->caCertificate->ssl_certificate = $this->certificateContent;
$this->caCertificate->save();
$this->loadCaCertificate();
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
server_id: $this->server->id,
force_regeneration: true
));
}
$this->dispatch('success', 'CA Certificate saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function regenerateCaCertificate()
{
try {
SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
serverId: $this->server->id,
isCaCertificate: true,
validityDays: 10 * 365
);
$this->loadCaCertificate();
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
server_id: $this->server->id,
force_regeneration: true
));
$this->loadCaCertificate();
$this->dispatch('success', 'CA Certificate regenerated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function writeCertificateToServer()
{
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
remote_process($commands, $this->server);
}
public function syncData(bool $toModel = false)
{
if ($toModel) {

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Livewire\Server\CaCertificate;
use App\Helpers\SslHelper;
use App\Jobs\RegenerateSslCertJob;
use App\Models\Server;
use App\Models\SslCertificate;
use Illuminate\Support\Carbon;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Show extends Component
{
#[Locked]
public Server $server;
public ?SslCertificate $caCertificate = null;
public $showCertificate = false;
public $certificateContent = '';
public ?Carbon $certificateValidUntil = null;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->loadCaCertificate();
} catch (\Throwable $e) {
return redirect()->route('server.index');
}
}
public function loadCaCertificate()
{
$this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first();
if ($this->caCertificate) {
$this->certificateContent = $this->caCertificate->ssl_certificate;
$this->certificateValidUntil = $this->caCertificate->valid_until;
}
}
public function toggleCertificate()
{
$this->showCertificate = ! $this->showCertificate;
}
public function saveCaCertificate()
{
try {
if (! $this->certificateContent) {
throw new \Exception('Certificate content cannot be empty.');
}
if (! openssl_x509_read($this->certificateContent)) {
throw new \Exception('Invalid certificate format.');
}
if ($this->caCertificate) {
$this->caCertificate->ssl_certificate = $this->certificateContent;
$this->caCertificate->save();
$this->loadCaCertificate();
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
server_id: $this->server->id,
force_regeneration: true
));
}
$this->dispatch('success', 'CA Certificate saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function regenerateCaCertificate()
{
try {
SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
serverId: $this->server->id,
isCaCertificate: true,
validityDays: 10 * 365
);
$this->loadCaCertificate();
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
server_id: $this->server->id,
force_regeneration: true
));
$this->loadCaCertificate();
$this->dispatch('success', 'CA Certificate regenerated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function writeCertificateToServer()
{
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
remote_process($commands, $this->server);
}
public function render()
{
return view('livewire.server.ca-certificate.show');
}
}

View File

@@ -9,6 +9,9 @@
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
</a>
<a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}"
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate
</a>
@if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare

View File

@@ -69,7 +69,6 @@
<div class="flex flex-col">
<h3>Builds</h3>
<div>Customize the build process.</div>
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4">
<x-forms.input id="concurrentBuilds" label="Number of concurrent builds" required
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
@@ -78,85 +77,6 @@
</div>
</div>
</div>
<div class="flex flex-col gap-4 pt-8">
<h3>CA SSL Certificate</h3>
<div class="flex gap-2">
<x-modal-confirmation title="Confirm changing of CA Certificate?" buttonTitle="Save Certificate"
submitAction="saveCaCertificate" :actions="[
'This will overwrite the existing CA certificate at /data/coolify/ssl/coolify-ca.crt with your custom CA certificate.',
'This will regenerate all SSL certificates for databases on this server and it will sign them with your custom CA.',
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with your new CA certificate.',
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
]"
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
step3ButtonText="Save Certificate">
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Regenerate Certificate?" buttonTitle="Regenerate Certificate"
submitAction="regenerateCaCertificate" :actions="[
'This will generate a new CA certificate at /data/coolify/ssl/coolify-ca.crt and replace the existing one.',
'This will regenerate all SSL certificates for databases on this server and it will sign them with the new CA certificate.',
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with the new CA certificate.',
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
]"
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
step3ButtonText="Regenerate Certificate">
</x-modal-confirmation>
</div>
<div class="space-y-4">
<div class="text-sm">
<p class="font-medium mb-2">Recommended Configuration:</p>
<ul class="list-disc pl-5 space-y-1">
<li>Mount this CA certificate of Coolify into all containers that need to connect to one of
your databases over SSL. You can see and copy the bind mount below.</li>
<li>Read more when and why this is needed <a class="underline"
href="https://coolify.io/docs/databases/ssl" target="_blank">here</a>.</li>
</ul>
</div>
<div class="relative">
<x-forms.copy-button
text="- /data/coolify/ssl/coolify-ca.crt:/etc/ssl/certs/coolify-ca.crt:ro" />
</div>
</div>
<div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-sm">CA Certificate</span>
@if ($certificateValidUntil)
<span class="text-sm">(Valid until:
@if (now()->gt($certificateValidUntil))
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} -
Expired)</span>
@elseif(now()->addDays(30)->gt($certificateValidUntil))
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} -
Expiring soon)</span>
@else
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }})</span>
@endif
</span>
@endif
</div>
<x-forms.button wire:click="toggleCertificate" type="button" class="py-1! px-2! text-sm">
{{ $showCertificate ? 'Hide' : 'Show' }}
</x-forms.button>
</div>
@if ($showCertificate)
<textarea class="w-full h-[370px] input" wire:model="certificateContent"
placeholder="Paste or edit CA certificate content here..."></textarea>
@else
<div class="w-full h-[370px] input">
<div class="h-full flex flex-col items-center justify-center text-gray-300">
<div class="mb-2">
━━━━━━━━ CERTIFICATE CONTENT ━━━━━━━━
</div>
<div class="text-sm">
Click "Show" to view or edit
</div>
</div>
</div>
@endif
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,88 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > CA Certificate | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="ca-certificate" />
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<h3>CA SSL Certificate</h3>
<div class="flex gap-2">
<x-modal-confirmation title="Confirm changing of CA Certificate?" buttonTitle="Save"
submitAction="saveCaCertificate" :actions="[
'This will overwrite the existing CA certificate at /data/coolify/ssl/coolify-ca.crt with your custom CA certificate.',
'This will regenerate all SSL certificates for databases on this server and it will sign them with your custom CA.',
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with your new CA certificate.',
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
]"
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
step3ButtonText="Save Certificate">
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Regenerate Certificate?" buttonTitle="Regenerate "
submitAction="regenerateCaCertificate" :actions="[
'This will generate a new CA certificate at /data/coolify/ssl/coolify-ca.crt and replace the existing one.',
'This will regenerate all SSL certificates for databases on this server and it will sign them with the new CA certificate.',
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with the new CA certificate.',
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
]"
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
step3ButtonText="Regenerate Certificate">
</x-modal-confirmation>
</div>
</div>
<div class="space-y-4">
<div class="text-sm">
<p class="font-medium mb-2">Recommended Configuration:</p>
<ul class="list-disc pl-5 space-y-1">
<li>Mount this CA certificate of Coolify into all containers that need to connect to one of
your databases over SSL. You can see and copy the bind mount below.</li>
<li>Read more when and why this is needed <a class="underline dark:text-white"
href="https://coolify.io/docs/databases/ssl" target="_blank">here</a>.</li>
</ul>
</div>
<div class="relative">
<x-forms.copy-button text="- /data/coolify/ssl/coolify-ca.crt:/etc/ssl/certs/coolify-ca.crt:ro" />
</div>
</div>
<div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-sm">CA Certificate</span>
@if ($certificateValidUntil)
<span class="text-sm">(Valid until:
@if (now()->gt($certificateValidUntil))
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} -
Expired)</span>
@elseif(now()->addDays(30)->gt($certificateValidUntil))
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} -
Expiring soon)</span>
@else
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }})</span>
@endif
</span>
@endif
</div>
<x-forms.button wire:click="toggleCertificate" type="button" class="py-1! px-2! text-sm">
{{ $showCertificate ? 'Hide' : 'Show' }}
</x-forms.button>
</div>
@if ($showCertificate)
<textarea class="w-full h-[370px] input" wire:model="certificateContent"
placeholder="Paste or edit CA certificate content here..."></textarea>
@else
<div class="w-full h-[370px] input">
<div class="h-full flex flex-col items-center justify-center text-gray-300">
<div class="mb-2">
━━━━━━━━ CERTIFICATE CONTENT ━━━━━━━━
</div>
<div class="text-sm">
Click "Show" to view or edit
</div>
</div>
</div>
@endif
</div>
</div>
</div>
</div>

View File

@@ -37,6 +37,7 @@ use App\Livewire\Security\ApiTokens;
use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex;
use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
use App\Livewire\Server\Advanced as ServerAdvanced;
use App\Livewire\Server\CaCertificate\Show as CaCertificateShow;
use App\Livewire\Server\Charts as ServerCharts;
use App\Livewire\Server\CloudflareTunnels;
use App\Livewire\Server\Delete as DeleteServer;
@@ -242,6 +243,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/', ServerShow::class)->name('server.show');
Route::get('/advanced', ServerAdvanced::class)->name('server.advanced');
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate');
Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels');
Route::get('/destinations', ServerDestinations::class)->name('server.destinations');