Allow embedding on Apache
Load mod_headers, unset X-Frame-Options with both Header unset and Header always unset, and set a Content-Security-Policy header with frame-ancestors. Apache keeps headers in two tables, so one unset line often misses the header.
4 min read · Updated
Before you start
You need mod_headers. On Debian and Ubuntu, turn it on and restart:
sudo a2enmod headers && sudo systemctl restart apache2
Then find every place that sets the header:
grep -Rni "x-frame-options\|frame-ancestors" /etc/apache2/ /var/www/
On Red Hat systems, search /etc/httpd/ in place of /etc/apache2/. Also check .htaccess files and your app. PHP apps and CMS plugins often send the header themselves.
Allow one partner site
Put this in the <VirtualHost> block or in .htaccess:
<IfModule mod_headers.c>
Header unset X-Frame-Options
Header always unset X-Frame-Options
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example"
</IfModule>
Why two unset lines? Apache keeps response headers in two tables:
- The default table, called
onsuccess. PlainHeader setwrites here. - The
alwaystable. Apache uses it for error responses too. Headers from CGI scripts andmod_proxy_fcgi, such as PHP-FPM, also land here.
One table is not part of the other. An unset on one table does not touch the other. Use both lines, and the header is gone wherever it came from.
Use always set for the CSP, so 404 and 500 pages get it too.
For .htaccess, the directory needs AllowOverride FileInfo or AllowOverride All. If nothing changes, Apache may be ignoring the file.
Allow framing on one path only
Leave the site locked and open only the embed path:
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
<Location "/embed/">
Header unset X-Frame-Options
Header always unset X-Frame-Options
Header always set Content-Security-Policy "frame-ancestors https://partner.example"
</Location>
</IfModule>
<Location> works in the server and virtual host config, not in .htaccess. In .htaccess, use <If "%{REQUEST_URI} =~ m#^/embed/#"> around the same lines.
Do not stack two CSP headers
Header always set Content-Security-Policy replaces a CSP in the always table only. If your app sends its own CSP, you can end up with two policies. The browser enforces both, so the stricter one blocks the frame.
Pick one place for the policy. Either change frame-ancestors in the app, or unset the app policy in both tables and write the full policy in Apache:
Header unset Content-Security-Policy
Header always unset Content-Security-Policy
Header always set Content-Security-Policy "default-src 'self'; frame-ancestors 'self' https://partner.example"
Copy every directive the app used. Dropping them weakens your site.
Check the result
sudo apachectl configtest && sudo systemctl reload apache2
curl -sI https://your-site.com/embed/ | grep -iE "x-frame-options|content-security-policy"
You want one CSP line with your frame-ancestors and no X-Frame-Options.
Common mistakes
- Only
Header unset. A header from PHP-FPM stays, because it sits in thealwaystable. ALLOW-FROM. Browsers ignore it, so every site can frame the page. Useframe-ancestors.- A meta tag.
frame-ancestorsand X-Frame-Options do nothing in<meta>. - A CDN in front. It can add the header after Apache. Check the response from outside, not only on the server.
Test from outside
Paste the page URL into the iframe tester. It tells you if the page can load in a frame now, and which header still blocks it if not.
To check many pages at once, use the bulk check. It shows X-Frame-Options and frame-ancestors for up to 100 URLs.