← All checks

HTTP security headers

medium external

What it inspects

Sends one HTTPS HEAD request for / to port 443 (certificate not verified, scanned name used as SNI and Host) and reads the headers of that first response; redirects are not followed. WARN if any of Strict-Transport-Security, Content-Security-Policy, X-Frame-Options or X-Content-Type-Options is missing. Values are not checked, and Content-Security-Policy-Report-Only does not count. If nothing answers HTTPS on 443 the check is skipped. This is a passive check: it sends only what a normal client would.

Why it matters

These headers switch on protections the browser already has: HSTS stops downgrade to plain HTTP, X-Frame-Options and CSP frame-ancestors stop other sites framing yours for clickjacking, X-Content-Type-Options stops MIME sniffing, and a full CSP limits where scripts may come from. Missing headers breach nothing by themselves, but they remove cheap layers of defence.

How to verify

# only against hosts you own
curl -sI https://example.com | grep -iE '^(strict-transport-security|content-security-policy|x-frame-options|x-content-type-options):'
# All four header names should be printed. (curl -I sends a HEAD request, like the check.)

How to fix

Add the four headers at http level in nginx or in Apache's configuration with a validated file that rolls itself back, or by hand in Caddy, then tighten HSTS and CSP deliberately.

Add the four headers in nginx and Apache

Any distro

# These headers apply to EVERY site on this server (nginx http level, Apache server-wide). If any site must be framed by another origin
# (widgets, payment or booking iframes, LMS or SSO embeds), skip this step and set the headers per site as described in the next step.
# nginx: sets the four headers for every server in this nginx (http level). Assumes the stock layout where nginx.conf includes
# /etc/nginx/conf.d/*.conf (check: grep -n 'conf.d' /etc/nginx/nginx.conf). Skipped when nginx is not installed.
# A server or location that has its own add_header lines does NOT inherit these: see the next step.
# Where your application already sends one of these headers, the response can carry it twice; remove one source.
if command -v nginx >/dev/null 2>&1 && [ -d /etc/nginx/conf.d ]; then
install -m 0644 /dev/stdin /etc/nginx/conf.d/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
add_header Strict-Transport-Security "max-age=300" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
EOF
nginx -t && systemctl try-reload-or-restart nginx
if ! nginx -t >/dev/null 2>&1; then rm -f /etc/nginx/conf.d/umbermark-security-headers.conf; echo 'nginx -t failed: file removed, nothing reloaded'; fi
fi
# Apache on Debian/Ubuntu (apache2): applies to every site. setifempty skips a header Apache already has, but a proxied or CGI backend's own header can still be sent as well (duplicates are possible).
if [ -d /etc/apache2/conf-available ] && command -v a2enconf >/dev/null 2>&1; then
install -m 0644 /dev/stdin /etc/apache2/conf-available/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
<IfModule mod_headers.c>
Header always setifempty Strict-Transport-Security "max-age=300"
Header always setifempty Content-Security-Policy "frame-ancestors 'self'"
Header always setifempty X-Frame-Options "SAMEORIGIN"
Header always setifempty X-Content-Type-Options "nosniff"
</IfModule>
EOF
a2enmod headers
a2enconf umbermark-security-headers
apachectl configtest && systemctl try-reload-or-restart apache2
if ! apachectl configtest >/dev/null 2>&1; then a2disconf umbermark-security-headers; rm -f /etc/apache2/conf-available/umbermark-security-headers.conf; echo 'apachectl configtest failed: configuration removed, nothing reloaded'; fi
fi
# Apache on the RHEL family (httpd): mod_headers is loaded by default there.
if [ -d /etc/httpd/conf.d ]; then
install -m 0644 /dev/stdin /etc/httpd/conf.d/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
<IfModule mod_headers.c>
Header always setifempty Strict-Transport-Security "max-age=300"
Header always setifempty Content-Security-Policy "frame-ancestors 'self'"
Header always setifempty X-Frame-Options "SAMEORIGIN"
Header always setifempty X-Content-Type-Options "nosniff"
</IfModule>
EOF
apachectl configtest && systemctl try-reload-or-restart httpd
if ! apachectl configtest >/dev/null 2>&1; then rm -f /etc/httpd/conf.d/umbermark-security-headers.conf; echo 'apachectl configtest failed: file removed, nothing reloaded'; fi
fi
  • Add them by hand where the step above does not reach (nginx servers with their own headers, Caddy, CDN)
  • Strengthen HSTS and CSP later, carefully

2 more steps in the Fix Pack. See pricing

References

Scan your server free