Web Server audit fixes

Security Headers: fixing all 8 checks

The security headers audit reads what nginx or Apache actually returns on a loopback probe of /, so a failure means browsers get no instruction to block framing, MIME sniffing or a downgrade.

Seven of the eight checks skip only when no web server is detected at all, so a skip here means nginx, Apache, Caddy and lighttpd were all absent. Cookie Flags has a second one: the probe requests / and never a login route, so a response carrying no Set-Cookie header leaves it nothing to grade.

8
checks in this audit
0
rated high severity
0
need root or sudo
~10s
automated run time

Transcribed from the web-server-security-security-headers audit script.

Every check

Find your finding

Listed in the order the audit runs them. Take the check name from the third field of the result line and open its page.

  • HSTS

    Soon

    Checks whether Strict-Transport-Security is sent and whether its max-age meets the six-month baseline

    MediumNo root4 results
  • Content Type Options

    Soon

    Checks whether X-Content-Type-Options nosniff is sent to stop browsers MIME-sniffing uploads

    LowNo root3 results
  • Frame Protection

    Soon

    Checks whether X-Frame-Options or a CSP frame-ancestors directive restricts framing

    MediumNo root3 results
  • Content Security Policy

    Soon

    Checks whether a Content-Security-Policy is sent and whether it relies on unsafe-inline, unsafe-eval or wildcards

    MediumNo root3 results
  • Referrer Policy

    Soon

    Checks whether a Referrer-Policy is sent so full URLs do not leak to third parties

    LowNo root3 results
  • Permissions Policy

    Soon

    Checks whether a Permissions-Policy is sent to deny camera, microphone and geolocation by default

    LowNo root3 results
  • CORS Policy

    Soon

    Access-Control-Allow-Origin scope and credentialed cross-origin access

    MediumNo root4 results
  • Cookie Flags

    Soon

    HttpOnly, Secure and SameSite on Set-Cookie

    MediumNo root3 results
Shared script behaviour

How the Security Headers audit reads your server

Every check in this audit runs after the same preamble, and several of its results only make sense once you know what that preamble could and could not see.

How header checks work. hdrcheck NAME REGEX returns live|<value> if the live probe response carried the header, config| if the header name appears anywhere in the nginx/Apache config (CFGTXT), or none|. Config evidence gives a softer PASS ("configured - confirm it is sent"). Every message may end with (no live response, config only) or (default vhost only - no server_name found to probe). Important probe caveats for all eight checks:

  • The probe tries https://127.0.0.1/ first with curl -k. If HTTPS is not bound on loopback, the probe falls back to http://127.0.0.1/; HSTS and Secure cookies are only sent over HTTPS, and many sites set headers only in the HTTPS server block, so results then under-report.
  • Only the root path / of the first server_name is examined. Application routes, other vhosts and error pages may differ.
  • Headers added by a CDN/WAF at the edge are not seen from loopback → false negatives. Headers added by the application (framework middleware) are seen if the app responds on /.
  • nginx add_header is not inherited when a child block defines its own add_header; a location with any add_header silently drops the parent's security headers. The probe sees the effect on / only.

All web-server scripts share one preamble. It detects nginx, Apache (apache2/httpd, APROOT = /etc/apache2 or /etc/httpd), Caddy and lighttpd. NGT is nginx -T output (effective config with every include expanded) when root, otherwise a concatenation of nginx.conf, conf.d/*.conf, sites-enabled/*. APC is the concatenated Apache config tree (main file, ports.conf, conf.d, conf-enabled, sites-enabled, mods-enabled, conf.modules.d), comments stripped; APM is apachectl -M (root) or the mods-enabled/*.load names. WROOTS are all root/DocumentRoot values. A live probe curls https://127.0.0.1/, http://127.0.0.1/, http://127.0.0.1:8080/ with the first real server_name/ServerName as Host:. hdr NAME reads a response header; code URL returns the HTTP status. Caddy and lighttpd are detected but their configs are not parsed, so most checks on those hosts rely on the live probe only. Messages ending in "(no live response, config only)" or "(default vhost only - no server_name found to probe)" mean the probe could not be used.

Why checks skip

A SKIP is never a pass

A skipped check verified nothing at all. The most common cause is privilege: no check in this audit needs root, so a skip here means something else was missing. Before every run, each script works out which of four privilege modes it is in.

The four privilege modes an audit script can run in, and what each one means for checks that need root
ModeHow it is reachedWhat it means for your results
rootThe audit runs as UID 0.Every privileged branch runs directly. No check skips for lack of permission.
nopasssudo -n true succeeds, so the account has passwordless sudo.Privileged commands run through sudo -n.
passA sudo password was supplied in the CtrlOps audit settings and accepted.Privileged commands run through sudo -S.
noneNone of the above worked.CAN_ROOT=0. Every privileged branch returns SKIP with a "Need root" message rather than guessing.

If many checks skip with a “need root” message, run the audit as root, grant the audit account sudo (with or without NOPASSWD), or supply the sudo password in the CtrlOps audit settings.

FAQ

Security Headers questions

The probe runs from the server itself against https://127.0.0.1/ and falls back to http://127.0.0.1/ when HTTPS is not bound on loopback, sending the first server_name as the Host header. Anything a CDN or WAF adds at the edge is invisible from there, and a header set only inside the HTTPS server block is missed whenever the probe lands on plain HTTP. Only the root path of one vhost is read, so a header added in a specific location block or by an application route is not seen either.
Only that the header name appears somewhere in the nginx or Apache config text, not that a browser ever receives it. The check falls back to that config match when the live probe returned no response or no header, and it reports a softer PASS rather than a FAIL. Confirm delivery with curl -sI against your real domain before treating it as done, particularly on nginx, where a location block that sets any add_header of its own silently drops every add_header it would otherwise inherit.
Content-Security-Policy and HSTS are the two to stage. A strict CSP blocks the inline scripts and styles many sites still depend on, so start with Content-Security-Policy-Report-Only and read the reports before enforcing. HSTS should start at a short max-age and only then move to the 15552000 the check wants; includeSubDomains breaks any subdomain still served over plain HTTP, and preload takes months to reverse. X-Content-Type-Options, Referrer-Policy and Permissions-Policy are safe to add straight away.
Audit your fleet

Run all 8 Security Headers checks, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Start instantly· No credit card· No sneaky autorenewals