WordPress security headers configureren: A+ zonder brekage

Van F naar A+ op securityheaders.com zonder dat de admin, popup of checkout stuk gaat. Report-Only eerst, per hostingpaneel, met exacte syntax.

· 10 september, 2026 · 17 min lezen
In het kort
  • WordPress security headers configureren lukt zonder brekage als je de CSP eerst in Report-Only zet en hem pas na twee weken violation-logs afdwingt.
  • De weg van F naar A+ loopt in zeven stappen: baseline meten, Report-Only aanzetten, logs verzamelen, policy schrijven, enforcen en opnieuw scannen.
  • Zet de headers op serverniveau in .htaccess, nginx of Cloudflare Transform Rules, want daar overleven ze een plugin die je .htaccess herschrijft.
  • De grootste valkuil is direct enforcen: CSP blokkeert dan de inline scripts van wp-admin en Gutenberg, en dan zit je zelf buiten je eigen beheer.
In dit artikel

WordPress security headers configureren is een klus die er op papier simpel uitziet, maar in de praktijk verraderlijk is. Een klant uit Middelburg mailde me vorige maand: securityheaders.com gaf een F, en in Google Search Console verscheen een waarschuwing over ontbrekende Content-Security-Policy.

Zijn vorige “WordPress hardening”-script had zomaar een strenge CSP toegevoegd. Gutenberg-blocks en Google Tag Manager laadden niet meer, en de admin was voor de helft wit. We hebben het teruggedraaid en opnieuw opgebouwd, in Report-Only, met precisie.

Reken voor een volledige, veilige implementatie op 2 tot 4 uur werk. Spreid dat over 1 tot 2 weken, zodat je genoeg CSP-violation-logs verzamelt. In dit stuk lees je hoe ik dat systematisch doe, met exacte syntax per hostingpaneel en concrete workarounds voor de plekken waar het meestal misgaat.

WordPress security headers configureren A+ scan

WordPress security headers configureren: de vijf must-haves

Voordat je iets configureert, wil je begrijpen wat elke header oplost. Security headers zijn korte HTTP-response-instructies die de browser vertellen wat er wel en niet mag op jouw pagina. Ze compenseren voor kwetsbaarheden in themes, plugins en third-party scripts. Het OWASP Secure Headers Project publiceert de canonieke baseline, en die is in 2026 nog altijd de referentie voor productiesites.

De vijf headers die ik in elke WordPress-implementatie neerzet:

  1. Content-Security-Policy (CSP). Blokkeert cross-site scripting (XSS) en data-injection door precies te definiëren welke scripts, stijlen, images en frames de browser mag laden. Dit is verreweg de krachtigste header en tegelijk degene die het vaakst dingen breekt.
  2. X-Frame-Options. Voorkomt dat andere sites jouw pagina in een iframe embedden voor clickjacking-aanvallen. In moderne setups vervangt CSP’s frame-ancestors-directive deze header, maar veel oudere browsers (en scanning tools) verwachten hem nog. Ik zet beide.
  3. X-Content-Type-Options: nosniff. Blokkeert MIME-sniffing, waarbij de browser probeert te “raden” wat een bestand is. Zonder deze header kan een geüpload .txt-bestand met JavaScript-inhoud alsnog als script uitgevoerd worden. Er is maar één juiste waarde: nosniff.
  4. Referrer-Policy. Bepaalt hoeveel URL-informatie meegaat wanneer een bezoeker via een link jouw site verlaat. Zonder policy lekt de volledige URL (inclusief sessie-tokens in queryparameters) naar externe partijen.
  5. Permissions-Policy. Deze vervangt de oude Feature-Policy. Hij regelt welke browser-APIs (camera, microfoon, geolocatie, USB, payment) een pagina mag aanspreken. Voor een reguliere WordPress-site zet je vrijwel alles uit. Zie de MDN Permissions-Policy referentie voor de actuele directive-lijst.

HSTS (Strict-Transport-Security) hoort er ook bij, maar dat is een apart hoofdstuk met eigen preload-lijst en voorwaarden. Daar heb ik een aparte gids voor HSTS Preload op WordPress.

Vergelijkingstabel: syntax per header

HeaderFunctieSimpel syntaxAdvanced syntax
Content-Security-PolicyBlokkeert XSS en injectiedefault-src 'self'default-src 'self'; script-src 'self' 'nonce-{random}' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'self'; report-to csp-endpoint
X-Frame-OptionsClickjacking-preventieSAMEORIGINDENY (als je site nooit in eigen iframes hoeft)
X-Content-Type-OptionsMIME-sniffing blokkerennosniffn/a (één geldige waarde)
Referrer-PolicyPrivacylek beperkenstrict-origin-when-cross-originno-referrer-when-downgrade (marketing-vriendelijker) of no-referrer (strengst)
Permissions-PolicyCamera/mic/geo/USB gatecamera=(), microphone=(), geolocation=()camera=(), microphone=(), geolocation=(self), payment=(self "https://checkout.mollie.com"), interest-cohort=()

De keuze tussen “simpel” en “advanced” hangt af van je stack. Een brochuresite zonder formulieren komt weg met de simpele varianten. Een WooCommerce-site met Mollie, GTM en Elementor Pro heeft de advanced kolom nodig om A+ te halen zonder brekage.

Bouw je WordPress security headers-config

Kies je headers en platform. Je krijgt direct een kopieerbaar startconfig met waarschuwingen voor HSTS en CSP.

1. Kies je headers

De vijf basisheaders staan alvast aan. Permissions-Policy is optioneel.

Headers opnemen
2. Kies je platform
5 headers geselecteerd Apache-config met Header always set.
3. Jouw headers-config

Controleer bestaande headers eerst. Dubbele regels kunnen onverwachte uitkomsten geven.

Volgende stap Voeg dit toe op staging, controleer de response headers en bekijk eerst de CSP-rapporten.
Test bij Security Headers

Een startpunt, geen kant-en-klare productieconfig. Test elke wijziging en controleer de site voordat je CSP enforcement aanzet.

De 7-stap roadmap: van F naar A+ zonder brokstukken

Dit is de volgorde die ik altijd aanhoud. Doe geen enkele stap over of door elkaar.

  1. Baseline meten (5 minuten). Ga naar securityheaders.com, voer je domein in, screenshot de F- of D-score. Doe direct daarna een tweede scan op Mozilla Observatory. Twee referentiepunten, want ze wegen headers net anders.
  2. Report-Only mode inschakelen (15 minuten). Voeg alle vijf headers toe, maar de CSP als Content-Security-Policy-Report-Only. In deze mode blokkeert de browser niks; hij stuurt alleen rapportages over wat er geblokkeerd zou zijn. Zo zie je de brekage aankomen zonder impact op bezoekers.
  3. Logs verzamelen 1 tot 2 weken. Laat de site draaien. Verzamel de violation-reports via een report-endpoint (report-uri.com heeft een gratis tier) of via een simpel eigen PHP-endpoint dat naar een logbestand schrijft.
  4. Analyseer de violations. Filter op unieke blocked-uri. Meestal zie je 5 tot 20 unieke domains die je onbedoeld blokkeert: Google Fonts, GTM, Facebook Pixel, YouTube-embed, Stripe, Mollie, je CDN. Elk hoort in de policy of moet weg.
  5. Custom CSP schrijven met specifieke domains. Bouw je policy op basis van de logs. Toets hem tegen Google's CSP Evaluator. Die tool markeert onveilige patronen zoals 'unsafe-inline' zonder nonce, of te brede wildcards.
  6. Report-Only naar Enforce switchen. Verander de header-naam van Content-Security-Policy-Report-Only naar Content-Security-Policy. Vanaf dat moment blokkeert de browser echt. Doe dit buiten kantooruren, en houd de eerste 24 uur violation-logs strak in de gaten.
  7. Post-implementation verify (10 minuten). Nieuwe scan op securityheaders.com. Als je alle vijf headers goed hebt en HSTS eronder, staat er A of A+. Screenshot als bewijsstuk voor je klant of voor je jaarrapport.

Rush geen enkele stap. Ik heb sites gezien die op stap 6 in vier uur alle inline JavaScript van Gutenberg blokkeerden, waardoor de editor voor redacteuren onbruikbaar werd. De Report-Only-fase van 1 tot 2 weken bestaat juist om dat te voorkomen.

Per hostingpaneel: waar zet je de headers neer

De headers moeten op server- of platformniveau. In principe kan het ook via een WordPress plugin (zie verderop), maar server-level is stabieler omdat je headers dan ook stuurt bij statische bestanden en 404's.

Overzicht per paneel

PaneelConfiguratielocatieBestand of UIReload nodig
cPanel (Apache/LiteSpeed)Document root van de site.htaccessNee (direct actief)
PleskDomain settingsApache & nginx Settings → Additional nginx directivesAuto-reload nginx
DirectAdminPer-domein config templatecustom_httpd.conf of .htaccessApache/LiteSpeed restart
Hostinger hPanelFile Manager.htaccess in public_htmlNee
CloudflareDashboardRules → Transform Rules → Response HeaderNee (paar seconden)
RunCloud/GridPaneWeb application settingsNginx custom configAuto-reload nginx
WordPress fallbackSite zelfmu-plugins/security-headers.phpNee

cPanel .htaccess (Antagonist, Vimexx, Hostnet, Argeweb)

cPanel-hostings draaien meestal Apache of LiteSpeed. Beide lezen .htaccess.

  1. Log in op cPanel → File Manager.
  2. Navigeer naar public_html/ (of de subfolder van je site).
  3. Open .htaccess. Als hij verborgen is: rechtsboven Settings → "Show Hidden Files" aanvinken.
  4. Voeg dit blok toe boven de # BEGIN WordPress-regel:
<IfModule mod_headers.c>
    # De vier veilige headers direct enforce
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)"

    # CSP eerst als Report-Only voor 2 weken
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; frame-ancestors 'self'; report-uri /csp-endpoint.php"
</IfModule>
  1. Bewaar en refresh de site.
  2. Test met curl -I https://jouwsite.nl in een terminal, of gebruik https://securityheaders.com. Je moet alle vijf headers in de respons zien.

Plesk Additional nginx directives (TransIP zakelijk, Combell)

Plesk zet nginx voor Apache als reverse-proxy. Headers zet je in het Plesk-panel zelf, dan wordt nginx opnieuw geladen.

  1. Domains → jouwsite.nl → Apache & nginx Settings.
  2. Scroll naar Additional nginx directives.
  3. Plak in:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; report-uri /csp-endpoint.php" always;
  1. Apply onderaan de pagina. Plesk herlaadt nginx automatisch.
  2. Test opnieuw via securityheaders.com.

Let op: de always-parameter is belangrijk. Zonder hem stuurt nginx de header niet mee bij 4xx/5xx-responses, wat op securityheaders.com tot puntenverlies leidt bij deep scans.

DirectAdmin custom_httpd (Neostrada, Argeweb-oud)

DirectAdmin gebruikt template-bestanden voor Apache/OpenLiteSpeed. Voor per-domein overrides zet je een custom_httpd config aan.

  1. SSH-toegang aanvragen bij je hoster, of via Advanced FeaturesFile Manager in DirectAdmin.
  2. Navigeer naar /usr/local/directadmin/data/users/{gebruikersnaam}/domains/{domein}.conf/.
  3. Maak (of open) custom_httpd.conf voor de VirtualHost en voeg toe:
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)"
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-endpoint.php"
</IfModule>
  1. Rebuild de config: directadmin --build all (root-only). Of via DirectAdmin: Server ManagerCustom HTTPD Configurations → Save.
  2. Herstart Apache/LiteSpeed via het paneel.

Als je géén SSH hebt op DirectAdmin, gebruik dan de .htaccess-route zoals bij cPanel. DirectAdmin ondersteunt dat ook.

Cloudflare Transform Rules → Rewrite Response Headers

Als je site achter Cloudflare zit, is dit de simpelste plek. Je hoeft niks op je server aan te passen, en je kunt regels per URL-patroon maken (bijvoorbeeld strengere CSP voor /wp-admin/* uit).

  1. Log in op Cloudflare dashboard → kies je zone.
  2. RulesOverviewCreate ruleResponse Header Transform Rule.
  3. Rule name: "Security headers baseline".
  4. When incoming requests match: kies "All incoming requests" (of filter op hostname als je meerdere zones hebt).
  5. Then: klik Modify response header → Operation: Set static → Header name en Value invullen. Voeg voor elk van de vijf headers een aparte "Modify" toe.
  • Set: X-Content-Type-Options = nosniff.
  • Set: X-Frame-Options = SAMEORIGIN.
  • Set: Referrer-Policy = strict-origin-when-cross-origin.
  • Set: Permissions-Policy = camera=(), microphone=(), geolocation=(self).
  • Set: Content-Security-Policy-Report-Only = je policy-string.
  1. Deploy. Werkt binnen enkele seconden globaal.

Cloudflare heeft daarnaast Managed Transforms onder Rules → Managed Transforms met een "Add security headers"-preset. Handig voor snel starten, maar je hebt geen controle over de CSP. Zelf schrijven blijft aan te raden. Zie de Cloudflare docs voor Response Header Transform Rules voor de meest actuele UI.

WP-CLI + functions.php als noodoplossing

Als je géén server-toegang hebt en geen Cloudflare, kun je headers via WordPress zelf sturen. Nadeel: alleen bij PHP-responses, dus niet bij statische assets of 404's van niet-WordPress-bestanden. Toch werkt het voor de meeste scanners.

Voeg toe aan wp-content/themes/{child-theme}/functions.php, of nog beter, in een mu-plugins/security-headers.php:

<?php
add_action('send_headers', function() {
    if (!headers_sent()) {
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: SAMEORIGIN');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        header('Permissions-Policy: camera=(), microphone=(), geolocation=(self)');
        header("Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; report-uri /csp-endpoint.php");
    }
});

Deployen via WP-CLI vanaf je lokale machine (of via SSH):

# Bevestig dat de mu-plugin actief is
wp plugin list --status=must-use

# Test dat de header binnenkomt
wp eval 'wp_remote_get(home_url()); print_r(wp_remote_retrieve_headers(wp_remote_get(home_url())));'

Voor productie is dit de minst nette oplossing. Server-level of Cloudflare heeft mijn voorkeur. De WordPress-route is prima voor development of als tijdelijke fix terwijl je op je hoster wacht.

Cloudflare Transform Rules met vijf headers

Wat breekt CSP het vaakst (en hoe je het fixt)

CSP breekt vaak omdat WordPress zwaar leunt op inline styles en inline scripts. Daar komen de third-party CDN's van het ecosysteem nog bij. Onderstaande tabel toont de patronen die ik 90% van de tijd tegenkom.

ElementWaarom breekt CSPFix
Inline -blok zonder externe srcVoeg 'unsafe-inline' toe (zwak) of implementeer nonces via wp_get_script_tag()-filter (sterk)
Google Fontsfonts.googleapis.com (CSS) en fonts.gstatic.com (WOFF) zijn twee aparte domainsstyle-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com
Google Tag ManagerGTM injecteert extra scripts runtime van variabele hostnamesscript-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com https://analytics.google.com
Google AdSenseAds laden via ep2.adtrafficquality.google en andere subdomainsscript-src 'self' https://pagead2.googlesyndication.com https://*.googlesyndication.com; frame-src https://googleads.g.doubleclick.net
Gutenberg blocks met inline stylesBlock-editor injecteert style="..." attributes op elementenstyle-src 'self' 'unsafe-inline' blijft nodig totdat WP core nonces uitrolt (Core issue #57381)
YouTube embedyoutube.com/embed/* iframesframe-src https://www.youtube.com https://www.youtube-nocookie.com
Mollie/Stripe checkoutPayment iframes en JSscript-src ... https://js.stripe.com https://www.mollie.com; frame-src https://js.stripe.com https://www.mollie.com

Elke keer dat je een nieuwe integratie toevoegt (nieuwsbrief-widget, chatbot, webinar-tool), scan je opnieuw en verleng je de policy. Dat is de reden dat CSP een levend document is en geen one-shot config.

Drie concrete scenario's uit de praktijk

Scenario A: A+ score halen zonder Cloudflare

Klant in Terneuzen met een simpele brochuresite op Antagonist. Geen CDN, geen GTM, alleen Google Fonts en een Contact Form 7-formulier.

Aanpak:

  1. cPanel → .htaccess → vijf headers toevoegen. CSP eerst Report-Only.
  2. Report-Only twee weken laten draaien. Enige violation was Google Fonts (verwacht).
  3. CSP naar enforce, met default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; frame-ancestors 'self'.
  4. HSTS al aan sinds de SSL-uitrol vorig jaar.
  5. Eindresultaat: A+ op securityheaders.com. Doorlooptijd 2,5 uur verspreid over 15 dagen.

Scenario B: WordPress admin-toolbar breekt na CSP enable

Bij een webshop in Hulst na de Report-Only-naar-Enforce switch: de zwarte toolbar bovenaan wp-admin renderde niet meer, plus de Gutenberg-editor toonde alleen een spinner.

Diagnose: In browserconsole: Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". WordPress core injecteert honderden inline scripts in de admin.

Fix: Uitzondering voor /wp-admin/*. In .htaccess:

<IfModule mod_headers.c>
    # Standaard strenge CSP
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'"

    # Ruimere CSP alleen voor wp-admin
    <If "%{REQUEST_URI} =~ m#^/wp-admin/#">
        Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
    </If>
</IfModule>

Voor Cloudflare-users: maak een tweede Transform Rule met filter URI Path starts with /wp-admin/ en een lossere CSP-waarde. Frontend blijft streng, admin blijft werken.

Scenario C: Elementor of WPBakery inline styles versus CSP

Elementor Pro-site met een strenge policy: style-src 'self' (zonder 'unsafe-inline'). Resultaat: alle Elementor-widgets kwijt hun styling. Pagina's zagen eruit als 1998.

Waarom: Elementor genereert per-widget inline styles op

attributen. Nonces werken niet voor style-attributes, alleen voor