> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Demo HTTP Headers

> Configuring NGINX to add security HTTP headers, terminate TLS and load balance to Apache backends while forwarding client proxy headers for accurate backend logging.

In this lesson you'll configure an NGINX server to:

* Add common HTTP security headers so browsers handle your site more safely.
* Extend NGINX to act as a TLS-terminating load balancer that forwards requests to two Apache backends.
* Preserve original client metadata for backend logging by sending proxy headers (client IP, forwarded chain, and original protocol).

We'll first inspect the site with no custom headers, then add security headers and verify them. Finally we'll add an upstream and proxy headers so the backend Apache servers can log the original client information.

Initial local DNS record (example)

```bash theme={null}
root@nginx ~ ➜  cat /etc/hosts
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
fe00::0         ip6-localnet
ff00::0         ip6-mcastprefix
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
192.231.70.6    nginx
127.0.0.1       example.com

root@nginx ~ ➜
```

A quick initial test (no custom headers yet). Note the 200 OK and the default Server header from NGINX:

```bash theme={null}
root@nginx ~ ➜  curl --head https://example.com
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 12 Feb 2025 19:24:14 GMT
Content-Type: text/html
Content-Length: 8710
Last-Modified: Wed, 12 Feb 2025 18:42:19 GMT
Connection: keep-alive
ETag: "67aceb8b-2206"
Accept-Ranges: bytes

root@nginx ~ ➜
```

Open your browser DevTools → Network and inspect a resource to view the response headers (Date, Server, Content-Type, etc.). We will add security headers that improve how browsers treat your site and confirm the headers in both curl and browser responses.

We're also going to configure NGINX as a load balancer and forward proxy headers so backend Apache servers receive the original client information. Without these proxy headers, backend logs will show the load balancer IP as the client for every request.

![A simple architecture diagram showing a client browser sending requests to an NGINX load balancer, which forwards traffic to two backend Apache web servers (labeled 1 and 2). Icons for monitoring/health checks are also shown.](https://mintcdn.com/kodekloud-c4ac6d9a/x1wBthvq78ZWHRHX/images/Nginx-for-Beginners/Security/Demo-HTTP-Headers/nginx-load-balancer-apache-backends-health.jpg?fit=max\&auto=format\&n=x1wBthvq78ZWHRHX\&q=85\&s=d4905fc6ad07d197ffddd7af2fd2fbb4)

These proxy headers allow the backend web servers to see where the request originated from and whether it arrived via HTTPS.

## Add security headers to the NGINX site config

* Edit your site config (for example `/etc/nginx/sites-available/example-https`).
* Place the headers inside the `server { listen 443 ssl; ... }` block to apply them globally for that TLS server (you may also set them per `location` if needed).

Example NGINX server configuration with common security headers:

```nginx theme={null}
server {
    listen 80;

    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/certs/example.com-key.pem;

    root /var/www/html;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    add_header X-Frame-Options "SAMEORIGIN";
    add_header Content-Security-Policy "default-src 'self'";
    add_header Referrer-Policy "origin";

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    location / {
        # First attempt to serve request as file, then as directory,
        # then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }
}
```

Table — security headers included and their purpose:

| Header | Purpose | Example value |
| - | - | - |
| `Strict-Transport-Security` | Enforce HTTPS for future requests and optionally include subdomains | `max-age=31536000; includeSubDomains; preload` |
| `X-Frame-Options` | Prevent clickjacking by restricting framing | `SAMEORIGIN` |
| `Content-Security-Policy` | Restrict where resources can be loaded from to mitigate XSS | `default-src 'self'` |
| `Referrer-Policy` | Control information sent in the Referer header | `origin` |

By default `add_header` applies only to certain response codes (200, 204, 301, 302, 303, 304, 307, 308). To include headers on error responses as well, append the `always` parameter, for example: `add_header Strict-Transport-Security "..." always;`. Remember to include semicolons at the end of each directive.

Validate and reload NGINX:

```bash theme={null}
root@nginx /etc/nginx/sites-available ➜ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

root@nginx /etc/nginx/sites-available ➜ nginx -s reload
```

Verify headers are present:

```bash theme={null}
root@nginx /etc/nginx/sites-available → curl --head https://example.com
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 12 Feb 2025 19:28:15 GMT
Content-Type: text/html
Content-Length: 8710
Last-Modified: Wed, 12 Feb 2025 18:42:19 GMT
Connection: keep-alive
ETag: "67aceb8b-2206"
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: default-src 'self'
Referrer-Policy: origin
Accept-Ranges: bytes

root@nginx /etc/nginx/sites-available →
```

You should now also see these headers in the browser DevTools → Network → Response headers.

## Add upstream and proxy headers for backend logging

To make backend Apache servers see the original client IP, forwarded chain, and protocol, create an `upstream` block and configure `proxy_set_header` in the `location` that proxies requests.

Example NGINX config with upstream and proxy headers:

```nginx theme={null}
# Upstream configuration
upstream example {
    server node01:443;
    server node02:443;
}

server {
    listen 80;

    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/certs/example.com-key.pem;

    root /var/www/html;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    add_header X-Frame-Options "SAMEORIGIN";
    add_header Content-Security-Policy "default-src 'self'";
    add_header Referrer-Policy "origin";

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass https://example;
    }
}
```

If you proxy to upstreams over HTTPS (for example `node01:443`), enable SNI and configure TLS verification appropriately. Use `proxy_ssl_server_name on;` and set `proxy_ssl_trusted_certificate` with `proxy_ssl_verify on;` for production. Disabling verification (`proxy_ssl_verify off;`) may be acceptable in isolated labs but is unsafe for production.

Note: when using `proxy_pass` to a named `upstream` that uses HTTPS, NGINX will not automatically set the backend server name for SNI unless `proxy_ssl_server_name on;` is present and the upstream server name is resolvable.

Test and reload NGINX after changes:

```bash theme={null}
root@nginx /etc/nginx/sites-available ➜ nginx -t
root@nginx /etc/nginx/sites-available ➜ nginx -s reload
```

## Inspect backend Apache logs

Default Apache logs are usually under `/var/log/apache2/`. Tail the `access.log` to watch requests as the load balancer forwards them.

Example of following the Apache access log before proxy headers were added:

```bash theme={null}
root@node01 /var/log/apache2 ➜ tail -f access.log
example.com:443 127.0.0.1 - - [12/Feb/2025:19:17:52 +0000] "GET / HTTP/1.1" 200 11223 "-" "curl/7.81.0"
example.com:443 192.231.70.4 - - [12/Feb/2025:19:30:42 +0000] "GET / HTTP/1.0" 200 3621 "https://k32e7jpvqa7xtxil.kk-lab-dev.kodekloud.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
```

To include the proxy headers in Apache logs, update your `LogFormat` to reference them. Example `LogFormat` that includes the incoming proxy headers:

```apache theme={null}
# Custom vhost combined format including proxy headers
LogFormat "%v:%p \"%{X-Real-IP}i\" \"%{X-Forwarded-For}i\" \"%{X-Forwarded-Proto}i\" %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
```

After editing Apache config:

```bash theme={null}
root@node01 /etc/apache2 ➜ apachectl -t
root@node01 /etc/apache2 ➜ systemctl restart apache2
```

Now tail the access log again — the proxy headers should appear in the entries (the X header values will appear in double quotes as shown):

```text theme={null}
example.com:443 "192.231.70.4" "174.0.252.84, 34.117.152.159, 169.254.169.126, 192.168.1.144, 192.231.70.4" "https" 192.231.70.4 - - [12/Feb/2025:19:37:22 +0000] "GET / HTTP/1.0" 200 3621 "https://k32e7jpvqa7xtxil.kk-lab-dev.kodekloud.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
```

Notes on header semantics:

* `X-Forwarded-For` is typically a comma-separated chain of IPs representing each proxy hop; the original client IP is usually the left-most entry in a well-formed chain.
* `X-Real-IP` commonly contains the immediate client's IP as seen by the last proxy (useful if you only have a single proxy).
* `X-Forwarded-Proto` indicates whether the original request used `http` or `https`.

## Recap

* Verified the site before adding headers and inspected response headers.
* Added common security headers in the NGINX TLS server block: `Strict-Transport-Security`, `X-Frame-Options`, `Content-Security-Policy`, `Referrer-Policy`.
* Configured an `upstream` with two backend Apache servers and added `proxy_set_header` directives so backends receive original client IP, forwarded chain, and protocol.
* Updated Apache `LogFormat` to log these incoming proxy headers, so logs reflect accurate request origin and protocol.

Further reading and references:

* [NGINX: Adding Response Headers](https://nginx.org/en/docs/http/ngx_http_headers_module.html)
* [NGINX: Proxying and Load Balancing](https://nginx.org/en/docs/http/load_balancing.html)
* [Apache Logging: LogFormat Directive](https://httpd.apache.org/docs/current/mod/mod_log_config.html)

You can expand this setup with additional security headers, caching, compression, access controls, or Apache modules like mod\_remoteip depending on your needs. Thanks, and happy configuring!

> **[Watch Video](https://learn.kodekloud.com/user/courses/nginx-for-beginners/module/8905470e-b1ea-48ec-b0cd-711687ce7159/lesson/ad952d6c-3932-42af-88c6-a41c7168fa07)**

> **[Practice Lab](https://learn.kodekloud.com/user/courses/nginx-for-beginners/module/8905470e-b1ea-48ec-b0cd-711687ce7159/lesson/e597c364-ed3e-403b-98d1-5138c40a7d5c)**


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.