Anpassen der Tlsa Record erstellung
parent
c2dab0a56b
commit
7fd76f3b32
|
|
@ -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/<host>.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/<host>.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;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
<?php
|
||||
|
||||
|
||||
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;
|
||||
|
|
@ -17,18 +14,18 @@ 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
|
||||
// ---- Basis aus ENV/Config ------------------------------------------------
|
||||
$platformBase = strtolower(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
|
||||
$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.<BASE_DOMAIN>), 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.<base>) – 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.<BASE_DOMAIN>), 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.<BASE_DOMAIN>), 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;
|
||||
//
|
||||
|
|
|
|||
Loading…
Reference in New Issue