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.
Transcribed from the web-server-security-security-headers audit script.
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
SoonChecks whether Strict-Transport-Security is sent and whether its max-age meets the six-month baseline
MediumNo root4 resultsContent Type Options
SoonChecks whether X-Content-Type-Options nosniff is sent to stop browsers MIME-sniffing uploads
LowNo root3 resultsFrame Protection
SoonChecks whether X-Frame-Options or a CSP frame-ancestors directive restricts framing
MediumNo root3 resultsContent Security Policy
SoonChecks whether a Content-Security-Policy is sent and whether it relies on unsafe-inline, unsafe-eval or wildcards
MediumNo root3 resultsReferrer Policy
SoonChecks whether a Referrer-Policy is sent so full URLs do not leak to third parties
LowNo root3 resultsPermissions Policy
SoonChecks whether a Permissions-Policy is sent to deny camera, microphone and geolocation by default
LowNo root3 resultsCORS Policy
SoonAccess-Control-Allow-Origin scope and credentialed cross-origin access
MediumNo root4 resultsCookie Flags
SoonHttpOnly, Secure and SameSite on Set-Cookie
MediumNo root3 results
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 withcurl -k. If HTTPS is not bound on loopback, the probe falls back tohttp://127.0.0.1/; HSTS andSecurecookies 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 firstserver_nameis 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_headeris not inherited when a child block defines its ownadd_header; alocationwith anyadd_headersilently 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.
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.
| Mode | How it is reached | What it means for your results |
|---|---|---|
root | The audit runs as UID 0. | Every privileged branch runs directly. No check skips for lack of permission. |
nopass | sudo -n true succeeds, so the account has passwordless sudo. | Privileged commands run through sudo -n. |
pass | A sudo password was supplied in the CtrlOps audit settings and accepted. | Privileged commands run through sudo -S. |
none | None 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.
Security Headers questions
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