An application opens inside the portal as an iframe. When the browser refuses to render that frame, you get an empty rectangle or a message that the site does not allow the connection — and Bitrix24 (Alaio) explains nothing, because it never made the decision. The refusal comes from the browser, based on response headers the application's own host sent.
Why does the portal show an empty frame instead of the app?
A framing policy belongs to the framed document, not to the page doing the framing. The browser loads the app URL, reads its response headers, compares the ancestor chain — here, the portal domain — against what those headers permit, and discards the render if embedding is forbidden. Nothing on the portal side takes part in that check, which is why "broken in the portal, fine in a tab" is the normal symptom. Two headers control it: the legacy X-Frame-Options and the frame-ancestors directive of Content-Security-Policy.
What is the difference between X-Frame-Options and frame-ancestors?
X-Frame-Options has two meaningful values: DENY (never embeddable) and SAMEORIGIN (embeddable only by same-origin pages). A third, ALLOW-FROM, is obsolete — modern browsers ignore the whole header when they see it. So this header cannot name an external portal at all: SAMEORIGIN blocks it, DENY blocks everyone, and the exception syntax no longer works.
Content-Security-Policy: frame-ancestors replaces it and takes a source list: 'none', 'self', concrete hosts, or a scheme — which is what lets an app permit precisely the portals it serves. It cannot be delivered in a <meta> tag; an HTTP response header is the only option. And per the CSP specification an enforced frame-ancestors makes X-Frame-Options ignored — but do not build on that override: if a stale X-Frame-Options is being emitted, remove it rather than out-vote it.
Which value should the application host send?
One header on the responses of the pages that get framed:
Content-Security-Policy: frame-ancestors 'self' https://your-domain.bitrix24.com;
On nginx: add_header Content-Security-Policy "frame-ancestors 'self' https://your-domain.bitrix24.com" always;, then nginx -t and a reload. An app serving many portals must list each one, or use a wildcard host pattern — a real trade-off, since clickjacking protection is the point of the directive.
The one exception to "fix it on the app side" is a self-hosted instance serving the framed pages itself: its Proactive Defense frame protection adds the header, and the exclusion is an URL mask under Settings — Proactive Defense — Frame Protection — Exceptions.
How do I find which header is actually blocking?
Open the browser console with the portal page loaded. The message names the culprit: a refusal to display a URL in a frame because it set X-Frame-Options, or a refusal to frame a URL because an ancestor violates the frame-ancestors directive, with the offending policy printed. Then read the response headers of the frame request in the Network tab, not the portal document; curl -I on the app URL answers the same without a browser.
Finding who added it is the harder half. Application code, web server, reverse proxy and CDN each attach headers independently, and two of them can emit a duplicate. On nginx, add_header in a nested block drops everything inherited from the parent — so a header you "already set" can be missing in the very location that serves the app.
What if the header is correct and the frame is still blank?
Several unrelated causes look identical. Mixed content: an HTTPS portal will not embed an HTTP app URL, silently. A self-signed or expired certificate: the interstitial you could click through in a tab cannot be accepted inside a frame, so it stays empty. Cookies with SameSite=Lax — the default — are not sent to a third-party frame at all; SameSite=None; Secure is required, and some browsers restrict third-party cookies anyway, so a cookie-only session is fragile here. And the app may just be answering 404 or 500 — REST API error codes maps the responses when the failure is on the REST side, while an app that loads but reports itself missing is the case in application not installed.
What comes up most often?
Can portal settings fix it? For cloud portals, no — the header lives on the app host. Does ALLOW-FROM work? No, obsolete and ignored. Can CSP go in a <meta> tag? Not frame-ancestors. Is dropping the headers safe? It works, but loses clickjacking protection — list the portal origins instead. None of this touches an incoming webhook: server-to-server, no frame involved.
What is the order to work through?
Read the browser console to see whether X-Frame-Options or frame-ancestors produced the refusal, find which component emits it, and replace it with a frame-ancestors list naming the portal origins — on the application's own host. If the header is provably right and the frame is still empty, move on to mixed content, certificates and cookie policy.