From Grade B to Grade A:
How We Fixed NIS2 Compliance in 30 Minutes
We scanned our own websites with KENSAI and found critical NIS2 compliance gaps. Missing HTTP security headers brought two of our three domains to Grade B (0.786). Here's exactly what was wrong, what NIS2 articles were at risk, and how 30 minutes of Caddy configuration fixed everything.
The Uncomfortable Truth
We build a security scanning platform. We preach NIS2 compliance to our customers. And then we ran KENSAI on our own domains — and found that two out of three were non-compliant.
No vulnerabilities, no breaches. Just missing HTTP security headers — the kind of silent gap that regulators check first and attackers exploit second. Under the NIS2 Directive (Art. 21, §2), these headers aren't optional for regulated entities. They're a baseline hygiene requirement.
Here's what we found, and how fast we fixed it.
The Scan Results
We ran KENSAI's automated NIS2 compliance scan across all three portfolio domains on March 15, 2026:
kensai.app scored near-perfect at 0.997 because it was already running the correct Caddy header configuration. The other two domains — brnz.ai and codeforceai.com — were both at 0.786, each missing the same six critical security headers.
What Was Missing — and Why It Matters for NIS2
Six HTTP response headers were absent from both failing domains. Each one maps directly to NIS2 Directive requirements:
| Missing Header | Purpose | NIS2 Reference | Status |
|---|---|---|---|
Strict-Transport-Security |
Forces HTTPS, prevents protocol downgrade attacks | Art. 21(2)(h) — Secure communications | ✗ MISSING |
Content-Security-Policy |
Blocks XSS, code injection, data exfiltration | Art. 21(2)(d) — Supply chain security | ✗ MISSING |
X-Frame-Options |
Prevents clickjacking / UI redress attacks | Art. 21(2)(b) — Incident prevention | ✗ MISSING |
X-Content-Type-Options |
Stops MIME-sniffing attacks | Art. 21(2)(b) — Basic cyber hygiene | ✗ MISSING |
Referrer-Policy |
Controls referrer data leakage to third parties | Art. 21(2)(e) — Data security policies | ✗ MISSING |
Permissions-Policy |
Restricts browser API access (camera, mic, geolocation) | Art. 25 — Security by design | ✗ MISSING |
The Fix: Caddy Header Configuration
We use Caddy as our reverse proxy. The fix was a single, reusable header block that we could apply to all domains. Here's exactly what we added:
# Global header snippet — add to all production sites
(security_headers) {
header {
# Force HTTPS — NIS2 Art. 21(2)(h): secure communications
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Block XSS & injection — NIS2 Art. 21(2)(d): supply chain security
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'"
# Prevent clickjacking — NIS2 Art. 21(2)(b)
X-Frame-Options "DENY"
# Stop MIME-sniffing — NIS2 Art. 21(2)(b)
X-Content-Type-Options "nosniff"
# Limit referrer leakage — NIS2 Art. 21(2)(e)
Referrer-Policy "strict-origin-when-cross-origin"
# Restrict browser APIs — NIS2 Art. 25: security by design
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()"
# Remove server fingerprinting
-Server
-X-Powered-By
}
}
Then we imported the snippet into each site block:
# brnz.ai
brnz.ai, www.brnz.ai {
import security_headers
reverse_proxy localhost:3001
}
# codeforceai.com
codeforceai.com, www.codeforceai.com {
import security_headers
reverse_proxy localhost:3002
}
Reload Caddy, verify headers in browser DevTools, done:
caddy reload --config /etc/caddy/Caddyfile
The Timeline: 30 Minutes Start to Finish
Before vs. After
- HSTS missing
- CSP missing
- X-Frame-Options missing
- X-Content-Type-Options missing
- Referrer-Policy missing
- Permissions-Policy missing
- HSTS with preload
- CSP blocking injections
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin
- Permissions-Policy: locked down
Key Lessons
1. Security headers are NIS2 compliance items, not optional extras
Every NIS2-regulated entity is expected to have basic HTTP security headers in place. Regulators are increasingly running automated scans during audits. A missing Strict-Transport-Security header isn't a minor omission — it's evidence of inadequate risk management under Art. 21(2).
2. Even security companies have gaps
We build compliance tools and still had two Grade B sites. This isn't embarrassing — it's a reminder that configuration drift happens. A domain launched quickly, a Caddyfile that wasn't synced, a missing import. Automated scanning catches what manual audits miss.
3. The fix is almost always simple
Six missing headers. Thirty lines of Caddy configuration. Thirty minutes of work. The gap between Grade B and Grade A is not a month-long project — it's an afternoon. The hard part is knowing the gap exists in the first place.
4. Reusable snippets prevent future drift
By extracting the headers into a (security_headers) snippet, any new domain we add automatically gets full NIS2 header coverage by including a single import line. Compliance becomes the default, not an afterthought.
What This Means for Your Organisation
If you're under NIS2 — or preparing for an audit — your HTTP security headers are one of the first things regulators check. They're fast to scan, unambiguous to evaluate, and trivial to fix. There's no excuse for a Grade B when a Grade A takes 30 minutes.
Run the scan. Read the report. Fix what's flagged. Re-scan to confirm.
Check Your NIS2 Compliance — Free
Run the same scan we ran on our own domains. Get your NIS2 compliance grade, see exactly which headers are missing, and get a remediation report you can act on immediately.
Start Free NIS2 Scan →