Anpassen der Tlsa Record erstellung

main
boban 2025-10-17 06:30:50 +02:00
parent 3ee53a0ae5
commit 0a03324eec
2 changed files with 142 additions and 50 deletions

View File

@ -21,9 +21,9 @@ class TlsaRefresh extends Command
return self::SUCCESS; return self::SUCCESS;
} }
$rec = $tlsa->refreshForMx(); $rec = $tlsa->refreshForMx(); // <─ HIER
if (!$rec) { if (!$rec) {
$this->warn('TLSA konnte nicht aktualisiert werden (Zertifikat fehlt?).'); $this->warn('TLSA konnte nicht aktualisiert werden (is_server / Zertifikat / Rechte?).');
return self::FAILURE; return self::FAILURE;
} }
$this->info("TLSA ok: {$rec->service}.{$rec->host} 3 1 1 {$rec->hash}"); $this->info("TLSA ok: {$rec->service}.{$rec->host} 3 1 1 {$rec->hash}");

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Services; namespace App\Services;
use App\Models\Domain; use App\Models\Domain;
@ -10,88 +9,181 @@ class TlsaService
public function resolveMxHost(): string public function resolveMxHost(): string
{ {
$base = env('BASE_DOMAIN', 'example.com'); $base = env('BASE_DOMAIN', 'example.com');
$sub = env('MTA_SUB', 'mx') ?: 'mx'; $sub = env('MTA_SUB', 'mx') ?: 'mx';
return "{$sub}.{$base}"; return "{$sub}.{$base}";
} }
private function certCandidates(string $host): array private function certCandidates(string $host): array
{ {
// bevorzugt die vom Deploy-Hook gesetzten, lesbaren Symlinks return [
$candidates = [ '/etc/ssl/mail/fullchain.pem', // vom Deploy-Wrapper kopiert (lesbar)
'/etc/ssl/mail/fullchain.pem', // MX "/etc/letsencrypt/live/{$host}/fullchain.pem", // Fallback (meist nicht lesbar)
"/etc/letsencrypt/live/{$host}/fullchain.pem", // Fallback
]; ];
return $candidates;
}
public function refreshForMx(string $service = '_25._tcp'): ?\App\Models\TlsaRecord
{
$host = $this->resolveMxHost();
// bevorzugt die als Server markierte Domain
$serverDomain = \App\Models\Domain::where('is_server', true)->first();
// Fallback: Domain über FQDN (mx.<BASE_DOMAIN>) finden
if (!$serverDomain) {
$serverDomain = \App\Models\Domain::where('domain', $host)->first();
}
if (!$serverDomain) {
// Keine passende Domain in der DB → nichts tun
return null;
}
return $this->refreshForServerDomain($serverDomain, $service);
} }
private function firstReadableCert(string $host): ?string private function firstReadableCert(string $host): ?string
{ {
foreach ($this->certCandidates($host) as $path) { foreach ($this->certCandidates($host) as $p) {
if (is_file($path) && is_readable($path)) { if (is_file($p) && is_readable($p)) return $p;
return $path;
}
} }
return null; return null;
} }
public function computeHashFromCert(string $host): ?string public function computeHashFromCertPath(string $certPath): ?string
{ {
$certPath = $this->firstReadableCert($host); $cmd = "openssl x509 -in " . escapeshellarg($certPath) . " -noout -pubkey"
if (!$certPath) return null;
$cmd = "openssl x509 -in ".escapeshellarg($certPath)." -noout -pubkey"
. " | openssl pkey -pubin -outform DER" . " | openssl pkey -pubin -outform DER"
. " | openssl dgst -sha256"; . " | openssl dgst -sha256";
$out = shell_exec($cmd.' 2>/dev/null') ?? ''; $out = shell_exec($cmd . ' 2>/dev/null') ?? '';
$hash = preg_replace('/^SHA256\(stdin\)=\s*/', '', trim($out)); $out = trim($out);
if ($out === '') return null;
// Variationen entfernen: "SHA256(stdin)= x", "SHA2-256(stdin)= x"
$hash = preg_replace('/^SHA2?56\(stdin\)=\s*/i', '', $out);
$hash = preg_replace('/^SHA2?-256\(stdin\)=\s*/i', '', $hash); // falls "SHA2-256"
$hash = trim($hash);
return $hash !== '' ? $hash : null; return $hash !== '' ? $hash : null;
} }
public function refreshForServerDomain(Domain $serverDomain, string $service = '_25._tcp'): ?TlsaRecord /** Holt is_server-Domain und schreibt/aktualisiert TLSA (3 1 1) */
public function refreshForMx(string $service = '_25._tcp'): ?TlsaRecord
{ {
$host = $this->resolveMxHost(); $host = $this->resolveMxHost();
$hash = $this->computeHashFromCert($host);
// 1) Server-Domain ermitteln (es darf genau eine mit is_server=true geben)
$serverDomain = Domain::where('is_server', true)
->where('domain', $host) // doppelt absichern: passt zur ENV
->first();
if (!$serverDomain) {
// Kein is_server Eintrag -> nichts zu tun
return null;
}
// 2) Zertifikat finden/lesen
$certPath = $this->firstReadableCert($host);
if (!$certPath) return null;
// 3) Hash berechnen
$hash = $this->computeHashFromCertPath($certPath);
if (!$hash) return null; if (!$hash) return null;
$certPath = $this->firstReadableCert($host) ?? ''; // 4) DB schreiben (idempotent)
$rec = TlsaRecord::updateOrCreate( $rec = TlsaRecord::updateOrCreate(
['domain_id' => $serverDomain->id, 'host' => $host, 'service' => $service], ['domain_id' => $serverDomain->id, 'host' => $host, 'service' => $service],
[ [
'usage' => 3, 'usage' => 3,
'selector' => 1, 'selector' => 1,
'matching' => 1, 'matching' => 1,
'hash' => $hash, 'hash' => $hash,
'cert_path' => $certPath, 'cert_path' => $certPath,
] ]
); );
// 5) optionale Export-Datei
@mkdir('/etc/mailwolt/dns', 0755, true); @mkdir('/etc/mailwolt/dns', 0755, true);
$line = sprintf('%s.%s IN TLSA %d %d %d %s', $service, $host, 3, 1, 1, $hash); $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", $line . "\n");
return $rec; return $rec;
} }
}
//namespace App\Services;
//
//use App\Models\Domain;
//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}";
// }
//
// private function certCandidates(string $host): array
// {
// // bevorzugt die vom Deploy-Hook gesetzten, lesbaren Symlinks
// $candidates = [
// '/etc/ssl/mail/fullchain.pem', // MX
// "/etc/letsencrypt/live/{$host}/fullchain.pem", // Fallback
// ];
// return $candidates;
// }
//
// public function refreshForMx(string $service = '_25._tcp'): ?\App\Models\TlsaRecord
// {
// $host = $this->resolveMxHost();
//
// // bevorzugt die als Server markierte Domain
// $serverDomain = \App\Models\Domain::where('is_server', true)->first();
//
// // Fallback: Domain über FQDN (mx.<BASE_DOMAIN>) finden
// if (!$serverDomain) {
// $serverDomain = \App\Models\Domain::where('domain', $host)->first();
// }
//
// if (!$serverDomain) {
// // Keine passende Domain in der DB → nichts tun
// return null;
// }
//
// return $this->refreshForServerDomain($serverDomain, $service);
// }
//
// private function firstReadableCert(string $host): ?string
// {
// foreach ($this->certCandidates($host) as $path) {
// if (is_file($path) && is_readable($path)) {
// return $path;
// }
// }
// return null;
// }
//
// public function computeHashFromCert(string $host): ?string
// {
// $certPath = $this->firstReadableCert($host);
// if (!$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;
// }
//
// public function refreshForServerDomain(Domain $serverDomain, string $service = '_25._tcp'): ?TlsaRecord
// {
// $host = $this->resolveMxHost();
// $hash = $this->computeHashFromCert($host);
// if (!$hash) return null;
//
// $certPath = $this->firstReadableCert($host) ?? '';
//
// $rec = TlsaRecord::updateOrCreate(
// ['domain_id' => $serverDomain->id, 'host' => $host, 'service' => $service],
// [
// 'usage' => 3,
// 'selector' => 1,
// 'matching' => 1,
// 'hash' => $hash,
// 'cert_path' => $certPath,
// ]
// );
//
// @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;
// }
// public function computeHashFromCert(string $host): ?string // public function computeHashFromCert(string $host): ?string
// { // {
@ -194,7 +286,7 @@ class TlsaService
// //
// return $rec; // return $rec;
// } // }
} //}
//class TlsaService //class TlsaService