Force SSL and Multi SSL (#456)

This commit is contained in:
Saeed Vaziry 2025-02-01 01:33:04 +01:00 committed by GitHub
parent ea396786e4
commit 262c5e040d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 161 additions and 73 deletions

View File

@ -0,0 +1,16 @@
<?php
namespace App\Actions\SSL;
use App\Models\Ssl;
class ActivateSSL
{
public function activate(Ssl $ssl): void
{
$ssl->site->ssls()->update(['is_active' => false]);
$ssl->is_active = true;
$ssl->save();
$ssl->site->webserver()->updateVHost($ssl->site);
}
}

View File

@ -31,6 +31,7 @@ public function create(Site $site, array $input): void
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
'email' => $input['email'] ?? null,
'is_active' => ! $site->activeSsl,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {

View File

@ -75,6 +75,7 @@ class Site extends AbstractModel
'port',
'progress',
'user',
'force_ssl',
];
protected $casts = [
@ -84,6 +85,7 @@ class Site extends AbstractModel
'progress' => 'integer',
'aliases' => 'array',
'source_control_id' => 'integer',
'force_ssl' => 'boolean',
];
public static array $statusColors = [
@ -227,6 +229,7 @@ public function activeSsl(): HasOne
return $this->hasOne(Ssl::class)
->where('expires_at', '>=', now())
->where('status', SslStatus::CREATED)
->where('is_active', true)
->orderByDesc('id');
}

View File

@ -17,10 +17,13 @@
* @property Carbon $expires_at
* @property string $status
* @property Site $site
* @property string $ca_path
* @property ?array $domains
* @property int $log_id
* @property string $email
* @property bool $is_active
* @property string $certificate_path
* @property string $pk_path
* @property string $ca_path
* @property ?ServerLog $log
*/
class Ssl extends AbstractModel
@ -38,6 +41,10 @@ class Ssl extends AbstractModel
'domains',
'log_id',
'email',
'is_active',
'certificate_path',
'pk_path',
'ca_path',
];
protected $casts = [
@ -48,6 +55,7 @@ class Ssl extends AbstractModel
'expires_at' => 'datetime',
'domains' => 'array',
'log_id' => 'integer',
'is_active' => 'boolean',
];
public static array $statusColors = [
@ -62,58 +70,6 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class);
}
public function getCertsDirectoryPath(): ?string
{
if ($this->type == 'letsencrypt') {
return '/etc/letsencrypt/live/'.$this->site->domain;
}
if ($this->type == 'custom') {
return '/etc/ssl/'.$this->site->domain;
}
return '';
}
public function getCertificatePath(): ?string
{
if ($this->type == 'letsencrypt') {
return $this->certificate;
}
if ($this->type == 'custom') {
return $this->getCertsDirectoryPath().'/cert.pem';
}
return '';
}
public function getPkPath(): ?string
{
if ($this->type == 'letsencrypt') {
return $this->pk;
}
if ($this->type == 'custom') {
return $this->getCertsDirectoryPath().'/privkey.pem';
}
return '';
}
public function getCaPath(): ?string
{
if ($this->type == 'letsencrypt') {
return $this->ca;
}
if ($this->type == 'custom') {
return $this->getCertsDirectoryPath().'/fullchain.pem';
}
return '';
}
public function validateSetup(string $result): bool
{
if (! Str::contains($result, 'Successfully received certificate')) {
@ -121,8 +77,8 @@ public function validateSetup(string $result): bool
}
if ($this->type == 'letsencrypt') {
$this->certificate = $this->getCertsDirectoryPath().'/fullchain.pem';
$this->pk = $this->getCertsDirectoryPath().'/privkey.pem';
$this->certificate_path = '/etc/letsencrypt/live/'.$this->id.'/fullchain.pem';
$this->pk_path = '/etc/letsencrypt/live/'.$this->id.'/privkey.pem';
$this->save();
}
@ -145,13 +101,4 @@ public function log(): BelongsTo
{
return $this->belongsTo(ServerLog::class);
}
public function getEmailAttribute(?string $value): string
{
if ($value) {
return $value;
}
return $this->site->server->creator->email;
}
}

View File

@ -169,16 +169,19 @@ public function setupSSL(Ssl $ssl): void
}
$command = view('ssh.services.webserver.nginx.create-letsencrypt-ssl', [
'email' => $ssl->email,
'domain' => $ssl->site->domain,
'name' => $ssl->id,
'domains' => $domains,
]);
if ($ssl->type == 'custom') {
$ssl->certificate_path = '/etc/ssl/'.$ssl->id.'/cert.pem';
$ssl->pk_path = '/etc/ssl/'.$ssl->id.'/privkey.pem';
$ssl->save();
$command = view('ssh.services.webserver.nginx.create-custom-ssl', [
'path' => $ssl->getCertsDirectoryPath(),
'path' => dirname($ssl->certificate_path),
'certificate' => $ssl->certificate,
'pk' => $ssl->pk,
'certificatePath' => $ssl->getCertificatePath(),
'pkPath' => $ssl->getPkPath(),
'certificatePath' => $ssl->certificate_path,
'pkPath' => $ssl->pk_path,
]);
}
$result = $this->service->server->ssh()->setLog($ssl->log)->exec(
@ -197,7 +200,7 @@ public function setupSSL(Ssl $ssl): void
public function removeSSL(Ssl $ssl): void
{
$this->service->server->ssh()->exec(
'sudo rm -rf '.$ssl->getCertsDirectoryPath().'*',
'sudo rm -rf '.dirname($ssl->certificate_path).'*',
'remove-ssl',
$ssl->site_id
);

View File

@ -15,6 +15,7 @@
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
@ -44,6 +45,24 @@ protected function getHeaderActions(): array
->color('gray')
->url('https://vitodeploy.com/sites/ssl')
->openUrlInNewTab(),
Action::make('force-ssl')
->label('Force SSL')
->tooltip(fn () => $this->site->force_ssl ? 'Disable force SSL' : 'Enable force SSL')
->icon(fn () => $this->site->force_ssl ? 'icon-force-ssl-enabled' : 'icon-force-ssl-disabled')
->requiresConfirmation()
->modalSubmitActionLabel(fn () => $this->site->force_ssl ? 'Disable' : 'Enable')
->action(function () {
$this->site->update([
'force_ssl' => ! $this->site->force_ssl,
]);
$this->site->webserver()->updateVHost($this->site);
Notification::make()
->success()
->title('SSL status has been updated.')
->send();
$this->dispatch('$refresh');
})
->color('gray'),
CreateAction::make('create')
->label('New Certificate')
->icon('heroicon-o-lock-closed')

View File

@ -2,11 +2,14 @@
namespace App\Web\Pages\Servers\Sites\Pages\SSL\Widgets;
use App\Actions\SSL\ActivateSSL;
use App\Actions\SSL\DeleteSSL;
use App\Models\Site;
use App\Models\Ssl;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -27,6 +30,9 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
IconColumn::make('is_active')
->color(fn (Ssl $record) => $record->is_active ? 'green' : 'gray')
->icon(fn (Ssl $record) => $record->is_active ? 'heroicon-o-lock-closed' : 'heroicon-o-lock-open'),
TextColumn::make('type')
->searchable()
->sortable(),
@ -52,6 +58,25 @@ public function table(Table $table): Table
->query($this->getTableQuery())
->columns($this->getTableColumns())
->actions([
Action::make('activate-ssl')
->hiddenLabel()
->visible(fn (Ssl $record) => ! $record->is_active)
->tooltip('Activate SSL')
->icon('heroicon-o-lock-closed')
->authorize(fn (Ssl $record) => auth()->user()->can('update', [$record->site, $this->site->server]))
->requiresConfirmation()
->modalHeading('Activate SSL')
->modalSubmitActionLabel('Activate')
->action(function (Ssl $record) {
run_action($this, function () use ($record) {
app(ActivateSSL::class)->activate($record);
Notification::make()
->success()
->title('SSL has been activated.')
->send();
});
}),
Action::make('logs')
->hiddenLabel()
->tooltip('Logs')

View File

@ -82,7 +82,9 @@ public function getHeaderActions(): array
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
$actions[] = $this->deployAction();
$actionsGroup[] = $this->autoDeploymentAction();
if ($this->site->sourceControl) {
$actionsGroup[] = $this->autoDeploymentAction();
}
$actionsGroup[] = $this->deploymentScriptAction();
}

View File

@ -0,0 +1,54 @@
<?php
use App\Enums\SslType;
use App\Models\Site;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('ssls', function (Blueprint $table) {
$table->boolean('is_active')->default(false);
$table->string('certificate_path')->nullable();
$table->string('pk_path')->nullable();
$table->string('ca_path')->nullable();
});
Site::query()->chunk(100, function ($sites) {
foreach ($sites as $site) {
foreach ($site->ssls as $ssl) {
if ($ssl->type === SslType::LETSENCRYPT) {
$ssl->certificate_path = $ssl->certificate;
$ssl->pk_path = $ssl->pk;
$ssl->ca_path = $ssl->ca;
$ssl->certificate = null;
$ssl->pk = null;
$ssl->ca = null;
}
if ($ssl->type === SslType::CUSTOM) {
$ssl->certificate_path = '/etc/ssl/'.$ssl->site->domain.'/cert.pem';
$ssl->pk_path = '/etc/ssl/'.$ssl->site->domain.'/privkey.pem';
$ssl->ca_path = '/etc/ssl/'.$ssl->site->domain.'/fullchain.pem';
}
$ssl->save();
}
$activeSSL = $site->ssls()->where('expires_at', '>=', now())->latest()->first();
if ($activeSSL) {
$activeSSL->update(['is_active' => true]);
}
}
});
}
public function down(): void
{
Schema::table('ssls', function (Blueprint $table) {
$table->dropColumn('is_active');
$table->dropColumn('certificate_path');
$table->dropColumn('pk_path');
$table->dropColumn('ca_path');
});
}
};

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#16a34a">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -1,3 +1,3 @@
if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name {{ $domain }} -m {{ $email }} {{ $domains }} --verbose; then
if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name {{ $name }} -m {{ $email }} {{ $domains }} --verbose; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -33,8 +33,8 @@
@endif
@if ($site->activeSsl)
listen 443 ssl;
ssl_certificate {{ $site->activeSsl->getCertificatePath() }};
ssl_certificate_key {{ $site->activeSsl->getPkPath() }};
ssl_certificate {{ $site->activeSsl->certificate_path }};
ssl_certificate_key {{ $site->activeSsl->pk_path }};
@endif
server_name {{ $site->domain }} {{ $site->getAliasesString() }};

View File

@ -48,12 +48,17 @@ public function test_letsencrypt_ssl()
])
->assertSuccessful();
$ssl = Ssl::query()->where('site_id', $this->site->id)->first();
$this->assertNotEmpty($ssl);
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode([$this->site->domain]),
'email' => 'ssl@example.com',
'certificate_path' => '/etc/letsencrypt/live/'.$ssl->id.'/fullchain.pem',
'pk_path' => '/etc/letsencrypt/live/'.$ssl->id.'/privkey.pem',
]);
}
@ -101,10 +106,16 @@ public function test_custom_ssl()
])
->assertSuccessful();
$ssl = Ssl::query()->where('site_id', $this->site->id)->first();
$this->assertNotEmpty($ssl);
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::CUSTOM,
'status' => SslStatus::CREATED,
'domains' => json_encode([$this->site->domain]),
'certificate_path' => '/etc/ssl/'.$ssl->id.'/cert.pem',
'pk_path' => '/etc/ssl/'.$ssl->id.'/privkey.pem',
]);
}