๐ Web-Server Security Controls: HTTPS, CORS, CSP, HSTS, and Headers on Apache and Nginx
Intro: Not every web-security control belongs in application code. A large part of real production posture is determined by what the edge, reverse proxy, and web server do with TLS, CORS, security headers, content types, and caching. This page focuses on operator-owned browser and web-server controls rather than secure coding.
What this page includes
- what HTTPS, HSTS, CORS, CSP, and adjacent headers actually defend;
- where to configure them in Apache and Nginx;
- deployable reference snippets for header and preflight handling;
- what tools like Skipfish and w3af usually complain about, and how to interpret those findings.
Quick mental model
Think in four layers:
- transport - HTTPS and HSTS reduce downgrade and interception risk;
- browser exposure - CORS, CSP, frame restrictions, MIME controls, and referrer policy shape what the browser may read, execute, or embed;
- caching and delivery -
Cache-Control, content type, and attachment behavior change how sensitive or active content behaves in user agents; - review and verification - scanners, browser DevTools, curl, and route-by-route testing prove that what the team intended is what production actually emits.
What each control is for
HTTPS
Use HTTPS everywhere for authenticated pages, APIs, admin routes, upload/download flows, and supporting assets. The practical reason is not only secrecy of credentials in transit. Without HTTPS, an active network attacker can also alter returned HTML, CSS, JavaScript, or redirects and turn a delivery path into a code-execution path.
HSTS
HSTS tells a browser to keep using HTTPS for future visits to the host. This is stronger than โwe redirect HTTP to HTTPSโ because a plain redirect still leaves the first insecure HTTP request exposed. HSTS only takes effect when learned over HTTPS, and preload is operationally useful only after the team is sure every covered host is truly HTTPS-only.
CORS
CORS is not authorization. It is a browser-enforced read/exposure policy for cross-origin JavaScript access. Treat it as a browser trust boundary, not as a substitute for server-side authn or authz.
CSP
CSP helps narrow what script, frame, and resource sources the browser may use. It is a containment and hardening mechanism for XSS, third-party script sprawl, and framing mistakes. It should be deployed deliberately, not as a giant wildcard copied from a forum answer.
Other headers worth owning
A strong baseline usually also includes:
X-Content-Type-Options: nosniff;Referrer-Policy;Permissions-Policy;- frame protections such as
frame-ancestorsin CSP, withX-Frame-Optionskept only for older-client compatibility; - careful
Cache-Controlon personalized and export-like responses; Cross-Origin-Resource-Policyor related cross-origin isolation headers only where the application model supports them.
What Skipfish and w3af findings usually mean
These tools often flag issues such as:
- missing
Strict-Transport-Security; - missing
Content-Security-Policy; - missing
X-Content-Type-Options; - permissive or inconsistent CORS responses;
- weak clickjacking protections;
- mixed or inconsistent cache rules on sensitive pages.
Treat those findings as configuration review prompts, not automatic severity truth. For example:
- missing HSTS on a public marketing page is different from missing HSTS on an authenticated product origin;
- a missing CSP on a purely static docs site is not the same risk as missing CSP on a privileged admin application;
- missing
nosniffon script or style routes is usually more important than on inert binary downloads.
Where to configure in Nginx
Common locations are:
/etc/nginx/nginx.conffor global behavior;/etc/nginx/conf.d/*.conffor shared site fragments;/etc/nginx/sites-available/*.confandsites-enabled/on Debian-style layouts;- the relevant
server {}block for host-specific behavior; location {}only when a route family genuinely needs a different policy.
Nginx baseline for headers on an authenticated web app
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-$request_id'; connect-src 'self' https://api.example.com" always;
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
Nginx pattern for CORS preflight on a narrow API surface
location /api/public/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age "600" always;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
proxy_pass http://api_upstream;
}
Nginx review notes
- keep exact origins for credentialed flows;
- do not combine
Access-Control-Allow-Credentials: truewith*; - use
alwaysso headers still appear on relevant error paths; - prefer host- or route-specific CORS blocks rather than one site-wide wildcard rule.
Where to configure in Apache HTTP Server
Common locations are:
/etc/apache2/sites-available/*.confon Debian-style systems;/etc/httpd/conf/httpd.confor/etc/httpd/conf.d/*.confon RHEL-style systems;- the relevant
<VirtualHost *:443>block for host-specific policy; mod_headers,mod_ssl, and sometimesmod_rewritefor the actual controls.
Make sure these modules are available where needed:
sudo a2enmod ssl headers rewrite
sudo systemctl reload apache2
Apache baseline for HTTPS and headers
<VirtualHost *:443>
ServerName app.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'"
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>
Apache HTTPS redirect on the cleartext vhost
<VirtualHost *:80>
ServerName app.example.com
RewriteEngine On
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
Apache CORS example for a narrow API path
<Location "/api/public/">
Header always set Access-Control-Allow-Origin "https://frontend.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "600"
</Location>
CORS interview and review prompts worth keeping
These questions are useful because they test whether someone understands browser security rather than memorized header names:
- What three things define an origin?
- Does CORS block the request, the response, or both in all cases?
- Why is
Access-Control-Allow-Origin: *incompatible with credentialed browser flows? - Which requests cause a preflight and why?
- Why is CORS not a defense against CSRF by itself?
- Why should
OPTIONShandling often be optimized at the web-server or proxy layer instead of waking the whole app?
Route-by-route test commands
curl -I https://app.example.com/
curl -I https://app.example.com/login
curl -I https://app.example.com/account
curl -I https://app.example.com/admin
curl -I https://app.example.com/download/export.csv
curl -i -X OPTIONS -H 'Origin: https://frontend.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: Authorization, Content-Type' https://api.example.com/api/public/report
Production mistakes that keep recurring
- HSTS enabled on one host but missing on login or admin subdomains;
- CSP copied from a blog with broad wildcards and permanent
unsafe-inline; - CORS set globally even though only a few routes need browser cross-origin access;
OPTIONSnot implemented for real browser behavior, even though API docs look fine;nosniffmissing on script or style paths behind CDN or legacy static handlers;- cleartext port 80 left enabled without clean redirect and host coverage;
- personalized pages cached too broadly by edge, proxy, or browser.
Use this page with
- Browser Security Foundations: CSP, CORS, Cookies, and Sessions
- Security Headers and Reference Configurations
- Frontend Security Review Playbook
- CSP, SRI, and Third-Party JavaScript Control Patterns
- snippets/frontend/nginx-security-headers.conf
- snippets/frontend/apache-security-headers.conf
- assets/report-samples/web-scanner-header-findings-sample.pdf
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.