# Web-Server Security Controls: HTTPS, CORS, CSP, and HSTS for 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:

1. **transport** - HTTPS and HSTS reduce downgrade and interception risk;
2. **browser exposure** - CORS, CSP, frame restrictions, MIME controls, and referrer policy shape what the browser may read, execute, or embed;
3. **caching and delivery** - `Cache-Control`, content type, and attachment behavior change how sensitive or active content behaves in user agents;
4. **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-ancestors` in CSP, with `X-Frame-Options` kept only for older-client compatibility;
* careful `Cache-Control` on personalized and export-like responses;
* `Cross-Origin-Resource-Policy` or 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 `nosniff` on script or style routes is usually more important than on inert binary downloads.

## Where to configure in Nginx

Common locations are:

* `/etc/nginx/nginx.conf` for global behavior;
* `/etc/nginx/conf.d/*.conf` for shared site fragments;
* `/etc/nginx/sites-available/*.conf` and `sites-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

```nginx
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

```nginx
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: true` with `*`;
* use `always` so 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/*.conf` on Debian-style systems;
* `/etc/httpd/conf/httpd.conf` or `/etc/httpd/conf.d/*.conf` on RHEL-style systems;
* the relevant `<VirtualHost *:443>` block for host-specific policy;
* `mod_headers`, `mod_ssl`, and sometimes `mod_rewrite` for the actual controls.

Make sure these modules are available where needed:

```bash
sudo a2enmod ssl headers rewrite
sudo systemctl reload apache2
```

### Apache baseline for HTTPS and headers

```apache
<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

```apache
<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

```apache
<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:

1. What three things define an origin?
2. Does CORS block the request, the response, or both in all cases?
3. Why is `Access-Control-Allow-Origin: *` incompatible with credentialed browser flows?
4. Which requests cause a preflight and why?
5. Why is CORS not a defense against CSRF by itself?
6. Why should `OPTIONS` handling often be optimized at the web-server or proxy layer instead of waking the whole app?

## Route-by-route test commands

```bash
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;
* `OPTIONS` not implemented for real browser behavior, even though API docs look fine;
* `nosniff` missing 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](/application-security-and-secure-sdlc/index-2/browser-security-foundations-csp-cors-cookies-and-sessions.md)
* [Security Headers and Reference Configurations](https://github.com/D3One/Product-Security-Gitbook/blob/main/18-frontend-and-browser-security/security-headers-and-reference-configurations.md)
* [Frontend Security Review Playbook](https://github.com/D3One/Product-Security-Gitbook/blob/main/18-frontend-and-browser-security/frontend-security-review-playbook.md)
* [CSP, SRI, and Third-Party JavaScript Control Patterns](https://github.com/D3One/Product-Security-Gitbook/blob/main/18-frontend-and-browser-security/csp-sri-and-third-party-javascript-control-patterns.md)
* [snippets/frontend/nginx-security-headers.conf](https://github.com/D3One/Product-Security-Gitbook/blob/main/snippets/frontend/nginx-security-headers.conf)
* [snippets/frontend/apache-security-headers.conf](https://github.com/D3One/Product-Security-Gitbook/blob/main/snippets/frontend/apache-security-headers.conf)
* [assets/report-samples/web-scanner-header-findings-sample.pdf](https://github.com/D3One/Product-Security-Gitbook/blob/main/assets/report-samples/web-scanner-header-findings-sample.pdf)

***

*Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.*


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.product-security.expert/application-security-and-secure-sdlc/index-2/web-server-security-headers-https-cors-csp-and-hsts-for-apache-and-nginx.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
