From 7fd76f3b32570d2dc72bce189f6c31c4ab66dd2c Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 17 Oct 2025 05:33:12 +0200 Subject: [PATCH] Anpassen der Tlsa Record erstellung --- app/Services/TlsaService.php | 77 ++++++++-- database/seeders/SystemDomainSeeder.php | 183 ++++++++++++++++++++++-- 2 files changed, 235 insertions(+), 25 deletions(-) diff --git a/app/Services/TlsaService.php b/app/Services/TlsaService.php index 2afeb64..fa5645e 100644 --- a/app/Services/TlsaService.php +++ b/app/Services/TlsaService.php @@ -7,17 +7,14 @@ use App\Models\TlsaRecord; class TlsaService { - public function resolveMxHost(): string - { - $base = env('BASE_DOMAIN', 'example.com'); - $sub = env('MTA_SUB', 'mx') ?: 'mx'; - return "{$sub}.{$base}"; - } - public function computeHashFromCert(string $host): ?string { $certPath = "/etc/letsencrypt/live/{$host}/fullchain.pem"; - if (!is_file($certPath)) return null; + if (!is_file($certPath)) { + // optional: Log für Debug + logger()->warning("TLSA: Zertifikat nicht gefunden", ['path' => $certPath]); + return null; + } $cmd = "openssl x509 -in ".escapeshellarg($certPath)." -noout -pubkey" . " | openssl pkey -pubin -outform DER" @@ -28,12 +25,12 @@ class TlsaService } /** - * Schreibt/aktualisiert TLSA (3 1 1) für den MX-Host in DB - * und legt zusätzlich /etc/mailwolt/dns/.tlsa.txt ab. + * TLSA (3 1 1) für den MX-Host der übergebenen Domain erzeugen/aktualisieren. + * Host kommt DIREKT aus $serverDomain->domain, nicht aus ENV. */ public function refreshForServerDomain(Domain $serverDomain, string $service = '_25._tcp'): ?TlsaRecord { - $host = $this->resolveMxHost(); + $host = $serverDomain->domain; // <— wichtig: Domain-Objekt statt ENV $hash = $this->computeHashFromCert($host); if (!$hash) { return null; // Zert (noch) nicht vorhanden @@ -52,13 +49,65 @@ class TlsaService ] ); - // Optional: TXT-Datei für Export/Debug + // optional: Datei für Export/Debug @mkdir('/etc/mailwolt/dns', 0755, true); - $line = sprintf('%s.%s IN TLSA %d %d %d %s', $service, $host, 3, 1, 1, $hash); - @file_put_contents("/etc/mailwolt/dns/{$host}.tlsa.txt", $line."\n"); + @file_put_contents("/etc/mailwolt/dns/{$host}.tlsa.txt", sprintf('%s.%s IN TLSA 3 1 1 %s', $service, $host, $hash)."\n"); return $rec; } + +// public function resolveMxHost(): string +// { +// $base = env('BASE_DOMAIN', 'example.com'); +// $sub = env('MTA_SUB', 'mx') ?: 'mx'; +// return "{$sub}.{$base}"; +// } +// +// public function computeHashFromCert(string $host): ?string +// { +// $certPath = "/etc/letsencrypt/live/{$host}/fullchain.pem"; +// if (!is_file($certPath)) return null; +// +// $cmd = "openssl x509 -in ".escapeshellarg($certPath)." -noout -pubkey" +// . " | openssl pkey -pubin -outform DER" +// . " | openssl dgst -sha256"; +// $out = shell_exec($cmd.' 2>/dev/null') ?? ''; +// $hash = preg_replace('/^SHA256\(stdin\)=\s*/', '', trim($out)); +// return $hash !== '' ? $hash : null; +// } +// +// /** +// * Schreibt/aktualisiert TLSA (3 1 1) für den MX-Host in DB +// * und legt zusätzlich /etc/mailwolt/dns/.tlsa.txt ab. +// */ +// public function refreshForServerDomain(Domain $serverDomain, string $service = '_25._tcp'): ?TlsaRecord +// { +// $host = $this->resolveMxHost(); +// $hash = $this->computeHashFromCert($host); +// if (!$hash) { +// return null; // Zert (noch) nicht vorhanden +// } +// +// $certPath = "/etc/letsencrypt/live/{$host}/fullchain.pem"; +// +// $rec = TlsaRecord::updateOrCreate( +// ['domain_id' => $serverDomain->id, 'host' => $host, 'service' => $service], +// [ +// 'usage' => 3, // DANE-EE +// 'selector' => 1, // SPKI +// 'matching' => 1, // SHA-256 +// 'hash' => $hash, +// 'cert_path' => $certPath, +// ] +// ); +// +// // Optional: TXT-Datei für Export/Debug +// @mkdir('/etc/mailwolt/dns', 0755, true); +// $line = sprintf('%s.%s IN TLSA %d %d %d %s', $service, $host, 3, 1, 1, $hash); +// @file_put_contents("/etc/mailwolt/dns/{$host}.tlsa.txt", $line."\n"); +// +// return $rec; +// } } diff --git a/database/seeders/SystemDomainSeeder.php b/database/seeders/SystemDomainSeeder.php index 3fe03b8..b93ab58 100644 --- a/database/seeders/SystemDomainSeeder.php +++ b/database/seeders/SystemDomainSeeder.php @@ -1,14 +1,11 @@ command->warn("BASE_DOMAIN ist 'example.com' – Seeder überspringt produktive Einträge."); return; } - $mtaSub = env('MTA_SUB', 'mx') ?: 'mx'; // z.B. mx - $serverFqdn = "{$mtaSub}.{$platformBase}"; // z.B. mx.nexlab.at + $mtaSub = strtolower(env('MTA_SUB', 'mx') ?: 'mx'); // z.B. mx + $systemSub = strtolower(config('mailpool.platform_system_zone') ?: 'sysmail'); // z.B. sysmail - $systemSub = config('mailpool.platform_system_zone') ?: 'sysmail'; - $systemFqdn = "{$systemSub}.{$platformBase}"; // z.B. sysmail.nexlab.at + $serverFqdn = strtolower("{$mtaSub}.{$platformBase}"); // mx.nexlab.at + $systemFqdn = strtolower("{$systemSub}.{$platformBase}"); // sysmail.nexlab.at // ========================================================================= // 1) MAILSERVER-DOMAIN zuerst (mx.), is_server = true @@ -38,24 +35,31 @@ class SystemDomainSeeder extends Seeder ['is_active' => true, 'is_system' => false, 'is_server' => true] ); + Domain::where('is_server', true) + ->where('id', '!=', $serverDomain->id) + ->update(['is_server' => false]); + + // Falls bereits vorhanden, sicherstellen, dass is_server=true bleibt if (!$serverDomain->is_server) { $serverDomain->is_server = true; $serverDomain->save(); $this->command->info("Server-Host markiert: {$serverDomain->domain}"); } - // --- TLSA (3 1 1) prüfen/erzeugen (nur wenn LE-Zert schon da ist) ----- + // --- TLSA (3 1 1) prüfen/erzeugen (nur wenn noch nicht vorhanden) ------- $hasTlsa = TlsaRecord::where('domain_id', $serverDomain->id) ->where('host', $serverFqdn) ->where('service', '_25._tcp') ->exists(); if (!$hasTlsa) { + // TlsaService nutzt denselben Host aus ENV (mx.) – passt zu serverFqdn. $tlsa = app(TlsaService::class)->refreshForServerDomain($serverDomain); if ($tlsa) { $this->command->info("TLSA erstellt: _25._tcp.{$tlsa->host} 3 1 1 {$tlsa->hash}"); } else { - $this->command->warn("TLSA übersprungen: LE-Zertifikat (noch) nicht vorhanden – später erneut seeden."); + $path = "/etc/letsencrypt/live/{$serverFqdn}/fullchain.pem"; + $this->command->warn("TLSA übersprungen: LE-Zertifikat (noch) nicht vorhanden unter {$path}"); } } else { $this->command->line("TLSA bereits vorhanden, übersprungen."); @@ -68,6 +72,9 @@ class SystemDomainSeeder extends Seeder ['domain' => $systemFqdn], ['is_active' => true, 'is_system' => true] ); + if ($systemDomain->wasRecentlyCreated) { + $this->command->line("System-Domain angelegt: {$systemDomain->domain}"); + } // System-Absender (no-reply) – ohne Passwort (kein Login) MailUser::firstOrCreate( @@ -127,6 +134,7 @@ class SystemDomainSeeder extends Seeder openssl_pkey_export($res, $privateKeyPem); $details = openssl_pkey_get_details($res); $pubDer = $details['key']; + // Public PEM zu reinem Base64 für DKIM p= normalisieren $publicTxt = trim(preg_replace('/-----(BEGIN|END) PUBLIC KEY-----|\s+/', '', $pubDer)); return [$privateKeyPem, $publicTxt]; } @@ -155,6 +163,159 @@ class SystemDomainSeeder extends Seeder } } +//namespace Database\Seeders; +// +//use App\Models\DkimKey; +//use App\Models\Domain; +//use App\Models\MailUser; +//use App\Models\TlsaRecord; +// +//// ← NEU: fürs exists() +//use App\Services\DnsRecordService; +//use App\Services\TlsaService; +//use Illuminate\Database\Seeder; +// +//class SystemDomainSeeder extends Seeder +//{ +// public function run(): void +// { +// // --- Basiswerte aus Config/ENV --- +// $platformBase = config('mailpool.platform_zone', env('BASE_DOMAIN', 'example.com')); // z.B. nexlab.at +// if (!$platformBase || $platformBase === 'example.com') { +// $this->command->warn("BASE_DOMAIN ist 'example.com' – Seeder überspringt produktive Einträge."); +// return; +// } +// +// $mtaSub = env('MTA_SUB', 'mx') ?: 'mx'; // z.B. mx +// $serverFqdn = "{$mtaSub}.{$platformBase}"; // z.B. mx.nexlab.at +// +// $systemSub = config('mailpool.platform_system_zone') ?: 'sysmail'; +// $systemFqdn = "{$systemSub}.{$platformBase}"; // z.B. sysmail.nexlab.at +// +// // ========================================================================= +// // 1) MAILSERVER-DOMAIN zuerst (mx.), is_server = true +// // ========================================================================= +// $serverDomain = Domain::firstOrCreate( +// ['domain' => $serverFqdn], +// ['is_active' => true, 'is_system' => false, 'is_server' => true] +// ); +// +// if (!$serverDomain->is_server) { +// $serverDomain->is_server = true; +// $serverDomain->save(); +// $this->command->info("Server-Host markiert: {$serverDomain->domain}"); +// } +// +// // --- TLSA (3 1 1) prüfen/erzeugen (nur wenn LE-Zert schon da ist) ----- +// $hasTlsa = TlsaRecord::where('domain_id', $serverDomain->id) +// ->where('host', $serverFqdn) +// ->where('service', '_25._tcp') +// ->exists(); +// +// if (!$hasTlsa) { +// $tlsa = app(TlsaService::class)->refreshForServerDomain($serverDomain); +// if ($tlsa) { +// $this->command->info("TLSA erstellt: _25._tcp.{$tlsa->host} 3 1 1 {$tlsa->hash}"); +// } else { +// $path = "/etc/letsencrypt/live/{$serverDomain->domain}/fullchain.pem"; +// $this->command->warn("TLSA übersprungen: LE-Zertifikat (noch) nicht vorhanden unter {$path}"); +// } +// } +// +// // ========================================================================= +// // 2) SYSTEM-DOMAIN danach (sysmail.), is_system = true +// // ========================================================================= +// $systemDomain = Domain::firstOrCreate( +// ['domain' => $systemFqdn], +// ['is_active' => true, 'is_system' => true] +// ); +// +// // System-Absender (no-reply) – ohne Passwort (kein Login) +// MailUser::firstOrCreate( +// ['email' => "no-reply@{$systemFqdn}"], +// [ +// 'domain_id' => $systemDomain->id, +// 'localpart' => 'no-reply', +// 'password_hash' => null, +// 'is_active' => true, +// 'is_system' => true, +// 'must_change_pw' => false, +// 'quota_mb' => 0, +// ] +// ); +// +// // DKIM – Key erzeugen, falls keiner aktiv existiert +// if (!$systemDomain->dkimKeys()->where('is_active', true)->exists()) { +// [$privPem, $pubTxt] = $this->generateDkimKeyPair(); +// $selector = 'mwl1'; // frei wählbar, später rotieren +// +// DkimKey::create([ +// 'domain_id' => $systemDomain->id, +// 'selector' => $selector, +// 'private_key_pem' => $privPem, +// 'public_key_txt' => $pubTxt, +// 'is_active' => true, +// ]); +// +// $this->command->info("DKIM angelegt: Host = {$selector}._domainkey.{$systemFqdn}"); +// } +// +// $dk = $systemDomain->dkimKeys()->where('is_active', true)->latest()->first(); +// $dkimTxt = $dk ? "v=DKIM1; k=rsa; p={$dk->public_key_txt}" : null; +// +// app(DnsRecordService::class)->provision( +// $systemDomain, +// dkimSelector: $dk?->selector, +// dkimTxt: $dkimTxt, +// opts: [ +// 'dmarc_policy' => 'none', +// 'spf_tail' => '-all', +// // optional: 'ipv4' => $serverIp, 'ipv6' => ... +// ] +// ); +// +// $this->command->info("System-Domain '{$systemFqdn}' fertig. SPF/DMARC/DKIM & DNS-Empfehlungen eingetragen."); +// $this->printDnsHints($systemDomain); +// } +// +// /** @return array{0:string privatePem,1:string publicTxt} */ +// private function generateDkimKeyPair(): array +// { +// $res = openssl_pkey_new([ +// 'private_key_bits' => 2048, +// 'private_key_type' => OPENSSL_KEYTYPE_RSA, +// ]); +// openssl_pkey_export($res, $privateKeyPem); +// $details = openssl_pkey_get_details($res); +// $pubDer = $details['key']; +// $publicTxt = trim(preg_replace('/-----(BEGIN|END) PUBLIC KEY-----|\s+/', '', $pubDer)); +// return [$privateKeyPem, $publicTxt]; +// } +// +// private function printDnsHints(Domain $domain): void +// { +// $base = $domain->domain; +// +// $dkim = $domain->dkimKeys()->where('is_active', true)->latest()->first(); +// if ($dkim) { +// $this->command->line(" • DKIM TXT @ {$dkim->selector}._domainkey.{$base}"); +// $this->command->line(" v=DKIM1; k=rsa; p={$dkim->public_key_txt}"); +// } +// +// $spf = $domain->spf()->where('is_active', true)->latest()->first(); +// if ($spf) { +// $this->command->line(" • SPF TXT @ {$base}"); +// $this->command->line(" {$spf->record_txt}"); +// } +// +// $dmarc = $domain->dmarc()->where('is_active', true)->latest()->first(); +// if ($dmarc) { +// $this->command->line(" • DMARC TXT @ _dmarc.{$base}"); +// $this->command->line(" " . ($dmarc->record_txt ?? $dmarc->renderTxt())); +// } +// } +//} + //namespace Database\Seeders; //