How I Hardened My Web App's Security Headers Using Nuclei and Automated Scanning
A practical walkthrough of scanning, finding, and fixing HTTP security misconfigurations on Nginx and Vercel

You've deployed your app. SSL is green. Authentication works. Everything looks secure — until you actually scan it.
I run a development studio with a Next.js frontend behind Nginx on EC2 and a marketing landing page on Vercel. Both were serving traffic over HTTPS with valid certificates. But when I pointed an automated Nuclei scanner at my own domains, it came back with 54 findings. No critical vulnerabilities, but a stack of missing security headers and information leaks that any attacker could use for reconnaissance.
In this article, I'll walk through the exact findings, why they matter, and the specific configuration changes I made to fix them. If you're running a similar stack, you can apply these fixes in a single deployment.
The Setup: Automated Scanning with Nuclei and n8n
Before diving into findings, here's how the scan ran. I have an n8n workflow on my homelab that triggers Nuclei scans against my domains on a schedule. Nuclei is an open-source vulnerability scanner that uses YAML templates to check for thousands of known misconfigurations, CVEs, and information disclosures.
The scan targeted two domains:
- studio.example.com — Next.js + Django behind Nginx on EC2
- example.com — Next.js landing page on Vercel
The results came back as JSONL — one JSON object per finding. 54 total, all classified as "Info" severity. No critical or high findings, which is good. But "Info" doesn't mean "ignore."
What the Scan Found
The findings fell into four categories:
1. Missing HTTP Security Headers (Highest Impact)
Both domains were missing several defense-in-depth headers. The Nginx server had only four security headers configured:
# What I had before
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
Missing entirely: Content-Security-Policy, Permissions-Policy, Referrer-Policy, Cross-Origin-Resource-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, and X-Permitted-Cross-Domain-Policies.
2. Nginx Version Disclosure
The Server header was broadcasting nginx/1.29.4. This gives attackers a specific version to search for CVEs against.
3. Deprecated X-XSS-Protection Header
The X-XSS-Protection: 1; mode=block header is not just outdated — it can actually introduce XSS vulnerabilities in older browsers. Modern browsers have removed the XSS auditor entirely. The correct value is 0, and the real protection comes from Content-Security-Policy.
4. SSH and DNS Issues
The scan also flagged SHA-1 HMAC algorithms enabled on SSH, an outdated OpenSSH version, a weak DMARC policy (p=none), and missing DNSSEC. These are server-level and DNS-level fixes, not application config.
Fixing the Nginx Configuration
Hiding the Version Number
In nginx.conf, one line inside the http block:
http {
server_tokens off;
# ... rest of config
}
Before: Server: nginx/1.29.4
After: Server: nginx
Adding Security Headers
In the Nginx server block (default.conf), I replaced the old four headers with a comprehensive set:
server {
listen 443 ssl http2;
server_name studio.example.com;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "0" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://studio.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
# Permissions Policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
# Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cross-Origin Policies
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Block Flash/PDF cross-domain policies
add_header X-Permitted-Cross-Domain-Policies "none" always;
# ... rest of server config
}
Key changes from the original:
- X-Frame-Options changed from
SAMEORIGINtoDENY— the app doesn't need to be framed anywhere - X-XSS-Protection changed from
1; mode=blockto0— deprecated header disabled - HSTS added
preloaddirective for HSTS preload list eligibility - Seven new headers added for CSP, permissions, referrer control, and cross-origin isolation
Fixing the Vercel Landing Page
For the Next.js site on Vercel, security headers go in next.config.ts:
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-XSS-Protection", value: "0" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" },
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"base-uri 'self'",
"img-src 'self' https: data:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self' https: data:",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"connect-src 'self' https://studio.example.com",
"frame-ancestors 'none'",
"object-src 'none'",
"form-action 'self'",
].join("; "),
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Resource-Policy", value: "same-origin" },
{ key: "X-Permitted-Cross-Domain-Policies", value: "none" },
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: securityHeaders,
},
];
},
};
One thing to note with Vercel: if your apex domain (example.com) redirects to www.example.com, the custom headers only apply on the final destination, not on the 308 redirect response. This is expected behavior.
What Each Header Actually Does
If you're adding these headers, you should understand what they protect against:
| Header | What It Prevents |
|---|---|
| Content-Security-Policy | XSS attacks, code injection, data exfiltration. Defines exactly which sources can load scripts, styles, images, etc. |
| Permissions-Policy | Blocks browser APIs you don't use (camera, microphone, geolocation) so compromised scripts can't access them. |
| Referrer-Policy | Controls how much URL information leaks when users click links to external sites. Prevents token/path leakage. |
| Cross-Origin-Resource-Policy | Prevents other sites from loading your resources, protecting against data theft. |
| Cross-Origin-Embedder-Policy | Required for SharedArrayBuffer and cross-origin isolation. Mitigates Spectre-type side-channel attacks. |
| Cross-Origin-Opener-Policy | Prevents other sites from gaining a reference to your window via popups. |
| X-Permitted-Cross-Domain-Policies | Blocks legacy Flash and PDF plugins from making cross-domain requests. |
Verification
After deploying, a quick curl confirms everything is in place:
curl -I https://studio.example.com 2>/dev/null | grep -iE "content-security|permissions|referrer|cross-origin|x-xss|server|x-frame"
server: nginx
x-frame-options: DENY
x-xss-protection: 0
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' ...
permissions-policy: camera=(), microphone=(), geolocation=(), payment=() ...
referrer-policy: strict-origin-when-cross-origin
cross-origin-resource-policy: same-origin
cross-origin-embedder-policy: require-corp
cross-origin-opener-policy: same-origin
No version number in the Server header. All security headers present. The X-XSS-Protection correctly set to 0.
Hardening robots.txt
While fixing headers, I also tightened the robots.txt files. The studio app was allowing crawlers to index everything including /api/, /admin/, and /django-admin/. The fix:
User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Disallow: /django-admin/
Disallow: /_next/
Disallow: /static/admin/
For the landing page, I went further and blocked AI training bots (GPTBot, CCBot, anthropic-ai, Google-Extended, Bytespider) and aggressive SEO crawlers (AhrefsBot, SemrushBot, MJ12bot) while keeping Google and Bing access to public pages.
What's Left
Not everything can be fixed in application config:
- SSH hardening — Disable SHA-1 HMAC algorithms and update OpenSSH on the server
- DMARC policy — Gradually tighten from
p=nonetop=quarantinetop=rejectafter monitoring reports - DNSSEC — Enable at the domain registrar level
These are operational tasks that require direct server access and DNS management, not code deployments.
Key Takeaways
-
Scan your own infrastructure regularly. Automated tools like Nuclei find things you'd never check manually. Set up a recurring scan with n8n or cron.
-
"Info" severity doesn't mean harmless. Missing security headers are classified as informational, but they're the difference between a hardened app and one that's easy to attack.
-
The deprecated X-XSS-Protection header can make things worse. If you still have
1; mode=block, change it to0or remove it. Use Content-Security-Policy instead. -
Hide version numbers.
server_tokens offin Nginx is one line that removes a free reconnaissance data point for attackers. -
Different platforms need different approaches. Nginx uses
add_headerdirectives. Vercel/Next.js uses theheaders()function in config. The headers themselves are the same. -
Block what you don't need. Permissions-Policy disabling unused browser APIs, robots.txt blocking unnecessary crawlers — both reduce your attack surface with minimal effort.
Liban Abdullahi is the founder of CraftRipple Studio, a Brussels-based development studio specialising in secure full-stack web applications.
Work with CraftRipple Studio if you need security built into your application from day one.