All guides

Allow embedding on Nginx

Remove X-Frame-Options from your Nginx config and hide it if your app sends it. Then add a Content-Security-Policy header with frame-ancestors and the always flag. Repeat server-level add_header lines in any location that has its own add_header.

4 min read · Updated

Find where the header comes from

Search the whole config. Security snippets often hide in an included file:

grep -Rni "x-frame-options\|frame-ancestors" /etc/nginx/

Also check the app behind Nginx. A framework or a library such as Helmet can send the header too. Nginx passes it on unless you hide it.

Allow one partner site

server {
    listen 443 ssl;
    server_name your-site.com;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # Drop the header if the app sends it
        proxy_hide_header X-Frame-Options;

        add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always;
    }
}
  • Delete each add_header X-Frame-Options ... line in the http, server, and location blocks, and in included files. Use the CSP frame-ancestors for framing rules.
  • Use proxy_hide_header for proxy_pass. For PHP-FPM, use fastcgi_hide_header X-Frame-Options;.
  • Keep always. Without it, Nginx adds the header only to some status codes, such as 200 and 301. Error pages would miss it.

To allow any site, use frame-ancestors *. Do this only for pages that have no buttons that change data.

Allow framing on one path only

Keep the rest of the site locked. Open only the embed path:

server {
    # listen, server_name, and your other locations stay as they are

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    location /embed/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_hide_header X-Frame-Options;

        add_header Content-Security-Policy "frame-ancestors https://partner.example" always;
        add_header X-Content-Type-Options "nosniff" always;
    }
}

Nginx inherits add_header lines from the level above only if the current level has no add_header of its own. The location above has one, so it gets none of the server lines. That removes X-Frame-Options here, which you want. It also removes X-Content-Type-Options, so you must repeat it.

Nginx 1.29.3 added add_header_inherit merge. With merge, the location also gets the server-level X-Frame-Options. Then you must remove that line from the server block.

Edit an existing CSP, do not add a second one

If the app already sends a Content-Security-Policy, an extra add_header creates a second policy. The browser enforces both, so the stricter one still blocks the frame.

Pick one owner for the policy:

  • Change frame-ancestors in the app, and do not add CSP in Nginx.
  • Or hide the app policy with proxy_hide_header Content-Security-Policy; and write the full policy in Nginx. Copy every directive the app used, not only frame-ancestors.

Check the result

Test the config and reload:

sudo nginx -t && sudo nginx -s reload

Then read the headers:

curl -sI https://your-site.com/embed/ | grep -iE "x-frame-options|content-security-policy"

You want one Content-Security-Policy line with your frame-ancestors, and no X-Frame-Options line. Check a 404 URL too. Clear the CDN cache if a CDN sits in front of Nginx.

Common mistakes

  • No always flag. Headers vanish on 4xx and 5xx responses.
  • add_header inside if. It follows the same inheritance rule and is hard to read. Use a map or a separate location.
  • ALLOW-FROM. Browsers ignore it, so it allows every site. See X-Frame-Options.
  • A CDN adds the header again. Cloudflare and other CDNs can add X-Frame-Options after Nginx. See Allow embedding on Cloudflare.

Confirm from outside

Paste the page URL into the iframe tester. It shows if the page can load in a frame and which header decides it.

To check many pages at once, use the bulk check. It shows X-Frame-Options and frame-ancestors for up to 100 URLs.

Sources

Check your page now

Know before you embed.

Free, no sign-up, nothing stored. Paste a URL and get a verdict, a live preview, and the fix.