How to add custom response headers in Nginx

Custom response headers let Nginx stamp every reply with security policy, cache instructions, routing hints, or integration metadata without changing application code. That is useful when a site needs consistent browser behavior, an operator needs proof of which layer served a request, or a proxy must attach a site-wide policy before the response leaves the server.

In Nginx, response headers are added with add_header name value [always]; inside the http, server, location, or if-inside-location contexts. By default the directive only applies to a defined set of success and redirect status codes, so headers can disappear on 404 and 500 responses unless the always parameter is used. Multiple add_header lines are allowed, and current upstream documentation also notes that newer builds can change inheritance behavior with add_header_inherit.

The clearest place to start is the target site's server block so the change stays scoped to one hostname or application. The examples use the common Debian and Ubuntu layout (/etc/nginx/sites-available/ and the nginx service name). If the same header already comes from the application, a CDN, or another proxy layer, keep one source of truth to avoid duplicate header lines. A current Ubuntu 24.04 package example (nginx/1.24.0) still uses the older inheritance model and does not support add_header_inherit yet.

Steps to add custom response headers in Nginx:

  1. Decide which headers and values should be sent.

    Common site-wide security examples are X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, and Referrer-Policy: strict-origin-when-cross-origin.

  2. Open the Nginx site configuration file for the host that should send the headers.
    $ sudoedit /etc/nginx/sites-available/example.com

    Site configs are commonly stored under /etc/nginx/sites-available/ (symlinked into /etc/nginx/sites-enabled/) or /etc/nginx/conf.d/ depending on the distribution.

  3. Locate the server block that serves the target site.
    server {
        listen 80;
        listen [::]:80;
        server_name example.com;
        root /var/www/html;
    
        location / {
            try_files $uri $uri/ =404;
        }
    }
  4. Add the required add_header directives inside that scope.
    server {
        listen 80;
        listen [::]:80;
        server_name example.com;
        root /var/www/html;
    
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
        location / {
            try_files $uri $uri/ =404;
        }
    }
    • add_header can be repeated for each header that should be sent.
    • always keeps the header on locally generated non-success responses such as 404 Not Found and 500 Internal Server Error instead of limiting it to the default success and redirect status codes.
    • If the header should apply to every virtual host, place the directives higher in the http block instead of repeating them in each server block.

    Policies such as Content-Security-Policy, Permissions-Policy, and Strict-Transport-Security can break embeds, third-party scripts, OAuth flows, or subdomains when values are too strict. Validate them in staging before broad rollout.

  5. Repeat required headers in nested location blocks that add their own headers, or use add_header_inherit merge; on Nginx 1.29.3 and newer builds.
    location /api/ {
        add_header Content-Security-Policy "default-src 'self'" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
        try_files $uri =404;
    }

    If a nested block defines any add_header directive, older builds stop inheriting headers from the parent block. When nginx -t reports unknown directive "add_header_inherit", repeat the required headers manually in the child block.

    Current upstream Nginx documentation says add_header_inherit merge; became available in version 1.29.3 and appends parent headers to the current block instead of replacing them.

  6. Test the Nginx configuration before reloading it.
    $ sudo nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  7. Reload Nginx so new worker processes pick up the header rules.
    $ sudo systemctl reload nginx

    On systems that do not use systemd, sudo nginx -s reload asks the running master process to reload the configuration.

  8. Verify the headers on a normal response.
    $ curl --head --silent http://127.0.0.1/index.html | grep -iE '^(HTTP/|x-frame-options:|x-content-type-options:|referrer-policy:)'
    HTTP/1.1 200 OK
    X-Frame-Options: SAMEORIGIN
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin

    Use the real site URL instead of 127.0.0.1 when a reverse proxy, load balancer, or CDN sits in front of Nginx and the final client response matters more than the local origin response.

  9. Verify the headers on an error response to confirm the always behavior.
    $ curl --head --silent http://127.0.0.1/does-not-exist | grep -iE '^(HTTP/|x-frame-options:|x-content-type-options:|referrer-policy:)'
    HTTP/1.1 404 Not Found
    X-Frame-Options: SAMEORIGIN
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin

    If the headers appear on the 200 OK response but disappear on the 404 response, the relevant add_header lines are probably missing the always parameter.

  10. Check for duplicate header lines after the change.
    $ curl --head --silent http://127.0.0.1/index.html | grep -i '^x-frame-options:'
    X-Frame-Options: SAMEORIGIN

    Repeated lines usually mean the same header is being set in more than one layer. Keep one authoritative source or hide upstream copies with proxy_hide_header or fastcgi_hide_header where appropriate.