mailwolt/app/Livewire/Ui/Security/TlsCiphersForm.php

135 lines
5.3 KiB
PHP

<?php
namespace App\Livewire\Ui\Security;
use App\Models\Setting;
use Illuminate\Support\Facades\Process;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.dvx')]
#[Title('TLS-Ciphers · Mailwolt')]
class TlsCiphersForm extends Component
{
public string $preset = 'intermediate';
public string $postfix_protocols = '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1';
public string $postfix_ciphers = 'medium';
public string $dovecot_min_proto = 'TLSv1.2';
public string $dovecot_ciphers = 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+AES256:!aNULL:!MD5:!DSS';
private const PRESETS = [
'modern' => [
'postfix_protocols' => '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2',
'postfix_ciphers' => 'high',
'dovecot_min_proto' => 'TLSv1.3',
'dovecot_ciphers' => 'ECDH+AESGCM:ECDH+CHACHA20:!aNULL:!MD5',
],
'intermediate' => [
'postfix_protocols' => '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1',
'postfix_ciphers' => 'medium',
'dovecot_min_proto' => 'TLSv1.2',
'dovecot_ciphers' => 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+AES256:!aNULL:!MD5:!DSS',
],
'old' => [
'postfix_protocols' => '!SSLv2, !SSLv3',
'postfix_ciphers' => 'low',
'dovecot_min_proto' => 'TLSv1',
'dovecot_ciphers' => 'HIGH:MEDIUM:!aNULL:!MD5',
],
];
public function mount(): void
{
$this->preset = Setting::get('tls.preset', 'intermediate');
$this->postfix_protocols = Setting::get('tls.postfix_protocols', self::PRESETS['intermediate']['postfix_protocols']);
$this->postfix_ciphers = Setting::get('tls.postfix_ciphers', self::PRESETS['intermediate']['postfix_ciphers']);
$this->dovecot_min_proto = Setting::get('tls.dovecot_min_proto', self::PRESETS['intermediate']['dovecot_min_proto']);
$this->dovecot_ciphers = Setting::get('tls.dovecot_ciphers', self::PRESETS['intermediate']['dovecot_ciphers']);
}
public function applyPreset(string $preset): void
{
if (!isset(self::PRESETS[$preset])) return;
$p = self::PRESETS[$preset];
$this->preset = $preset;
$this->postfix_protocols = $p['postfix_protocols'];
$this->postfix_ciphers = $p['postfix_ciphers'];
$this->dovecot_min_proto = $p['dovecot_min_proto'];
$this->dovecot_ciphers = $p['dovecot_ciphers'];
}
public function save(): void
{
$this->validate([
'postfix_protocols' => 'required|string|max:200',
'postfix_ciphers' => 'required|string|max:500',
'dovecot_min_proto' => 'required|string|max:50',
'dovecot_ciphers' => 'required|string|max:500',
]);
Setting::setMany([
'tls.preset' => $this->preset,
'tls.postfix_protocols' => $this->postfix_protocols,
'tls.postfix_ciphers' => $this->postfix_ciphers,
'tls.dovecot_min_proto' => $this->dovecot_min_proto,
'tls.dovecot_ciphers' => $this->dovecot_ciphers,
]);
try {
$this->writePostfixConfig();
$this->writeDovecotConfig();
Process::run(['sudo', '-n', '/usr/bin/systemctl', 'reload', 'postfix']);
Process::run(['sudo', '-n', '/usr/bin/systemctl', 'reload', 'dovecot']);
$this->dispatch('toast', type: 'done', badge: 'TLS',
title: 'TLS-Konfiguration übernommen',
text: 'Postfix und Dovecot wurden neu geladen.', duration: 5000);
} catch (\Throwable $e) {
$this->dispatch('toast', type: 'error', badge: 'TLS',
title: 'Fehler', text: $e->getMessage(), duration: 0);
}
}
private function writePostfixConfig(): void
{
$target = '/etc/postfix/mailwolt-tls.cf';
$content = "smtpd_tls_protocols = {$this->postfix_protocols}\n"
. "smtp_tls_protocols = {$this->postfix_protocols}\n"
. "smtpd_tls_ciphers = {$this->postfix_ciphers}\n"
. "smtp_tls_ciphers = {$this->postfix_ciphers}\n";
$this->tee($target, $content);
}
private function writeDovecotConfig(): void
{
$target = '/etc/dovecot/conf.d/99-mailwolt-tls.conf';
$content = "ssl_min_protocol = {$this->dovecot_min_proto}\n"
. "ssl_cipher_list = {$this->dovecot_ciphers}\n";
$this->tee($target, $content);
}
private function tee(string $target, string $content): void
{
$proc = proc_open(
sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target)),
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
if (!is_resource($proc)) throw new \RuntimeException('tee start fehlgeschlagen');
fwrite($pipes[0], $content);
fclose($pipes[0]);
stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
if (proc_close($proc) !== 0) throw new \RuntimeException("tee failed: {$target}");
}
public function render()
{
return view('livewire.ui.security.tls-ciphers-form', ['presets' => array_keys(self::PRESETS)]);
}
}