Allow Specific Origins to Frame Your Site

This guide is part of the Cross-Origin Frame Controls: X-Frame-Options & frame-ancestors reference. It covers the one framing scenario the legacy header physically cannot express: letting a small, named set of partner origins embed your pages in an <iframe> while blocking every other site. The only mechanism that works in current browsers is the Content-Security-Policy frame-ancestors directive with an explicit source list. The dead X-Frame-Options: ALLOW-FROM syntax must be removed, not adapted.

Configuration Syntax & Exact Values

The task is an allowlist: your own origin plus one or more trusted https partner origins may frame the page; everyone else is refused. frame-ancestors takes a source list, so you enumerate exactly those origins and nothing else.

Content-Security-Policy: frame-ancestors 'self' https://partner.example.com https://portal.vendor.net

Every token in that list is load-bearing. Read it left to right:

There is no comma anywhere. Sources are separated by single spaces; a stray comma starts a new directive under CSP grammar and corrupts the policy.

Annotated frame-ancestors allowlist The header string broken into its directive name, self keyword, and two explicit https partner origins with a callout for each token. Content-Security-Policy: frame-ancestors 'self' https://partner.example.com https://portal.vendor.net directive name your own origin quotes required partner 1 — explicit https scheme partner 2 space-separated, no comma
Each token in the allowlist and what it authorizes.

What you must NOT do

X-Frame-Options cannot express a multi-origin allowlist. Its ALLOW-FROM uri form was a Microsoft extension that shipped only in IE and old Edge, was never implemented in Chromium or Gecko, and is now removed everywhere. A header like X-Frame-Options: ALLOW-FROM https://partner.example.com is treated as an invalid value: modern browsers ignore the whole header, meaning the page becomes framable by anyone, the exact opposite of the intent. Delete it.

# DEAD — never ships. Browsers ignore this and the page ends up framable by all.
X-Frame-Options: ALLOW-FROM https://partner.example.com

If you keep an X-Frame-Options header at all during migration, the only defensible value is SAMEORIGIN as a fallback for pre-2015 engines — and note that SAMEORIGIN blocks your named partners on those old browsers. There is no XFO value that permits a foreign origin. This is why the allowlist scenario is frame-ancestors-only. See the X-Frame-Options vs CSP frame-ancestors comparison for the full precedence contract.

Server-Side Configuration

Ship the directive on the framed response every time — framing is enforced per navigation, with no client-side cache. If you already send a broader CSP for scripts and styles, frame-ancestors must live on that same header line, not a second Content-Security-Policy header. Ordering matters: see the edge cases below.

Nginx

add_header is silently dropped in any location that later sets its own headers, and it does not fire on internally generated error responses unless flagged. Use always so the policy is present on 4xx/5xx bodies that an attacker might frame too.

# In the server or location block. 'always' emits the header on error responses as well.
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example.com https://portal.vendor.net" always;

# Remove any legacy allowlist attempt; do not send X-Frame-Options ALLOW-FROM.
proxy_hide_header X-Frame-Options;

If you use the third-party headers-more module, more_set_headers replaces rather than appends and applies to all status codes by default, which avoids the duplicate-CSP trap:

more_set_headers "Content-Security-Policy: frame-ancestors 'self' https://partner.example.com https://portal.vendor.net";
more_clear_headers "X-Frame-Options";

Apache

Header always set guarantees the header on error documents; the default set (without always) skips them. unset strips any inherited XFO allowlist.

# httpd.conf, VirtualHost, or .htaccess with mod_headers enabled.
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example.com https://portal.vendor.net"
Header always unset X-Frame-Options

See the Apache hardening guide for merging this with an existing CSP.

Cloudflare (Transform Rule)

A Cloudflare Response Header Transform Rule sets the value at the edge. Set the CSP header and remove any origin-sent X-Frame-Options.

{
  "action": "rewrite",
  "action_parameters": {
    "headers": {
      "Content-Security-Policy": {
        "operation": "set",
        "value": "frame-ancestors 'self' https://partner.example.com https://portal.vendor.net"
      },
      "X-Frame-Options": { "operation": "remove" }
    }
  }
}

Express / Helmet

Configure the directive through Helmet, and disable Helmet’s default X-Frame-Options so it does not fight your CSP.

const helmet = require("helmet");

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: false,
      directives: {
        "frame-ancestors": ["'self'", "https://partner.example.com", "https://portal.vendor.net"],
      },
    },
    frameguard: false, // stop Helmet from emitting X-Frame-Options: SAMEORIGIN
  })
);

Django

In Django, the built-in XFrameOptionsMiddleware cannot express an allowlist, so set the CSP header directly and leave XFO off. django-csp 4.x uses the CONTENT_SECURITY_POLICY setting:

# settings.py — django-csp 4.x. Emit frame-ancestors and do not enable XFrameOptionsMiddleware.
CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "frame-ancestors": ["'self'", "https://partner.example.com", "https://portal.vendor.net"],
    },
}

Diagnostic & Verification Steps

Confirm the wire bytes first, then confirm real browser framing behavior.

1. Inspect the response header with curl. -s silences the progress meter, -I issues a HEAD so you see only headers:

curl -sI https://your-site.example/embed-page | grep -i -E 'content-security-policy|x-frame-options'

Expected output — a single CSP line naming your allowlist and no X-Frame-Options:

content-security-policy: frame-ancestors 'self' https://partner.example.com https://portal.vendor.net

If two content-security-policy lines appear, the browser intersects them and the stricter one may drop your partners. Collapse them onto one directive set. If x-frame-options still appears, your removal directive did not run.

2. Verify the allowed partner can frame you. Serve a one-line test page from the partner origin and load it:

<!-- hosted at https://partner.example.com/frametest.html -->
<iframe src="https://your-site.example/embed-page" width="600" height="400"></iframe>

The iframe renders normally. In DevTools, the Network panel shows the framed request completing with 200 and no CSP violation on the console.

3. Verify a non-allowlisted origin is refused. Load the same iframe from any origin not in the list. The frame stays blank and the browser console logs a refusal that explicitly cites the directive:

Refused to frame 'https://your-site.example/' because an ancestor
violates the following Content Security Policy directive: "frame-ancestors 'self'
https://partner.example.com https://portal.vendor.net".

The presence of the directive text in the error is the definitive signal that frame-ancestors — not a leftover XFO header — is doing the enforcing.

Allowlist enforcement across three embedding origins A sequence showing an allowed partner and self being rendered while a non-listed origin is refused with a console error citing frame-ancestors. Who tries to frame https://your-site.example/embed-page ? your-site (self) matches 'self' partner.example.com in allowlist evil.example not listed Browser evaluates frame-ancestors on the framed response Rendered Rendered Refused console cites directive
Two origins clear the allowlist; the unlisted origin is refused and logged.

Edge Cases, Security Implications & Safe Rollback

Trap 1 — an existing broad CSP silently overrides you. If your app already sends Content-Security-Policy: default-src 'self' from application code and your web server adds a second Content-Security-Policy header with frame-ancestors, the browser enforces both policies independently. A policy without frame-ancestors does not block framing, but if the first policy happens to include its own frame-ancestors 'none', that stricter one wins and your partners are blocked. Frame-ancestors is not additive across headers in your favor — the intersection applies. Emit a single, merged Content-Security-Policy header that contains every directive including frame-ancestors.

One merged CSP header versus two conflicting headers A comparison matrix contrasting a single merged Content-Security-Policy header that allows partners against two separate headers whose intersection blocks them. Correct: one header Wrong: two headers CSP: default-src 'self'; frame-ancestors 'self' partner CSP: default-src 'self' CSP: frame-ancestors partner Partners can frame single directive set enforced Intersection enforced each policy checked separately a stricter policy can block partners
Keep frame-ancestors on the same header as the rest of your CSP.

Trap 2 — origin, not path. frame-ancestors matches by origin (scheme, host, port). You cannot allow https://partner.example.com/app/ while blocking https://partner.example.com/marketing/; listing the origin authorizes any page on it to frame you. If a partner host runs untrusted user content on the same origin, that content can frame you too. Where a partner cannot isolate to a dedicated origin, framing is the wrong integration; use a signed, purpose-built embed instead.

Trap 3 — subdomains and ports are exact. https://partner.example.com does not match https://app.partner.example.com or https://partner.example.com:8443. Use a host wildcard https://*.partner.example.com only if you genuinely trust every subdomain; a wildcard covers hosts you have not vetted. Enumerate exact origins whenever the partner list is finite.

Safe rollback. Ship frame-ancestors in report-only mode first so a mistake cannot break a partner integration:

Content-Security-Policy-Report-Only: frame-ancestors 'self' https://partner.example.com https://portal.vendor.net; report-to csp-endpoint

Report-only evaluates the policy and emits violation reports without blocking any frame. Watch the reports for legitimate partner origins you missed, add them, then promote the identical directive to the enforcing Content-Security-Policy header. To roll back instantly, revert to frame-ancestors 'self' (partners lose embedding, your site stays protected) rather than removing the header, which would reopen framing to everyone.

Conclusion

Stage the allowlist in a non-production environment and confirm each partner origin renders and each stranger is refused. Move to production behind Content-Security-Policy-Report-Only for one to two weeks, mining violation reports for partner origins you forgot. When the reports are clean, promote the byte-identical frame-ancestors directive to an enforcing Content-Security-Policy header on a single line and delete every trace of X-Frame-Options: ALLOW-FROM.

Frequently Asked Questions

Why can’t I just use X-Frame-Options: ALLOW-FROM for one partner? Because no shipping browser implements it. ALLOW-FROM was an IE/old-Edge-only extension, removed from Chromium and never present in Gecko or WebKit. Sending it produces an invalid header that modern browsers ignore, leaving your page framable by everyone. frame-ancestors is the only mechanism that allowlists foreign origins.

How do I allow more than one partner origin? List each origin space-separated on the same frame-ancestors directive: frame-ancestors 'self' https://a.example https://b.example. There are no commas — a comma starts a new CSP directive and corrupts the policy. Add 'self' so your own pages can still frame each other.

Does frame-ancestors match a specific path on the partner site? No. Matching is by origin (scheme, host, port) only. Listing https://partner.example.com authorizes every page on that origin to embed you. If the partner serves untrusted content on the same origin, that content is authorized too; require a dedicated origin or use a signed embed instead.

Will listing https://partner.example.com also allow its subdomains? No. Origin matching is exact, so app.partner.example.com and partner.example.com:8443 are not covered. Add each exact origin, or use a host wildcard like https://*.partner.example.com only when you trust every subdomain under it.

Do I still need an X-Frame-Options header for old browsers? Only SAMEORIGIN as a fallback, and it will block your named partners on pre-2015 engines that lack frame-ancestors — there is no XFO value that permits a foreign origin. If those partners must work on legacy browsers, framing is not viable there; otherwise drop XFO entirely once legacy traffic is negligible.