vcard4reseller/backend/src/Controller/TlsCheckController.php
Thomas Peterson 79e996ab03 Deployment: Caddy-Edge (TLS + On-Demand für Custom-Domains) + Hetzner DNS
- 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>
2026-05-31 22:13:29 +02:00

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);
}
}