Skip to main content
In this lesson we configure an NGINX server to return a set of security HTTP headers, then extend that configuration to act as a TLS-terminating load balancer that forwards requests to two Apache backend servers. We also pass useful proxy headers so the Apache backends can log the original client information for debugging and auditing. Flow overview:
  • Inspect current response headers from the site.
  • Add security headers to the TLS (listen 443 ssl) server block.
  • Configure an upstream block and proxy traffic to two Apache backends.
  • Add proxy_set_header directives so the backend sees the original client IP and scheme.
  • Update Apache logging to include forwarded headers or use mod_remoteip.
A simple diagram showing a desktop computer on the left communicating with a website/browser window on the right via dashed arrows to represent requests and responses. Icons of a magnifying glass over code and a small HTTP request/status box indicate inspection and response details.
We will then configure NGINX as a load balancer to distribute traffic to two backend Apache servers and include proxy headers so the Apache logs record useful client information (instead of all requests appearing to come from the load balancer).
A simple architecture diagram showing a user/browser sending requests to an NGINX load balancer which distributes traffic to two backend web servers. The two web servers are running Apache HTTP Server and are labeled 1 and 2.

1) Initial checks — inspect current headers

I added an internal DNS entry for example.com pointing to loopback. For example, your /etc/hosts may include:
Check headers with curl:
In the browser: open DevTools → Network → select a resource to view response headers. At this point the server returns standard headers such as Server and Date, but no custom security headers yet.

2) Add security headers in NGINX (TLS server block)

Edit your site config (e.g., /etc/nginx/sites-available/example-https) and add the security headers inside the server { listen 443 ssl; ... } block. Example configuration:
Save, test and reload NGINX:
Re-check response headers:
You should now see Strict-Transport-Security, X-Frame-Options, Content-Security-Policy, and Referrer-Policy in DevTools → Network for resource responses.
Strict-Transport-Security (HSTS) instructs browsers to access the site only over HTTPS. When testing, use a conservative max-age (for example, a few hours) before committing a long duration or adding preload. For more, see the HSTS specification and browser docs.
Quick reference — common security headers:

3) Configure NGINX as a load balancer (upstream block)

Add an upstream block and change the site location / to proxy requests to the example upstream. Initially this will forward traffic, but backends will only see the load balancer IP unless we forward proxy headers. Example:
Test and reload NGINX after editing.

4) Forward proxy headers so backends see original client info

To ensure Apache backends can log and act on the original client IP and protocol, set the appropriate proxy headers inside the location block. Update location /:
Notes:
  • X-Real-IP sends the immediate client IP as seen by NGINX ($remote_addr).
  • X-Forwarded-For accumulates client IPs across hops; $proxy_add_x_forwarded_for appends the current hop.
  • X-Forwarded-Proto tells the backend whether the original request used http or https.
  • Always end directives with semicolons.
Test and reload:

5) Inspect backend Apache logs and include proxy headers in log format

On the Apache backend nodes, check access logs:
Tail the access log to observe incoming requests:
To log the proxy headers forwarded by NGINX, add or update an Apache LogFormat (often in /etc/apache2/apache2.conf or an included conf-enabled file). Example LogFormat additions:
Test and restart Apache:
If you prefer the remote host (%h) to reflect the original client IP automatically, consider using Apache’s mod_remoteip which rewrites the client IP based on trusted proxy headers.
If you receive X-Forwarded-For from trusted proxies, enable Apache’s mod_remoteip (see the official docs) so %h and access control reflect the real client IP. Only enable this when you trust the upstream proxies.
Relevant links:

6) Compare log output (before and after)

Before forwarding proxy headers, Apache access logs typically show the load balancer IP:
After adding proxy_set_header and an extended LogFormat, log lines can include the forwarded IPs and protocol, improving traceability:
Field meanings:
  • First quoted field: X-Real-IP (immediate client IP seen by NGINX).
  • Long comma-separated list: X-Forwarded-For (client IP chain across proxies).
  • Next quoted field: X-Forwarded-Proto (original request scheme, e.g., https).
This makes it much easier to trace request origin and diagnose issues across multiple proxy layers.

7) Recap and next steps

  • Inspected default response headers and added security headers in the NGINX TLS server block.
  • Implemented an upstream with two Apache backend nodes and proxied TLS traffic.
  • Added proxy_set_header directives (X-Real-IP, X-Forwarded-For, Host, X-Forwarded-Proto) so backends can see the original client context.
  • Updated Apache LogFormat to include forwarded headers or considered mod_remoteip to rewrite %h.
Recommended next topics:
  • Enforce stricter Content-Security-Policy rules and test with CSP reports.
  • Harden TLS with modern ciphers and TLS versions (see Mozilla SSL configuration guide).
  • Add caching, compression, and authentication at the NGINX edge.
  • Monitor and alert on access logs and security header violations.
Thanks for following along. Further reading and references:

Watch Video

Practice Lab