safeKey($domain->domain); // $selKey = $this->safeKey($selector, 32); $dirKey = $this->dirKeyFor($domain); $selKey = preg_replace('/[^A-Za-z0-9._-]/', '_', substr($selector, 0, 32)); // schlicht & stabil // Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code) $disk = Storage::disk('local'); $baseRel = "dkim/{$dirKey}"; // unsere Dateien (alle Varianten, damit sowohl App als auch OpenDKIM glücklich sind) $privPemRel = "{$baseRel}/{$selKey}.pem"; // PKCS#1/PKCS#8 (App-Backup) $pubPemRel = "{$baseRel}/{$selKey}.pub"; // Public PEM (App-Backup) $privOKRel = "{$baseRel}/{$selKey}.private"; // <-- OpenDKIM erwartet *genau das* $dnsTxtRel = "{$baseRel}/{$selKey}.txt"; // TXT-String "v=DKIM1; k=rsa; p=..." // Absolute Pfade $privPemAbs = method_exists($disk, 'path') ? $disk->path($privPemRel) : storage_path('app/private/'.$privPemRel); $pubPemAbs = method_exists($disk, 'path') ? $disk->path($pubPemRel) : storage_path('app/private/'.$pubPemRel); $privOKAbs = method_exists($disk, 'path') ? $disk->path($privOKRel) : storage_path('app/private/'.$privOKRel); $dnsTxtAbs = method_exists($disk, 'path') ? $disk->path($dnsTxtRel) : storage_path('app/private/'.$dnsTxtRel); // Wenn schon vollständig vorhanden → direkt zurück if ($disk->exists($privOKRel) && $disk->exists($dnsTxtRel)) { $privatePem = $disk->exists($privPemRel) ? $disk->get($privPemRel) : $disk->get($privOKRel); $publicPem = $disk->exists($pubPemRel) ? $disk->get($pubPemRel) : null; $dnsTxt = $disk->get($dnsTxtRel); return [ 'selector' => $selKey, 'priv_path' => $privOKAbs, 'pub_path' => $pubPemAbs, 'private_pem' => $privatePem, 'public_pem' => $publicPem, 'dns_name' => "{$selKey}._domainkey", 'dns_txt' => $dnsTxt, 'bits' => $bits, ]; } // Neu generieren $disk->makeDirectory($baseRel); $res = openssl_pkey_new([ 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => $bits, ]); if ($res === false) { throw new RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } $privatePem = ''; if (!openssl_pkey_export($res, $privatePem)) { throw new RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } $details = openssl_pkey_get_details($res); if ($details === false || empty($details['key'])) { throw new RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); } $publicPem = $details['key']; $publicBase64 = trim(preg_replace('/-----(BEGIN|END)\s+PUBLIC KEY-----|\s+/', '', $publicPem)); if (strlen($publicBase64) < 300) { throw new RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); } $dnsTxt = "v=DKIM1; k=rsa; p={$publicBase64}"; // Dateien schreiben (App + OpenDKIM) if (!$disk->put($privPemRel, $privatePem)) throw new RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privPemRel}"); if (!$disk->put($pubPemRel, $publicPem)) throw new RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubPemRel}"); if (!$disk->put($privOKRel, $privatePem)) throw new RuntimeException("DKIM: OpenDKIM-Key schreiben fehlgeschlagen: {$privOKRel}"); if (!$disk->put($dnsTxtRel, $dnsTxt)) throw new RuntimeException("DKIM: DNS-TXT schreiben fehlgeschlagen: {$dnsTxtRel}"); // Dateirechte (best effort) @chmod($privPemAbs, 0600); @chmod($privOKAbs, 0600); @chmod($pubPemAbs, 0640); @chmod($dnsTxtAbs, 0644); // DB pflegen DkimKey::updateOrCreate( ['domain_id' => $domain->id, 'selector' => $selKey], [ 'private_key_pem' => $privatePem, 'public_key_txt' => $publicBase64, 'is_active' => true, ] ); // OpenDKIM einhängen (wenn Helper existiert) $helper = '/usr/local/sbin/mailwolt-install-dkim'; Log::debug('DKIM helper call', [ 'as' => trim(Process::run(['whoami'])->output()), 'helper' => $helper, 'exists' => is_file($helper), // KEINE privaten Keys loggen! ]); $helper = '/usr/local/sbin/mailwolt-install-dkim'; $proc = Process::timeout(30)->run([ 'sudo','-n', $helper, $domain->domain, $selKey, $privOKAbs, $dnsTxtAbs ]); Log::info('DKIM install exit', [ 'cmd' => $helper, 'exit' => $proc->exitCode(), 'out' => $proc->output(), 'err' => $proc->errorOutput(), ]); if (!$proc->successful()) { // Optionale bessere Fehlermeldung $err = $proc->errorOutput(); if (str_contains($err, 'command not found') || str_contains($err, 'No such file')) { throw new \RuntimeException('Helper fehlt: '.$helper.' (Installer erneut ausführen?)'); } if (str_contains($err, 'sudo') && str_contains($err, 'a password is required')) { throw new \RuntimeException('sudo NOPASSWD fehlt für www-data → /etc/sudoers.d/mailwolt-dkim prüfen.'); } throw new \RuntimeException("install-dkim failed: ".$err); } Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']); // if (is_file($helper)) { // $cmd = [ // 'sudo','-n', $helper, // $domain->domain, // $selKey, // $privOKAbs, // $dnsTxtAbs, // ]; // // $res = Process::timeout(30)->run($cmd); // // Log::info('DKIM install exit', [ // 'cmd' => implode(' ', $cmd), // 'exit' => $res->exitCode(), // 'out' => $res->output(), // 'err' => $res->errorOutput(), // ]); // // if ($res->failed()) { // throw new RuntimeException('OpenDKIM-Install fehlgeschlagen: '.$res->errorOutput()); // } // // Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']); // } else { // Log::warning('DKIM helper not found', ['path' => $helper]); // } return [ 'selector' => $selKey, 'priv_path' => $privOKAbs, 'pub_path' => $pubPemAbs, 'private_pem' => $privatePem, 'public_pem' => $publicPem, 'dns_name' => "{$selKey}._domainkey", 'dns_txt' => $dnsTxt, 'bits' => $bits, ]; } protected function dirKeyFor(Domain $domain, int $max = 64): string { $raw = $domain->domain; // <<< WICHTIG: Domain-String, NICHT die ID! $san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw); if ($san === '') $san = 'unknown'; if (strlen($san) > $max) { $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12); } return $san; } // selector optional: wenn null → alle Selector der Domain löschen public function removeForDomain(Domain|string $domain, ?string $selector = null): void { $name = $domain instanceof Domain ? $domain->domain : $domain; // Selektoren ermitteln if (is_null($selector)) { $keys = $domain instanceof Domain ? $domain->dkimKeys()->pluck('selector')->all() : \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name)) ->pluck('selector')->all(); $keys = $keys ?: ['mwl1']; } else { $keys = [$selector]; } foreach ($keys as $sel) { $cmd = ['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim',$name,$sel]; $res = \Illuminate\Support\Facades\Process::timeout(30)->run($cmd); Log::info('DKIM remove exit', [ 'domain' => $name, 'selector' => $sel, 'exit' => $res->exitCode(), 'out' => $res->output(), 'err' => $res->errorOutput(), ]); if ($res->failed()) { throw new \RuntimeException('OpenDKIM-Remove fehlgeschlagen: '.$res->errorOutput()); } } // Dienst neu laden (best effort) \Illuminate\Support\Facades\Process::run(['sudo','-n','/bin/systemctl','reload','opendkim']); } // public function removeForDomain(Domain|string $domain, ?string $selector = null): void // { // $name = $domain instanceof Domain ? $domain->domain : $domain; // // if (is_null($selector)) { // // alle Selector aus DB holen und nacheinander entfernen // $keys = $domain instanceof Domain // ? $domain->dkimKeys()->pluck('selector')->all() // : \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name)) // ->pluck('selector')->all(); // // $keys = $keys ?: ['mwl1']; // notfalls versuchen wir Standard // } else { // $keys = [$selector]; // } // // foreach ($keys as $sel) { // Process::run(['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim',$name,$sel]); // } // // // Dienst neu laden (ohne Fehler abbrechen) // Process::run(['sudo','-n','/bin/systemctl','reload','opendkim']); // // $res = Process::timeout(30)->run($cmd); // Log::info('DKIM install exit', [ // 'exit' => $res->exitCode(), // 'out' => $res->output(), // 'err' => $res->errorOutput(), // ]); // } // public function removeForDomain(Domain|string $domain, ?string $selector = null): void // { // $name = $domain instanceof \App\Models\Domain ? $domain->domain : $domain; // $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); // // // Root-Helper ausführen // $p = Process::run([ // 'sudo','-n','/usr/local/sbin/mailwolt-remove-dkim', // $name, $selector // ]); // if (!$p->successful()) { // throw new \RuntimeException('mailwolt-remove-dkim failed: '.$p->errorOutput()); // } // // // OpenDKIM neu laden // Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']); // } // public function removeForDomain(Domain|string $domain): void // { // $domainName = $domain instanceof Domain ? $domain->domain : $domain; // $keyTable = '/etc/opendkim/KeyTable'; // $signTable = '/etc/opendkim/SigningTable'; // $keyDir = "/etc/opendkim/keys/{$domainName}"; // // // Tabellen bereinigen // foreach ([$keyTable, $signTable] as $file) { // if (is_file($file)) { // $lines = file($file, FILE_IGNORE_NEW_LINES); // $filtered = array_filter($lines, fn($l) => !str_contains($l, $domainName)); // file_put_contents($file, implode(PHP_EOL, $filtered) . PHP_EOL); // } // } // // // Key-Verzeichnis löschen // if (is_dir($keyDir)) { // \Illuminate\Support\Facades\File::deleteDirectory($keyDir); // } // } // protected function safeKey($value, int $max = 64): string // { // if (is_object($value)) { // if (isset($value->id)) $value = $value->id; // elseif (method_exists($value, 'getKey')) $value = $value->getKey(); // else $value = json_encode($value); // } // $raw = (string) $value; // $san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw); // if ($san === '' ) $san = 'unknown'; // if (strlen($san) > $max) { // $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12); // } // return $san; // } // // protected static function extractPublicKeyBase64(string $pem): string // { // // Hole den Body zwischen den Headern (multiline, dotall) // if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) { // throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).'); // } // // // Whitespace entfernen → reines Base64 // $b64 = preg_replace('/\s+/', '', $m[1]); // // if ($b64 === '' || base64_decode($b64, true) === false) { // throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.'); // } // // return $b64; // } }