- Caddy ersetzt den Hetzner-LB: terminiert TLS (Portal-Domain automatisch) und load-balanced per reverse_proxy über die App-Nodes. Für Custom-Domains (§11) On-Demand-TLS, autorisiert über GET /internal/tls-allowed. - TlsCheckController + DomainRepository::findVerifiedByHostname: erlaubt Zertifikate nur für Portal-Domain oder verifizierte Domains (Schutz vor Cert-Flooding). - Terraform: hcloud_load_balancer entfernt, Caddy-Server + Firewall (80/443) + cloud-init-caddy (Caddyfile templated mit Upstreams/Domain/ACME). - Optional Hetzner DNS via API (manage_dns): A-Record Portal + Wildcard → Caddy. - nginx.prod: /internal zu Symfony geroutet; APP_PORTAL_DOMAIN-Env. Validiert: Caddyfile (caddy validate), Terraform (validate), /internal/tls-allowed (200/403/400). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 lines
1.4 KiB
PHP
45 lines
1.4 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Repository\DomainRepository;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
/**
|
|
* On-Demand-TLS-Autorisierung für Caddy (KONZEPT §11): Caddy fragt vor dem
|
|
* Ausstellen eines Let's-Encrypt-Zertifikats hier nach, ob die Domain erlaubt ist.
|
|
* Erlaubt = Portal-Domain oder eine verifizierte Custom-Domain aus der DB.
|
|
* 200 → ausstellen, sonst ablehnen (verhindert unbegrenzte Zertifikatsanfragen).
|
|
*/
|
|
final class TlsCheckController
|
|
{
|
|
public function __construct(
|
|
#[Autowire('%env(APP_PORTAL_DOMAIN)%')]
|
|
private readonly string $portalDomain,
|
|
) {
|
|
}
|
|
|
|
#[Route('/internal/tls-allowed', name: 'tls_allowed', methods: ['GET'])]
|
|
public function __invoke(Request $request, DomainRepository $domains): Response
|
|
{
|
|
$host = strtolower(trim((string) $request->query->get('domain')));
|
|
if ('' === $host) {
|
|
return new Response('missing domain', 400);
|
|
}
|
|
|
|
$portal = strtolower($this->portalDomain);
|
|
if ($host === $portal || $host === 'www.'.$portal) {
|
|
return new Response('ok', 200);
|
|
}
|
|
|
|
if (null !== $domains->findVerifiedByHostname($host)) {
|
|
return new Response('ok', 200);
|
|
}
|
|
|
|
return new Response('not allowed', 403);
|
|
}
|
|
}
|