From 40d3dc0fbecec6b70e9d538f87fe58b32d212690 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 13 Jul 2026 16:56:28 +0000 Subject: [PATCH] Provision PowerDNS zones immediately on domain link and add ensure-zone command. Async-only zone jobs left Ladill NS returning REFUSED for new domains, which surfaces as Let's Encrypt SERVFAIL during SSL issuance. Co-authored-by: Cursor --- .../Commands/EnsureDomainZoneCommand.php | 58 +++++++++++++++++++ .../Domain/DomainVerificationService.php | 23 +++++++- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/EnsureDomainZoneCommand.php diff --git a/app/Console/Commands/EnsureDomainZoneCommand.php b/app/Console/Commands/EnsureDomainZoneCommand.php new file mode 100644 index 0000000..942626c --- /dev/null +++ b/app/Console/Commands/EnsureDomainZoneCommand.php @@ -0,0 +1,58 @@ +argument('host'), ". \t\n\r\0\x0B")); + $domain = Domain::query()->where('host', $host)->first(); + + if (! $domain) { + $this->error("No Domain row found for {$host}. Link the domain in Hosting first."); + + return self::FAILURE; + } + + $this->info(sprintf( + 'Ensuring zone for %s (domain_id=%d, dns_mode=%s, onboarding=%s)', + $domain->host, + $domain->id, + $domain->dns_mode ?: '(null)', + $domain->onboarding_mode ?: '(null)' + )); + + if (! $this->option('sync')) { + ProvisionDomainZoneJob::dispatch($domain->id); + $this->info('Queued ProvisionDomainZoneJob. Use --sync to run immediately.'); + + return self::SUCCESS; + } + + try { + (new ProvisionDomainZoneJob($domain->id))->handle($pdns); + $this->info('Zone provisioned on PowerDNS master.'); + ProvisionDomainSlaveZoneJob::dispatch($domain->id); + $this->info('Queued slave zone sync.'); + } catch (\Throwable $e) { + $this->error('Failed: '.$e->getMessage()); + + return self::FAILURE; + } + + return self::SUCCESS; + } +} diff --git a/app/Services/Domain/DomainVerificationService.php b/app/Services/Domain/DomainVerificationService.php index ab9e386..99a1a80 100644 --- a/app/Services/Domain/DomainVerificationService.php +++ b/app/Services/Domain/DomainVerificationService.php @@ -3,6 +3,7 @@ namespace App\Services\Domain; use App\Jobs\MailProvisioningJob; +use App\Jobs\ProvisionDomainSlaveZoneJob; use App\Jobs\ProvisionDomainZoneJob; use App\Jobs\SslProvisioningJob; use App\Models\Domain; @@ -12,6 +13,7 @@ use App\Models\DomainNsCheck; use App\Notifications\DomainVerifiedNotification; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; class DomainVerificationService @@ -265,7 +267,26 @@ class DomainVerificationService public function reprovision(Domain $domain): void { - $this->dispatchProvisioningChain($domain->id); + $domain = $domain->fresh() ?? $domain; + $this->ensureDkimKeys($domain); + + // Create the authoritative zone immediately when possible. Async-only + // provisioning left Ladill NS delegated with REFUSED answers (SERVFAIL + // for Let's Encrypt) whenever the queue lagged or failed. + try { + $this->pdns->ensureZone($domain->fresh()); + ProvisionDomainSlaveZoneJob::dispatch($domain->id); + } catch (\Throwable $e) { + Log::warning('Immediate PDNS ensureZone failed; queueing retry', [ + 'domain_id' => $domain->id, + 'host' => $domain->host, + 'error' => $e->getMessage(), + ]); + ProvisionDomainZoneJob::dispatch($domain->id); + } + + MailProvisioningJob::dispatch($domain->id); + SslProvisioningJob::dispatch($domain->id); } private function dispatchProvisioningChain(int $domainId): void