— Artikel — № 104

104 —Security

TLS-overdracht: cert, chain, cron en vier verborgen valkuilen

Een eenpaginige TLS-overdrachtchecklist voor overgenomen sites: de leaf, de chain, de cron en de vier verborgen valkuilen die hosts vergeten te noemen.

Stilleven van bovenaf op linnen: TLS-overdrachtchecklist op ruitjespapier, papieren certificaten, messing sleutel, vulpen, rode lakzegel.
Hero · gestileerd stilleven№ 104

In het overdrachtsdocument stond "SSL wordt door de hostingmaatschappij geregeld, auto-renew staat aan." Drie weken later, om 06:12 op een zondag, stopte de iOS-app met praten tegen de WordPress-API en begon de telefoon van het bureau te rinkelen. Het leaf TLS-certificaat was netjes vernieuwd. De intermediate niet. Safari op iOS was de enige client die streng genoeg was om het op te merken, en alleen omdat Apple die ochtend stilletjes was gestopt met het vertrouwen van de oude cross-sign.

Dit is het derde incident dit jaar dat we op exact dezelfde manier zien terugkomen bij een overgenomen verouderde site. Dus hebben we het opgeschreven. Wat volgt is de TLS-overdrachtchecklist die we nu draaien op elke site die we overnemen, in de volgorde waarin we hem afwerken, met de commando's en de vier dingen die hosts vergeten te noemen.

De eenpaginige TLS-overdrachtchecklist

Voordat je de sleutels van een site aanneemt, loop je deze van boven naar beneden door. Op één domein kost dit ongeveer vijftien minuten en het brengt negentig procent van de tikkende bommen aan het licht.

  1. Leaf-certificaat: issuer, vervaldatum, SANs, type sleutel.
  2. Chain: elke intermediate aanwezig, in volgorde, geen self-signed root onderaan.
  3. OCSP stapling: ingeschakeld, en de staple is vers.
  4. Vernieuwingsmechanisme: wie voert het uit, waar staat het, wanneer liep het voor het laatst succesvol.
  5. Reload-hook: pikt de webserver het nieuwe cert daadwerkelijk op zonder dat er iemand bij komt.
  6. HSTS: header aanwezig, max-age redelijk, preload-status bekend.
  7. Redirect-keten: één hop van http:// naar https://, alleen de canonieke host.
  8. Mixed content: geen http://-assets in de gerenderde HTML of in de database.
  9. Subdomeinen en mail: www, api, mail, cdn allemaal afgedekt of expliciet uitgesloten.
  10. Private key: waar staat hij, wie kan hem lezen, zit hij ergens in een back-up-tarball.

De rest van deze post werkt de eerste vijf in detail uit, daarna de vier valkuilen. Items 6 tot en met 10 verdienen elk hun eigen post en krijgen die ook nog.

Leaf, chain en OCSP, van buiten naar binnen

Begin vanaf het publieke internet, niet vanaf de server. Wat de wereld ziet is wat telt. Eén commando vertelt je bijna alles:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null \
  | openssl x509 -noout -issuer -subject -dates -ext subjectAltName

Je let in de output op vier dingen. De issuer-regel moet een echte CA noemen, niet de host zelf. De notAfter-datum moet meer dan dertig dagen in de toekomst liggen. De subjectAltName-lijst moet elke hostname bevatten die de site daadwerkelijk serveert, inclusief www als je daarheen redirect. En het type sleutel, zichtbaar met -text, moet id-ecPublicKey zijn of RSA op 2048 bits of meer. RSA 1024 duikt nog altijd op bij sites die voor het laatst in 2014 zijn aangeraakt.

De chain is het stuk dat onze zondagochtend brak. Draai hetzelfde commando opnieuw zonder </dev/null en lees het Certificate chain-blok bovenaan. Je wilt de leaf, dan één of twee intermediates, en daarna niets meer. Zie je onderaan een self-signed root, dan stuurt de server het rootcertificaat over de lijn mee. Niet schadelijk, wel verspilling, maar ook een aanwijzing dat degene die het opzette een fullchain.pem heeft gekopieerd zonder die te begrijpen. Zie je alleen de leaf en geen intermediate, dan heb je een bom gevonden. Moderne desktopbrowsers halen de ontbrekende intermediate op via AIA en overleven het. Oudere Android-versies, sommige Java-clients en elke strikte pinning-library niet.

Voor OCSP is de snelste check SSL Labs, die in één rapport ook problemen met de chain-volgorde en zwakke ciphers vlagt. Liever in de terminal:

openssl s_client -connect example.com:443 -servername example.com -status </dev/null 2>&1 \
  | grep -A 5 'OCSP Response'

Je wilt OCSP Response Status: successful zien en een This Update-timestamp van de afgelopen paar dagen. Ontbreekt de response, dan staat stapling uit. Is This Update twee maanden oud, dan is de cron die de staple ververst stuk en zit je één revocation van een zachte storing af.

Het verhaal van vernieuwing en reload

Auto-renewal is de regel die elke host in het overdrachtsdocument zet. Het is bijna nooit het hele verhaal. De vraag waar je écht antwoord op nodig hebt: welk proces, op welke machine, schrijft welk bestand, en welk ander proces herlaadt welke daemon om het op te pikken.

Op een cPanel- of Plesk-bak is het antwoord meestal "het paneel regelt het," wat klopt tot iemand AutoSSL uitschakelt voor een domein dat zes maanden geleden een DCV-check niet haalde en niemand het merkte. Log in, zoek de SSL/TLS-statuspagina en kijk naar elk domein in een gele of rode staat. Dat zijn je bommen.

Op een kale VPS met certbot staat de renewal op een van drie plekken. Check ze alle drie:

systemctl list-timers | grep -i certbot
cat /etc/cron.d/certbot 2>/dev/null
crontab -l -u root | grep -i certbot

Controleer daarna of hij daadwerkelijk heeft gelopen:

journalctl -u certbot.timer --since '60 days ago' | tail -20
ls -la /var/log/letsencrypt/ | tail -5

Een renewal die op een 90-daags certificaat al 80 dagen niet meer is gelopen, vernieuwt niet automatisch. Hij rolt uit. Hetzelfde geldt voor de reload-hook. De --deploy-hook van certbot hoort de webserver te herstarten of te herladen na een geslaagde vernieuwing. Vind je een deploy-hook die service nginx restart draait terwijl de site op Apache draait, of die wijst naar een container die niet meer bestaat, dan vernieuwt het cert op disk en bereikt het nooit het luisterende proces.

De vier valkuilen die hosts vergeten

Dit zijn de items die bijna nooit in een overdrachtsdocument terechtkomen, in ongeveer de volgorde waarin we ze tegenkomen.

1. De www-variant die niemand vernieuwde

De leaf dekt example.com. De redirect stuurt gebruikers naar www.example.com. De www-SAN stond op het cert toen de site in 2019 live ging. Iemand heeft het cert in 2023 opnieuw uitgegeven zonder de www-SAN, omdat hij testte met curl en curl de redirect stilletjes volgde. Browsers doen dat niet. Check altijd zowel de apex als www, en elk subdomein dat ooit in een marketingmail is voorgekomen.

2. Het vergeten subdomein op een andere server

Sites groeien aan. shop.example.com op een Magento-bak, blog.example.com op de WordPress-bak, api.example.com op een kleine Node-service die iemand voor de iOS-app heeft opgezet. Elk heeft een eigen certificaat, een eigen renewal-cron, een eigen manier om kapot te gaan. Het overdrachtsdocument behandelt de WordPress-bak. Doe een snelle DNS-sweep en check elk A- en CNAME-record dat naar iets wijst dat je beheert.

dig +short example.com ANY
dig +short example.com TXT | grep -i spf
for sub in www shop blog api mail cdn staging; do
  echo "--- $sub.example.com ---"
  openssl s_client -connect $sub.example.com:443 -servername $sub.example.com </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -dates 2>/dev/null
done

3. De HSTS-preload-belofte

Heeft een eerdere ontwikkelaar Strict-Transport-Security: max-age=63072000; includeSubDomains; preload ingesteld en het domein ingediend bij de HSTS preload list, dan heb je een belofte geërfd dat elk subdomein, voor altijd, geldige HTTPS gaat serveren. De dag dat je internal.example.com voor een snelle test op gewone HTTP optrekt, is hij onbereikbaar vanuit gepreloade browsers. Controleer de huidige header en de preload-lijst voordat je de sleutels aanneemt.

4. De .htaccess-redirect die loopt achter een proxy

Het klassieke blok ziet er onschuldig uit:

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Achter een loadbalancer of een Cloudflare-achtige proxy die TLS termineert en platte HTTP doorstuurt naar de origin, staat %{HTTPS} altijd op off. De browser ziet een redirect-loop. De fix is de forwarded header vertrouwen:

RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Doe dit alleen als je hebt geverifieerd dat de proxy inkomende X-Forwarded-Proto van clients strikt verwijdert. Anders kan een aanvaller de header vervalsen en de redirect omzeilen. De mod_rewrite-documentatie van Apache heeft het volledige verhaal.

Wat we uiteindelijk voor onszelf hebben gebouwd

Het incident op zondagochtend was de derde keer dat we deze checklist vanuit een kladblok draaiden en de tweede keer dat we wensten dat de databasestap één klik was, in plaats van een wp search-replace dry run gevolgd door een echte run gevolgd door een cache flush. Toen we Pier bouwden liepen we precies hiertegenaan. Wat we uiteindelijk hebben gedaan, is de FTP-boomstructuur en de MySQL-editor achter hetzelfde chat-oppervlak zetten, zodat een TLS-overdrachts-audit en de siteurl-herschrijving die er altijd op volgt allebei in dezelfde versiegeschiedenis belanden.

Het kleinste wat je vandaag kunt doen: pak één site die je in de afgelopen twaalf maanden hebt overgenomen en draai het openssl s_client-commando vanaf het begin van deze post. Lees de chain. Ziet hij er verkeerd uit, dan heb je je volgende werkuur te pakken.

— Vragen —

Hoe vaak moet ik een TLS-overdrachtsaudit draaien op een overgenomen site?

Eén keer bij de overdracht, daarna één keer per jaar, en na elke infrastructuurwijziging: CDN-wissel, hostingmigratie, paneelupgrade of wisseling van DNS-provider. De renewal-cron is geen vervanging.

Waarom doet de chain ertoe als browsers ontbrekende intermediates via AIA ophalen?

Desktopbrowsers doen dat. Oudere Android-versies, Java HTTP-clients, sommige IoT-firmware en elke strikte pinning-library niet. Een ontbrekende intermediate breekt mobiele apps eerder dan websites.

Is HSTS preload veilig om in te schakelen op een legacy site?

Alleen als je elk huidig en toekomstig subdomein beheert en daar voor altijd geldige HTTPS op kunt serveren. Verwijderen uit de preload-lijst duurt maanden. Bij twijfel: laat de preload-vlag weg.