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 thehttp,server, andlocationblocks, and in included files. Use the CSPframe-ancestorsfor framing rules. - Use
proxy_hide_headerforproxy_pass. For PHP-FPM, usefastcgi_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-ancestorsin 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 onlyframe-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
alwaysflag. Headers vanish on 4xx and 5xx responses. add_headerinsideif. It follows the same inheritance rule and is hard to read. Use amapor a separatelocation.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.