All guides

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. Plain Header set writes here.
  • The always table. Apache uses it for error responses too. Headers from CGI scripts and mod_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 the always table.
  • ALLOW-FROM. Browsers ignore it, so every site can frame the page. Use frame-ancestors.
  • A meta tag. frame-ancestors and 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.

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.