feat(ca-certificate): add CA certificate management functionality with UI integration and routing
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
128
app/Livewire/Server/CaCertificate/Show.php
Normal file
128
app/Livewire/Server/CaCertificate/Show.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user