Server security guide · Romania

Which HTTP security headers should I configure in nginx?

Security headers are instructions your server gives the browser: which scripts may run, whether the page can sit inside an iframe, what information to pass on. A few lines of nginx configuration close whole classes of attacks on your visitors.

Updated:

Short answer

  1. Set X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy and frame-ancestors in nginx on every response using add_header with the always flag, and add server_tokens off.
  2. Introduce Content-Security-Policy as Content-Security-Policy-Report-Only first, and switch to blocking only when the reports stop showing legitimate resources being blocked.
  3. Use frame-ancestors in the CSP instead of X-Frame-Options, and keep X-Frame-Options only for old browsers.
  4. Test the result with curl -I and a public header scanner after every deploy.
Header every site needs
X-Content-Type-Options: nosniff
Against clickjacking
frame-ancestors 'self' in the CSP (plus X-Frame-Options)
How to introduce CSP
Content-Security-Policy-Report-Only, then enforce
The nginx trap
an add_header inside a location cancels those from the server block

What each header does and what it stops

Content-Security-Policy is the strongest: it lists the origins from which the browser accepts scripts, styles, images and connections, which sharply limits the damage of XSS because an injected script is not allowed to run. X-Content-Type-Options: nosniff stops the browser guessing a file's type and executing something served as text. frame-ancestors, and its predecessor X-Frame-Options, stop other sites embedding yours in an iframe, the basis of clickjacking. Referrer-Policy controls how much of the URL leaves for other sites. Permissions-Policy switches off camera, microphone or geolocation.

How to set them in nginx without breaking the site

The golden rule in nginx: add_header directives inside a location block replace the whole list inherited from the server block. Add one cache header in a location for images and the security headers vanish from those responses. The fix is an include file inside every block that has its own add_header. Add the always flag so the headers also appear on 4xx and 5xx responses. Write the CSP in report-only mode and watch it for a few weeks: inline scripts, external fonts and chat widgets all show up in the reports. Only then enforce it, using a nonce for inline scripts.

  • server_tokens off hides the nginx version in headers and error pages
  • HSTS has its own page in this guide and belongs only on HTTPS responses
  • object-src 'none' and base-uri 'self' close two often-forgotten vectors
Recommended HTTP security headers for a production site
HeaderRecommended valueWhat it prevents
Content-Security-Policydefault-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'XSS, resource injection, clickjacking
X-Content-Type-OptionsnosniffExecuting files served with the wrong type
X-Frame-OptionsSAMEORIGIN (for old browsers)Clickjacking
Referrer-Policystrict-origin-when-cross-originLeaking URLs that contain tokens
Permissions-Policycamera=(), microphone=(), geolocation=()Sensor access from compromised pages
Strict-Transport-Securitymax-age=31536000; includeSubDomains (HTTPS only)Downgrade to HTTP
Serverserver_tokens offVersion fingerprinting for exploitation

Commands and configuration

/etc/nginx/snippets/security-headers.conf, included in every block that has add_header
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=()" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; report-uri /csp-report" always;
The server block that uses it
server_tokens off;

server {
  listen 443 ssl;
  http2 on;
  server_name example.ro;
  include snippets/security-headers.conf;

  location ~* \.(png|jpg|webp|css|js)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
    include snippets/security-headers.conf;  # otherwise the headers disappear here
  }

  location = /csp-report {
    access_log /var/log/nginx/csp.log;
    return 204;
  }
}
Verify after each deploy
curl -sI https://example.ro/ | grep -iE "content-security|x-frame|x-content-type|referrer|permissions|strict-transport|^server"
curl -sI https://example.ro/missing-page | grep -i x-content-type

Practical examples

Online shop in Cluj-Napoca with XSS in a review field

A script injected into a product review read the cookies of logged-in users. After the CSP was switched to blocking, with script-src limited to the shop's own domain, the script no longer executed, and the CSP reports named the affected page before any customer complained.

Booking platform in Timisoara framed inside an iframe

A third-party site embedded the login page in an invisible iframe above a 'download' button. frame-ancestors 'self' stopped the embedding. The check also showed the header was missing on /login because of a cache add_header in that location.

Common mistakes

  • Putting the headers in the server block and losing them on routes that have their own add_header, without noticing.
  • Publishing the CSP straight in blocking mode and breaking the payment widget or the support chat.
  • Using 'unsafe-inline' in script-src 'temporarily', after which the CSP no longer stops any XSS.
  • Leaving out always, so the headers are missing on error pages.

How 4hosting helps

  • Hardening & Compliance configures the headers and moves the CSP from report-only to enforcing based on the reports, within your included hours: 1–8 h a month on VPS, 5–10 h on dedicated servers (depending on plan), 10 h on GPU servers, and 1 h from cPanel Advanced up.
  • You request those hours from the DevOps Services section of the client panel; they are not carried over, and extra hours are accepted and billed separately.
  • Programming hours can go to the application side of a strict CSP, such as the nonce for inline scripts in a React / Next.js app.
  • Pentesting & Security Audit includes a check of headers and CSP in the web audit report.

Related questions

  • Will a CSP break Google Analytics, Stripe or my support chat?

    Only if you do not declare them. Each third-party service publishes the origins it needs for script-src, connect-src and frame-src. The report-only period exists for exactly this: the reports show which origins are missing from the policy. You add them explicitly instead of opening the policy with a wildcard.

  • Do I still need X-Frame-Options if I have frame-ancestors?

    frame-ancestors takes priority in every modern browser and is more flexible, since it allows several origins. Keep X-Frame-Options for old browsers; when both are present, a modern browser ignores X-Frame-Options. Having both does no harm.

  • Do these headers belong in nginx or in the application?

    The static ones, nosniff, Referrer-Policy, Permissions-Policy and frame-ancestors, belong in nginx so they also cover static files and error pages. A CSP with a per-request nonce has to be generated by the application, because the nonce changes with every response. Check both layers so you do not duplicate headers.

Request a security review

A production server starts getting attacked within minutes of receiving a public IP address, however small the project is. This guide ranks the controls by impact, from SSH access to incident response, with the Romanian rules that apply along the way. Every topic has its own page with a short answer, examples and ready-to-use configuration.