{"schemaVersion":1,"id":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers","title":"Defense-in-Depth HTTP Security Headers","description":"HTTP security headers are the first layer of defense against common web attacks: XSS (Cross-Site Scripting), clickjacking, MIME-type sniffing, and information l","language":"en","datePublished":null,"dateModified":null,"contentFormat":"text/markdown","content":"ADR-017accepted25 de marzo de 2024\n\n# Defense-in-Depth HTTP Security Headers\n\n## Contexto\n\nHTTP security headers are the first layer of defense against common web attacks: XSS (Cross-Site Scripting), clickjacking, MIME-type sniffing, and information leakage. Next.js provides no security headers by default, the developer must explicitly configure them in next.config.js. A portfolio site has a smaller attack surface than an authenticated application, but it's still publicly accessible and serves as a professional representation. A security vulnerability on a CTO's portfolio site would be particularly ironic and reputationally damaging. The security headers must balance protection against real-world attack vectors with compatibility across browsers and embedded contexts (social media previews, link unfurling bots, search engine rendering).\n\n## Decisión\n\nConfigure seven security headers applied to all routes (/:path\\*) via next.config.js async headers. X-DNS-Prefetch-Control: on, enables DNS prefetching for faster navigation to linked domains. Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, enforces HTTPS for 2 years across all subdomains, with HSTS preload eligibility. X-Content-Type-Options: nosniff, prevents MIME-type sniffing attacks where browsers reinterpret response content types. X-Frame-Options: SAMEORIGIN, prevents clickjacking by disallowing iframe embedding from other origins while allowing same-origin embedding. X-XSS-Protection: 1; mode=block, enables legacy XSS filter in older browsers (modern browsers use CSP instead, but this provides backward compatibility). Referrer-Policy: strict-origin-when-cross-origin, sends full referrer for same-origin requests, origin-only for cross-origin requests, protecting URL paths from leaking to external sites. Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), explicitly disables unnecessary browser APIs and opts out of FLoC/Topics API tracking.\n\n## Consecuencias\n\nPositive: SecurityHeaders.com score of A+. HSTS preload eligibility protects against SSL stripping attacks and ensures all requests use HTTPS after the first visit. nosniff prevents a class of attacks where malicious content disguises its MIME type. SAMEORIGIN frame protection prevents the site from being embedded in phishing pages while allowing legitimate same-origin embedding. The Permissions-Policy explicitly opts out of surveillance capitalism APIs (FLoC/Topics), a principled stance for a privacy-aware technologist. The configuration is applied globally via next.config.js, so new pages automatically inherit all headers. Negative: X-Frame-Options: SAMEORIGIN prevents the site from being embedded in third-party tools that might legitimately want to preview it (e.g., link unfurling services that render in iframes). However, most modern unfurling services use server-side rendering, so this is rarely an issue. The HSTS preload submission is irreversible at the browser level, once a domain is on the preload list, removing HTTPS support would break the site for all users with updated preload lists. The X-XSS-Protection header is technically deprecated in modern browsers and can cause false positives in edge cases, but the mode=block directive minimizes this risk. No Content-Security-Policy (CSP) header is configured, this is a deliberate omission because styled-components generates inline styles that would require 'unsafe-inline' in the CSP, negating much of its XSS protection value.\n\n## Incertidumbre Calibrada\n\n## Predicciones al Momento de la Decisión\n\nExpected a SecurityHeaders.com score of A or higher. Predicted the headers would not break any legitimate functionality (social media previews, search engine rendering, browser compatibility). Assumed the missing CSP header would not be a significant security gap for a static portfolio without user-generated content. Predicted the HSTS preload commitment would be acceptable given no plans to ever revert to HTTP.\n\n## Resultados Medidos\n\nSecurityHeaders.com score is A+ as targeted. No legitimate functionality has been broken, social media previews (Twitter, LinkedIn, Slack) work correctly because they use server-side rendering for unfurling, not iframe embedding. Search engines render the site correctly despite the headers. The missing CSP has not been exploited (no XSS vectors exist on a static site without user input), confirming the prediction. HSTS preload commitment is comfortable, there is zero scenario where the site would revert to HTTP. The unexpected observation: the Permissions-Policy header disabling interest-cohort=() is now partially redundant because Google deprecated FLoC in favor of Topics API, but the header still serves as a documented privacy stance.\n\n## Incógnitas al Momento de la Decisión\n\nDid not know at decision time whether CSP nonce-based approach would become compatible with styled-components' runtime injection model. As of early 2026, styled-components v6 has experimental CSP nonce support, but the portfolio uses v5.3 and migration would require significant effort. Also unknown: whether X-XSS-Protection would be removed from browser implementations entirely, potentially causing unexpected behavior. Chrome has already removed it, but the header is harmless when unsupported. Unknown: whether future portfolio features (comment system, contact form with file upload) would require relaxing any of these headers.\n\n## Clasificación de Reversibilidad\n\nPuerta de Ida y Vuelta\n\nEach header is independently removable by editing the securityHeaders array in next.config.js. Adding, modifying, or removing individual headers requires a single line change plus redeploy. The only exception is HSTS preload: once submitted to the preload list, removal requires a formal delisting process that takes months. However, the HSTS header itself can be removed immediately, the preload list effect persists independently in browser distributions. Estimated effort: 5 minutes for any header change.\n\n## Contra-Argumento Más Fuerte\n\nFor a static portfolio site with zero user input and no authentication, the security header configuration is over-engineered. The actual attack surface is minimal: there are no forms to exploit (except a contact form that submits to a third-party), no cookies to steal, no sessions to hijack. The headers protect against theoretical attacks on a site with no practical attack vectors. A simpler approach: rely on Vercel's default headers and Next.js's built-in protections. The counter-counter: security headers are cheap insurance, the configuration took 30 minutes to implement and adds zero runtime cost. More importantly, for a CTO's portfolio, demonstrating security awareness through headers is a professional signal, not just a technical measure.\n\n## Contexto Técnico\n\nStack\n\n- next.config.js\n- HTTP Headers\n- HSTS\n- Permissions-Policy\n\nSecurity Headers Score\n\nA+\n\nHeaders Configured\n\n7\n\nHsts Max Age\n\n63072000 seconds (2 years)\n\nDisabled Browser Apis\n\n4\n\nRestricciones\n\n- No CSP due to styled-components inline styles\n- HSTS preload is effectively irreversible\n- X-Frame-Options may block legitimate embedding\n\n## Decisiones Relacionadas\n\n- [ADR-012accepted\n\n  Deliberate AI Crawler Access Policy over Default Blocking](<https://lucioduran.com/es/decisions/deliberate-ai-crawler-access-policy>)\n- [ADR-015accepted\n\n  Critical Rendering Path Optimization Strategy](<https://lucioduran.com/es/decisions/critical-rendering-path-optimization>)\n- [ADR-016accepted\n\n  Scoped Remote Image Patterns over Generic CDN Proxy](<https://lucioduran.com/es/decisions/scoped-remote-image-patterns>)\n\n[Volver a Decisiones](<https://lucioduran.com/es/decisions>)","headings":[{"level":1,"text":"Defense-in-Depth HTTP Security Headers","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#title"},{"level":2,"text":"Contexto","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-context"},{"level":2,"text":"Decisión","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-decision"},{"level":2,"text":"Consecuencias","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-consequences"},{"level":2,"text":"Incertidumbre Calibrada","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-uncertainty"},{"level":2,"text":"Predicciones al Momento de la Decisión","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-predictions"},{"level":2,"text":"Resultados Medidos","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-outcomes"},{"level":2,"text":"Incógnitas al Momento de la Decisión","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-unknowns"},{"level":2,"text":"Clasificación de Reversibilidad","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-reversibility"},{"level":2,"text":"Contra-Argumento Más Fuerte","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-counterArgument"},{"level":2,"text":"Contexto Técnico","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-techContext"},{"level":2,"text":"Decisiones Relacionadas","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-relatedDecisions"}],"sections":[{"heading":"Defense-in-Depth HTTP Security Headers","level":1,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers","content":"ADR-017accepted25 de marzo de 2024"},{"heading":"Defense-in-Depth HTTP Security Headers","level":1,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#title","content":""},{"heading":"Contexto","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-context","content":"HTTP security headers are the first layer of defense against common web attacks: XSS (Cross-Site Scripting), clickjacking, MIME-type sniffing, and information leakage. Next.js provides no security headers by default, the developer must explicitly configure them in next.config.js. A portfolio site has a smaller attack surface than an authenticated application, but it's still publicly accessible and serves as a professional representation. A security vulnerability on a CTO's portfolio site would be particularly ironic and reputationally damaging. The security headers must balance protection against real-world attack vectors with compatibility across browsers and embedded contexts (social media previews, link unfurling bots, search engine rendering)."},{"heading":"Decisión","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-decision","content":"Configure seven security headers applied to all routes (/:path\\*) via next.config.js async headers. X-DNS-Prefetch-Control: on, enables DNS prefetching for faster navigation to linked domains. Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, enforces HTTPS for 2 years across all subdomains, with HSTS preload eligibility. X-Content-Type-Options: nosniff, prevents MIME-type sniffing attacks where browsers reinterpret response content types. X-Frame-Options: SAMEORIGIN, prevents clickjacking by disallowing iframe embedding from other origins while allowing same-origin embedding. X-XSS-Protection: 1; mode=block, enables legacy XSS filter in older browsers (modern browsers use CSP instead, but this provides backward compatibility). Referrer-Policy: strict-origin-when-cross-origin, sends full referrer for same-origin requests, origin-only for cross-origin requests, protecting URL paths from leaking to external sites. Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), explicitly disables unnecessary browser APIs and opts out of FLoC/Topics API tracking."},{"heading":"Consecuencias","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-consequences","content":"Positive: SecurityHeaders.com score of A+. HSTS preload eligibility protects against SSL stripping attacks and ensures all requests use HTTPS after the first visit. nosniff prevents a class of attacks where malicious content disguises its MIME type. SAMEORIGIN frame protection prevents the site from being embedded in phishing pages while allowing legitimate same-origin embedding. The Permissions-Policy explicitly opts out of surveillance capitalism APIs (FLoC/Topics), a principled stance for a privacy-aware technologist. The configuration is applied globally via next.config.js, so new pages automatically inherit all headers. Negative: X-Frame-Options: SAMEORIGIN prevents the site from being embedded in third-party tools that might legitimately want to preview it (e.g., link unfurling services that render in iframes). However, most modern unfurling services use server-side rendering, so this is rarely an issue. The HSTS preload submission is irreversible at the browser level, once a domain is on the preload list, removing HTTPS support would break the site for all users with updated preload lists. The X-XSS-Protection header is technically deprecated in modern browsers and can cause false positives in edge cases, but the mode=block directive minimizes this risk. No Content-Security-Policy (CSP) header is configured, this is a deliberate omission because styled-components generates inline styles that would require 'unsafe-inline' in the CSP, negating much of its XSS protection value."},{"heading":"Incertidumbre Calibrada","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-uncertainty","content":""},{"heading":"Predicciones al Momento de la Decisión","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-predictions","content":"Expected a SecurityHeaders.com score of A or higher. Predicted the headers would not break any legitimate functionality (social media previews, search engine rendering, browser compatibility). Assumed the missing CSP header would not be a significant security gap for a static portfolio without user-generated content. Predicted the HSTS preload commitment would be acceptable given no plans to ever revert to HTTP."},{"heading":"Resultados Medidos","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-outcomes","content":"SecurityHeaders.com score is A+ as targeted. No legitimate functionality has been broken, social media previews (Twitter, LinkedIn, Slack) work correctly because they use server-side rendering for unfurling, not iframe embedding. Search engines render the site correctly despite the headers. The missing CSP has not been exploited (no XSS vectors exist on a static site without user input), confirming the prediction. HSTS preload commitment is comfortable, there is zero scenario where the site would revert to HTTP. The unexpected observation: the Permissions-Policy header disabling interest-cohort=() is now partially redundant because Google deprecated FLoC in favor of Topics API, but the header still serves as a documented privacy stance."},{"heading":"Incógnitas al Momento de la Decisión","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-unknowns","content":"Did not know at decision time whether CSP nonce-based approach would become compatible with styled-components' runtime injection model. As of early 2026, styled-components v6 has experimental CSP nonce support, but the portfolio uses v5.3 and migration would require significant effort. Also unknown: whether X-XSS-Protection would be removed from browser implementations entirely, potentially causing unexpected behavior. Chrome has already removed it, but the header is harmless when unsupported. Unknown: whether future portfolio features (comment system, contact form with file upload) would require relaxing any of these headers."},{"heading":"Clasificación de Reversibilidad","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-reversibility","content":"Puerta de Ida y Vuelta\n\nEach header is independently removable by editing the securityHeaders array in next.config.js. Adding, modifying, or removing individual headers requires a single line change plus redeploy. The only exception is HSTS preload: once submitted to the preload list, removal requires a formal delisting process that takes months. However, the HSTS header itself can be removed immediately, the preload list effect persists independently in browser distributions. Estimated effort: 5 minutes for any header change."},{"heading":"Contra-Argumento Más Fuerte","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-counterArgument","content":"For a static portfolio site with zero user input and no authentication, the security header configuration is over-engineered. The actual attack surface is minimal: there are no forms to exploit (except a contact form that submits to a third-party), no cookies to steal, no sessions to hijack. The headers protect against theoretical attacks on a site with no practical attack vectors. A simpler approach: rely on Vercel's default headers and Next.js's built-in protections. The counter-counter: security headers are cheap insurance, the configuration took 30 minutes to implement and adds zero runtime cost. More importantly, for a CTO's portfolio, demonstrating security awareness through headers is a professional signal, not just a technical measure."},{"heading":"Contexto Técnico","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-techContext","content":"Stack\n\n- next.config.js\n- HTTP Headers\n- HSTS\n- Permissions-Policy\n\nSecurity Headers Score\n\nA+\n\nHeaders Configured\n\n7\n\nHsts Max Age\n\n63072000 seconds (2 years)\n\nDisabled Browser Apis\n\n4\n\nRestricciones\n\n- No CSP due to styled-components inline styles\n- HSTS preload is effectively irreversible\n- X-Frame-Options may block legitimate embedding"},{"heading":"Decisiones Relacionadas","level":2,"url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers#decisionDetail-relatedDecisions","content":"- [ADR-012accepted\n\n  Deliberate AI Crawler Access Policy over Default Blocking](<https://lucioduran.com/es/decisions/deliberate-ai-crawler-access-policy>)\n- [ADR-015accepted\n\n  Critical Rendering Path Optimization Strategy](<https://lucioduran.com/es/decisions/critical-rendering-path-optimization>)\n- [ADR-016accepted\n\n  Scoped Remote Image Patterns over Generic CDN Proxy](<https://lucioduran.com/es/decisions/scoped-remote-image-patterns>)\n\n[Volver a Decisiones](<https://lucioduran.com/es/decisions>)"}],"links":[{"text":"ADR-012acceptedDeliberate AI Crawler Access Policy over Default Blocking","url":"https://lucioduran.com/es/decisions/deliberate-ai-crawler-access-policy"},{"text":"ADR-015acceptedCritical Rendering Path Optimization Strategy","url":"https://lucioduran.com/es/decisions/critical-rendering-path-optimization"},{"text":"ADR-016acceptedScoped Remote Image Patterns over Generic CDN Proxy","url":"https://lucioduran.com/es/decisions/scoped-remote-image-patterns"},{"text":"Volver a Decisiones","url":"https://lucioduran.com/es/decisions"}],"structuredData":[{"@context":"https://schema.org/","@graph":[{"@type":"TechArticle","url":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers","name":"ADR-017: Defense-in-Depth HTTP Security Headers","headline":"Defense-in-Depth HTTP Security Headers","datePublished":"2024-03-25","author":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"},"inLanguage":"en","about":"Software Architecture Decision Record","proficiencyLevel":"Expert","dependencies":"next.config.js, HTTP Headers, HSTS, Permissions-Policy"},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Decisiones de Arquitectura","item":"https://lucioduran.com/es/decisions"},{"@type":"ListItem","position":3,"name":"Defense-in-Depth HTTP Security Headers"}]}]}],"contentHash":"0aaaf0883f7508958ff262ca73df2e5f2499a3f29ae7658b471ea03e246ca783","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/decisions/defense-in-depth-security-headers","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":40552,"contentBytes":7705}}
