sslxy

server-haertung

TLS, headers, CSP, Trusted Types, rate limiting – A+ als resultaat van degelijk werk, niet als doel op zich.

Server-hardening begint niet met een toolscore en eindigt daar ook niet. Het begint met het begrip welk aanvalsoppervlak een website überhaupt heeft, en het leeft ervan dat elke ingestelde directive een bekend doel vervult.

Deze pagina beschrijft geen veiligheidsbijgeloof en geen copy-paste uit generatoren. Ze beschrijft de nuchtere praktijk: HTTPS zonder oude ballast, zinvolle security-headers, een passende CSP, duidelijke grenzen voor browserfuncties en een beheer dat logs daadwerkelijk leest.

Security Snapshot

> AUDIT OVERVIEW
TLS alleen moderne protocollen, geen nostalgische concessies HEADERS HSTS, CSP, nosniff, frame-ancestors, Referrer-Policy, Permissions-Policy BROWSER minder impliciet vertrouwen, meer expliciete grenzen BEHEER rate limiting, logzicht, korte reactietijden HOUDING begrijpen in plaats van overschrijven
Beveiliging is geen plaquette. Het is doorlopend onderhoudswerk.
[mindset/first]

Basishouding: begrijpen in plaats van kopiëren

De meest voorkomende misontwikkeling bij server-hardening is niet te weinig techniek, maar te weinig begrip. Men neemt configuratieblokken over uit forums, generatoren of blogposts, krijgt misschien een groen testrapport en merkt pas later dat de helft daarvan niet bij de eigen website past.

Een schone configuratie beantwoordt drie vragen: wat beschermt deze directive? Welk neveneffect heeft ze? Hoe controleer ik na een wijziging of ze nog correct werkt? Als één van die drie antwoorden ontbreekt, is de regel in de configuratie al verdacht.

[regels] > geen directive zonder onderbouwd doel > geen policy zonder browsertest en zicht op fouten > geen vertrouwen in A+, als de logica erachter onduidelijk blijft > geen externe afhankelijkheid zonder bewuste toestemming

„Een goede security-score is geen doel. Hij is alleen een nevenproduct van schone beslissingen.“

[transport/tls]

TLS: het transport moet eerst kloppen

TLS is de basis. Zonder schone transportversleuteling zijn alle latere headers slechts cosmetica. De eerste stap is daarom eenvoudig: HTTP consequent naar HTTPS omleiden en verouderde protocolgeneraties niet meer meeslepen.

  • HTTP → HTTPS: zonder uitzondering omleiden, niet optioneel aanbieden.
  • Alleen moderne protocollen: geen historische versies meer meeslepen alleen omdat enkele oude apparaten anders niet meer doorkomen.
  • Certificaten: volledige chain, automatische vernieuwing en controle na wijzigingen.
  • OCSP Stapling: zinvol, wanneer de server het schoon uitlevert en de resolver correct staat.
server { listen 80; listen [::]:80; server_name example.com www.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; }
[hsts/preload]

Als HSTS met preload wordt gebruikt, moeten includeSubDomains en een max-age van minstens één jaar aanwezig zijn. Dat vereist een werkelijk volledige HTTPS-discipline voor de hele domeinfamilie.

[policy/csp]

CSP: de eigenlijke grens tegen onnodig vertrouwen

Een Content-Security-Policy is alleen goed als ze bij de website past. Voor een puur statische pagina zonder externe scripts is de CSP prettig streng. Voor complexere pagina's moet ze bewust worden uitgebreid. Wat ik niet doe: een universele “op de een of andere manier groene” policy uit het internet overnemen.

Voor klassieke informatiepagina's is deze opbouw een schoon uitgangspunt:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;

Het verschil tussen een goede en een slechte CSP zit vaak in twee dingen: ten eerste het vermijden van gemakkelijke weekmakers zoals 'unsafe-inline', ten tweede eerlijke controle met Report-Only voordat er echt geblokkeerd wordt.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report;

Voor sterk statische pagina's kan in plaats van een nonce ook een hash voor kleine inline-scripts zinvol zijn. Voor dynamische toepassingen is een nonce vaak praktischer. Beide zijn schoner dan globale inline-vrijgaven.

[dom/trusted-types]

Trusted Types: alleen relevant waar DOM-sinks echt gebruikt worden

Trusted Types is geen algemene versiering voor elke website. Het is daar interessant waar JavaScript met riskante DOM-sinks zoals innerHTML werkt en waar men de stroom naar die sinks bewust wil controleren. Voor een puur statische website zonder zulke patronen is het niet de eerste hefboom.

Als het wordt ingezet, dan niet als simpele string-doorvoer, maar via een duidelijk gedefinieerde policy die HTML bewust verwerkt of sanitiseert.

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types ochsen-policy;
if (window.trustedTypes && trustedTypes.createPolicy) { const policy = trustedTypes.createPolicy("ochsen-policy", { createHTML: (input) => input.replace(/
[trusted-types]

Trusted Types vervangt geen schone CSP en ook geen inhoudelijke sanitization. Het is een extra blokkadelaag tegen DOM-gebaseerde XSS, niet de hele oplossing.

[operations/rate-limiting]

Rate limiting: kleine rem, groot verschil

Niet elke website heeft dezelfde hardheid nodig. Maar bijna elke publiek bereikbare website profiteert ervan wanneer individuele clients niet onbeperkt en zonder ritme mogen vuren. Voor login- of formulier-endpoints is dat verplicht. Voor statische websites is het op zijn minst een zinvolle filter tegen het alledaagse lawaai.

limit_req_zone $binary_remote_addr zone=allgemein:10m rate=30r/m; limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { limit_req zone=allgemein burst=10 nodelay; limit_conn conn_limit 20; }

Rate limiting vervangt geen toegangscontrole en ook geen Fail2Ban, maar het haalt druk uit veel banale aanvalspatronen en maakt logbeelden leesbaarder.

[operations/logs]

Logs: alleen nuttig als ze werkelijk gelezen worden

Security zonder zicht op logs is blind. Een scanner die 's nachts tien keer naar /.env, /wp-login.php of oude PHPMyAdmin-paden vraagt, is geen wereldramp. Maar je moet weten dat hij er was, hoe vaak hij opduikt en of het patroon verandert.

  • 404-clusters: tonen typisch scanverkeer en geven een gevoel voor het achtergrondlawaai.
  • 429-responses: verraden of rate limiting werkt of te zwak staat.
  • TLS-fouten: laten zien of verouderde clients, bots of configuratiefouten opduiken.
  • Ongewone user agents: zijn niet automatisch slecht, maar vaak een goed startpunt voor verdieping.
log_format security '$remote_addr - [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' '$request_time'; access_log /var/log/nginx/access.log security; error_log /var/log/nginx/error.log warn;

Tegelijk geldt: access-logs bevatten persoonsgegevens. Korte bewaartermijn, duidelijk doel en geen verzamelwoede.

[config/apache-nginx]

Apache en nginx: schone basisbouwstenen

Ik houd security-headers en TLS-parameters graag in herbruikbare snippets. Niet omdat modularisering er mooi uitziet, maar omdat onderhoudsfouten anders snel op meerdere hosts tegelijk uit elkaar lopen.

nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self), web-share=()" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always; server_tokens off;

Apache

<IfModule mod_headers.c> Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'" Header always set X-Frame-Options "DENY" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self), web-share=()" Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" </IfModule> ServerTokens Prod ServerSignature Off Protocols h2 http/1.1
[apache/http2]

Wie Apache met HTTP/2 correct wil draaien, zet dat niet alleen “gevoelsmatig”, maar expliciet via Protocols h2 http/1.1 en houdt de rest van de TLS-configuratie slank en begrijpelijk.

[operations/checkliste]

Checklist voor go-live of grotere wijziging

Transport

HTTPS actief
HTTP correct omgeleid
Certificaatketen volledig
Vernieuwing getest
OCSP Stapling gecontroleerd

Headers

HSTS bewust gezet
nosniff aanwezig
Referrer-Policy gezet
Permissions-Policy zinvol beperkt
X-Frame-Options of frame-ancestors gecontroleerd

CSP

geen onnodige wildcards
geen gedachteloos unsafe-inline
Report-Only vóór harde blokkade
Browserconsole zonder CSP-ruis

Beheer

Rate limiting actief
Logs leesbaar en geroteerd
Back-ups aanwezig
Updates planbaar
Test met echte browsers en echte requests

„Een veilige configuratie is niet de langste. Het is de configuratie die ook na maanden nog begrepen en onderhouden wordt.“

↑ Naar boven