Web
Security headers: CSP, HSTS and more — how to reach A+ in scanners
Which HTTP security headers you need, what each one protects against, and how to add them correctly in nginx — with a ready-to-use configuration.
· 7 min read

Security headers are extra instructions the server sends to the browser with every response. They tell the browser how to protect the page: open it only over HTTPS, don't run foreign scripts, don't let other sites embed it in a frame, and so on.
Missing headers are one of the quickest shortcomings to spot, both in the cybersecurity expertise and in public scanners such as securityheaders.com or Mozilla Observatory. The good news is that adding them usually takes a few lines of configuration.
The key headers and what they do
Strict-Transport-Security (HSTS)
Tells the browser to open this domain only over HTTPS for a set period. Even if the user types http://, no unencrypted request goes out.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Before adding includeSubDomains, make sure every subdomain works over HTTPS. Add the preload directive only once you understand the consequences: getting off the preload list takes time.
Content-Security-Policy (CSP)
The most powerful and the most complex header. It defines which sources the page may load scripts, styles, images and fonts from, and where it may send requests. A well-configured CSP significantly reduces the impact of XSS vulnerabilities.
A strict starting policy:
default-src 'self'; script-src 'self'; style-src 'self';
img-src 'self' data:; font-src 'self'; connect-src 'self';
object-src 'none'; base-uri 'self'; frame-ancestors 'self';
form-action 'self'
Key points:
- Avoid
'unsafe-inline'and'unsafe-eval'— they cancel out most of CSP's protection. If you need an inline script, use anonceor a hash. - Add external services (analytics, payment widget, maps) with their exact domain, not with
*. frame-ancestorsprotects against clickjacking and replacesX-Frame-Optionsin modern browsers.
X-Content-Type-Options
X-Content-Type-Options: nosniff
Tells the browser not to "guess" the file type. For example, an uploaded text file won't be executed as a script.
X-Frame-Options
X-Frame-Options: SAMEORIGIN
Prevents the page from being opened inside an <iframe> on other sites. Worth adding alongside CSP's frame-ancestors for older browsers.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
When navigating to another site, only the domain is sent, not the full URL with its parameters and tokens.
Permissions-Policy
Permissions-Policy: camera=(), microphone=(), geolocation=()
Disables browser features the site doesn't use. Even if foreign code ends up on the page, it can't reach the camera or geolocation.
Hiding the server version
By default nginx shows its version in the Server header and on error pages. It is a small form of information disclosure, but one the expertise does record. server_tokens off; closes it.
Note:
X-XSS-Protectionis a deprecated header. Modern browsers don't support it; you don't need to add it — CSP provides the protection.
nginx configuration
Add the following block inside server { ... } (the HTTPS part):
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
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;
The always parameter makes sure the headers are sent on error responses (404, 500) too.
A common trap: add_header inheritance
In nginx, add_header directives are inherited by a nested block only if that block has no add_header of its own. If you write location /assets/ { add_header Cache-Control ...; }, every security header disappears from responses in that location.
The fix: move the headers into a separate file and include it wherever needed.
# /etc/nginx/snippets/security-headers.conf holds the add_header lines above
server {
include snippets/security-headers.conf;
location /assets/ {
include snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
Rolling out CSP safely
Switching on a strict CSP on an existing site all at once can break features. So go step by step:
- First enable the policy via the
Content-Security-Policy-Report-Onlyheader — the browser blocks nothing and only logs violations to the console (or to areport-uriendpoint). - Collect violations for a few days and review each one: is it a source you actually need, or unnecessary or foreign code?
- Move inline scripts and styles into separate files, or add a
nonce. - Once the policy is clean, rename the header to
Content-Security-Policy.
Checking the result
Look at the headers in the browser or the terminal:
curl -sI https://your-domain.uz | grep -iE "strict|content-security|x-content|x-frame|referrer|permissions|server"
Then test the site on securityheaders.com and Mozilla Observatory. Scanners adjust their grading criteria from time to time, but HSTS and a CSP without 'unsafe-inline' are usually what decides a top grade.
Keep one thing in mind: an A+ in a scanner only means your headers are configured correctly. It says nothing about business logic or access control flaws in the application. Headers are one layer of protection.
Summary
Security headers take little effort and bring real benefit. Add HSTS, CSP, nosniff, Referrer-Policy, Permissions-Policy and server_tokens off, account for add_header inheritance, and try CSP in report-only mode first.
Want your site's headers and other settings checked? Request a security scan — the Claude AI agent and our specialist will review the configuration and show you what needs fixing.


