Stack guide
Apache security headers — .htaccess and httpd.conf patterns that pass Scorifya
Apache still serves a meaningful slice of the web (especially shared hosting and self-managed VMs). The good news: every header Scorifya scans for can be set with one mod_headers block, either in the main `httpd.conf` / VirtualHost stanza or in a per-directory `.htaccess` if you're on hosting where that's your only edit surface. This guide gives you the baseline block, notes on context precedence, and a deploy-time verification step.
1. Enable mod_headers and add the baseline block
Apache ships mod_headers but it isn't always enabled by default. On Debian/Ubuntu run `a2enmod headers && systemctl reload apache2`; on RHEL/CentOS uncomment the `LoadModule headers_module` line in `httpd.conf` and reload. Then drop the baseline `Header always set` directives into your main config or a `.htaccess` — `always` is important so the headers ship even on 4xx/5xx responses, where attackers do a lot of probing.
HSTS missing — nginx (server / location) (nginx)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # If TLS terminates at a CDN, set the header there instead.
HSTS missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.hsts({ maxAge: 31_536_000, includeSubDomains: true }));HSTS missing — Apache (vhost) (apache)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options missing — nginx (nginx)
add_header X-Frame-Options "DENY" always; # Use SAMEORIGIN instead of DENY only if same-origin framing is required.
X-Frame-Options missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.frameguard({ action: "deny" }));X-Frame-Options missing — Apache (apache)
Header set X-Frame-Options "DENY"
X-Content-Type-Options missing — nginx (nginx)
add_header X-Content-Type-Options "nosniff" always;
X-Content-Type-Options missing — Express (helmet) (typescript)
import helmet from "helmet"; app.use(helmet.noSniff());
X-Content-Type-Options missing — Apache (apache)
Header set X-Content-Type-Options "nosniff"
Related Scorifya checks: missing_hsts, missing_xfo, missing_xcto, missing_referrer_policy, missing_permissions_policy
2. Add a starter CSP in report-only mode first
CSP in Apache uses the same `Header always set` syntax as the others, but your first deploy should use `Content-Security-Policy-Report-Only` so violations land in a logging endpoint without breaking pages. Run report-only for at least one full release cycle, fix the violations, then flip the header name to `Content-Security-Policy`. If your app uses inline scripts, plan for nonces (Apache can't generate per-request values, so nonces have to come from the application layer).
CSP missing — nginx (nginx)
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'" always;
CSP missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
}),
);CSP missing — Apache (apache)
Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'"
Weak CSP — nginx (tighten script-src) (nginx)
# Drop 'unsafe-inline' / wildcards in production; use nonces or hashes for any inline script. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
Weak CSP — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
}),
);Weak CSP — Apache (apache)
# Replace permissive policy with explicit hosts and no unsafe-inline where possible. Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
Related Scorifya checks: missing_csp, weak_csp
3. Hide the Apache version banner
Apache's default `Server` header includes the version (`Apache/2.4.41 (Ubuntu)`). Add `ServerTokens Prod` and `ServerSignature Off` to your main config — `Prod` strips the version down to just `Apache`, and `ServerSignature` removes the footer Apache adds to its own error pages. This is a `.htaccess`-incompatible setting: it has to live in `httpd.conf` or a conf.d include.
Related Scorifya checks: server_banner_version
4. Verifying after deploy
After reload, run `curl -I https://yourdomain.com/` and check that every header you added shows up. Then re-scan with Scorifya — the headers category should jump immediately. If a `.htaccess` change isn't taking effect, check that `AllowOverride All` (or at least `AllowOverride FileInfo`) is set in the relevant `<Directory>` block in `httpd.conf` — that's the most common reason `.htaccess` headers silently fail.
Background
What is HSTS? HTTP Strict Transport Security explained →
How HSTS works, why the bootstrap window matters, what max-age and includeSubDomains do, and when (or whether) to submit your domain to the browser preload list.
Read more
Verify with a fresh scan
After deploy, run the scanner on the affected hostname. Headers and TLS settings update on the very next request, so you should see the score move within seconds.