Securing Federated Remotes #

Module Federation asks a page to execute JavaScript fetched at runtime from an origin the build never verified. That is a genuine trust decision, and most federated applications make it implicitly. This guide covers what the trust boundary actually is, and the four controls that make crossing it deliberate.

It sits under Core Micro-Frontend Architecture Tradeoffs and pairs with versioning strategies for remote apps, because a remote you cannot pin is a remote you cannot secure. Its step-by-step companions cover content security policy for Module Federation, subresource integrity for remote entry files, validating remote manifests before loading, and isolating untrusted remotes with iframes.

What the trust boundary actually is #

Four controls layered over an absent boundary Federation gives a remote the host's origin, realm, cookies and DOM by default; each control narrows a different part of that access. What each control actually constrains Same origin, same realm remote code runs as the host · no boundary exists by default Content Security Policy constrains where code may come from · an unlisted origin cannot load Subresource Integrity constrains what may arrive · modified bytes never execute Manifest validation constrains what the host asks for · fail closed on an unverifiable entry Iframe isolation removes the shared realm · for code you genuinely do not trust
Each layer answers a question the one above it cannot: where from, what exactly, asked for by whom, and running in whose realm.

A federated remote runs in the host’s origin, in the host’s JavaScript realm, with the host’s cookies, the host’s localStorage, and the host’s DOM. There is no boundary at all by default. The remote is not a service the host calls — it is code the host executes as itself.

That is the correct model for code your own teams write, and it is exactly why federation is convenient. It becomes a problem in three situations that most architectures eventually hit:

The controls in this guide address different points on that spectrum. Content Security Policy constrains where code may come from; Subresource Integrity constrains what may arrive from there; manifest validation constrains what the host will ask for; iframe isolation removes the shared realm entirely for code you genuinely do not trust.

Key objectives #

Setup: a policy that lists your remotes #

The first control is the cheapest. A Content Security Policy that enumerates permitted script origins turns “any URL the host resolves” into “these four origins”.

# nginx — served with the host shell's HTML
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' https://cdn.example.com https://checkout-cdn.example.com;
  connect-src 'self' https://cdn.example.com https://api.example.com;
  style-src 'self' 'unsafe-inline';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
" always;

Two directives matter beyond script-src. connect-src must list the remote origins too, because federation fetches chunks with fetch rather than only with script tags. And base-uri 'self' prevents an injected <base> tag from rewriting where every relative asset — including a remote’s chunks — resolves from.

The trap specific to Module Federation is that the runtime creates script elements dynamically. That is fine under a source-list policy, but it breaks under a strict nonce-based policy unless the nonce is propagated, which the dedicated CSP page covers in detail.

Integration: pinning what may arrive #

What each control stops, and what it does not The controls are complementary rather than alternative: each one leaves a specific gap that the next one closes. Control Stops Does not stop Content Security Policy code from an unlisted origin malicious code from a listed one Subresource Integrity modified bytes at a pinned URL a manifest pointing somewhere else Manifest validation an entry the host should not request a compromised build pipeline Iframe isolation access to host globals and cookies the remote misbehaving inside its frame
Reading the right-hand column is how you decide which controls you actually need for a given remote.

CSP says where code may come from. It says nothing about what arrives. If the CDN serving your remote is compromised, the malicious bundle is served from an allowed origin and CSP permits it.

Subresource Integrity closes that gap by pinning a hash of the exact bytes.

<script
  src="https://cdn.example.com/cart/remoteEntry.a91f3c.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

The awkwardness is that federation usually injects that script itself, so the integrity value has to reach the runtime — which means the manifest has to carry it, and the manifest becomes the thing worth protecting. That is not a weakness of the approach; it is the approach working, concentrating trust into one small artefact you can sign and audit rather than spreading it across every asset.

{
  "cart": {
    "entry": "https://cdn.example.com/cart/remoteEntry.a91f3c.js",
    "integrity": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC",
    "version": "4.2.1"
  }
}

Integration: validating the manifest itself #

Once the manifest decides what runs, its integrity is the whole system’s integrity. Three properties are worth enforcing before the host acts on it.

Every entry URL must match an allow-list of origins, checked in code rather than relying on CSP alone — CSP fails at fetch time, which is late and produces a confusing error. Every entry must carry an integrity hash, so a manifest that omits one is rejected rather than silently downgraded. And the manifest as a whole should be signed, so a host can verify it came from the publishing pipeline rather than from whoever last had write access to the bucket.

// src/verify-manifest.js — reject before fetching anything
const ALLOWED_ORIGINS = new Set([
  'https://cdn.example.com',
  'https://checkout-cdn.example.com',
]);

export function verifyManifest(manifest) {
  for (const [name, spec] of Object.entries(manifest)) {
    const origin = new URL(spec.entry).origin;
    if (!ALLOWED_ORIGINS.has(origin)) {
      throw new Error(`remote "${name}" resolves to unlisted origin ${origin}`);
    }
    if (!/^sha(256|384|512)-/.test(spec.integrity ?? '')) {
      throw new Error(`remote "${name}" has no usable integrity hash`);
    }
  }
  return manifest;
}

Failing closed here is the right default. A host that cannot verify a remote should render without it and report the failure, rather than loading it anyway — the same reasoning as the fail-closed branch in sharing authentication tokens securely across remote apps.

Edge cases #

A remote deploys and the integrity hash goes stale. This is the failure mode that gets integrity checks removed. The hash must be generated by the remote’s own build and published with the manifest in one atomic step, never maintained by hand in the host.

Development builds have no stable hash. Local dev servers rebuild constantly. Enforce integrity in staging and production only, and make the absence of enforcement in development explicit rather than accidental — otherwise a missing hash in production looks like the normal state.

A remote legitimately needs a new origin. Adding an origin should be a reviewed change to both the CSP and the allow-list, in the host’s repository. If that feels slow, that is the control working; origins should not be added casually.

The remote needs to call its own API. connect-src must list it. Remotes routinely forget this because it works in standalone development, where the policy is not applied at all.

Testing and validation #

Check Runs where Catches
CSP report-only in staging Staging, continuously Directives that would break a real page
Manifest schema + origin validation Host CI and at runtime An entry pointing somewhere unexpected
Integrity hash present for every entry Publishing pipeline A remote published without a hash
Tampering simulation Nightly Whether the host actually fails closed

The tampering simulation is the one teams skip and the one that finds real bugs. Serve a remote entry whose bytes do not match its published hash and confirm the host renders without that remote, reports the failure, and does not blank the page. Every part of that sentence is a separate thing that can be wrong.

Roll CSP out in Content-Security-Policy-Report-Only mode first, with a reporting endpoint, and leave it there long enough to see a full traffic cycle. Federated pages load code conditionally, so a directive that looks correct can break a route only a small share of users reach.

Deployment #

The controls have to survive independent deploys, which is the constraint that shapes every practical decision here.

The pattern that works: the remote’s pipeline computes its own integrity hash at build time, publishes the asset immutably, and writes the hash into the manifest as part of the same release step. The host never knows a hash exists — it reads the manifest and passes the value through. A remote release therefore updates its own integrity metadata without touching the host, and the host’s security posture stays constant.

# remote pipeline — hash and publish in one step
- name: Compute integrity
  run: |
    HASH=$(openssl dgst -sha384 -binary dist/remoteEntry.*.js | openssl base64 -A)
    echo "INTEGRITY=sha384-$HASH" >> "$GITHUB_ENV"

- name: Publish manifest entry
  run: |
    node scripts/publish-manifest.mjs \
      --remote cart --version "$VERSION" \
      --entry "$ENTRY_URL" --integrity "$INTEGRITY"

CSP origins are different: they change rarely and belong in the host’s repository under review. Treat the two on different cadences rather than trying to automate both.

Deciding how much isolation a remote actually needs #

Choosing controls by who publishes the remote The right level of isolation follows from the publisher's review standards and pipeline, not from how the remote is technically integrated. How much do you trust the team that publishes this remote? Own org, shared review standards CSP + integrity Same realm is fine Controls catch accidents Own org, separate pipeline Add manifest signing Verify the publisher Fail closed on mismatch Third party or acquired product Iframe with postMessage Separate realm No access to host state
Federation and isolation are not opposites — a third-party remote can still be composed into the page, just not into the realm.

Applying every control to every remote is how security work gets abandoned. The four controls have very different costs: a Content Security Policy is a header, while iframe isolation changes how a remote is built, styled and integrated. Matching the control to the risk is what makes the programme sustainable.

The useful axis is not what the remote does but who publishes it and through what pipeline. A remote built by another team in your organisation, reviewed to the same standard, and published through the same hardened pipeline is not a threat you need a realm boundary for — the controls you want there catch mistakes, not attacks. A remote from an acquired company still running its own release process is a different proposition, and one from a commercial partner is different again.

Three questions separate the cases cleanly. Does the publishing pipeline enforce code review and dependency scanning to your standard? Can someone outside your organisation change what the manifest points at? Would the remote’s code, if hostile, have access to anything that matters — a session token, a payment form, personal data on the page?

If the answer to the last question is yes and either of the first two is uncertain, the remote belongs in its own realm. Everything else is a matter of layering the cheaper controls, and the cheap controls genuinely do most of the work: in practice, far more federated incidents come from a stale asset, a mis-pointed manifest or an unreviewed dependency than from deliberately hostile code.

Handling the dependency supply chain #

The remote’s own dependencies are part of the surface you are securing, and they are the part the host cannot see at all. A remote that passes every integrity check is still executing whatever its node_modules contained at build time.

This is where federation differs from a monolith in a way that matters. In a single application, one lockfile and one dependency review cover the whole page. With federation there are as many lockfiles as there are remotes, each maintained by a different team, and the host has no visibility into any of them. A vulnerable transitive dependency in one remote is executing in the host’s origin with the host’s privileges, and nothing in the host’s own audit will mention it.

The practical answer is to make dependency posture part of the publishing contract rather than something each team decides privately. Three requirements are enough to be useful without being obstructive:

The third requirement is the one that ties back to everything else on this page. Integrity checks, signed manifests and version pinning all assume that a given version of a remote is a specific, unchanging artefact. A build that produces different output from the same source quietly undermines all three.

Rolling this out to an existing federated application #

Adding these controls to a system already in production is a sequencing problem, and the order matters because the early steps generate the information the later ones need.

Start with reporting, not enforcement. Deploy a Content-Security-Policy-Report-Only header with the policy you intend to enforce and a reporting endpoint, and leave it for at least a full week of traffic. Federated pages load code conditionally, so the origins you discover in the first hour are not the complete set. The report stream is also the fastest inventory you will get of what your application actually loads, which is frequently a surprise.

Next, make the manifest authoritative. Many federated applications resolve some remotes from a manifest and others from hard-coded URLs in the host build. Until every remote resolves the same way, manifest validation covers only part of the system and gives false confidence. Consolidating this is usually the largest piece of work in the whole rollout, and it pays for itself independently by making rollback uniform.

Then add integrity, remote by remote, driven by each team’s own pipeline as described above. Because the host simply passes through whatever hash the manifest carries, remotes can adopt this on their own schedule, and a remote without a hash keeps working until you flip validation from warn to fail.

Only then turn CSP from report-only to enforcing, and only then consider isolation for the remotes that need it. Doing it in this order means every step is reversible and no step requires a coordinated release across teams — which is, after all, the property federation exists to protect.

Common pitfalls #

Pitfall Root cause and resolution
CSP blocks the federation runtime script-src lists origins but the runtime injects inline bootstrapping. Use a source list, or propagate a nonce.
Integrity check fails after every remote deploy Hash maintained by hand in the host. Generate it in the remote’s build and publish it with the manifest.
A compromised CDN serves a malicious bundle CSP allows the origin and nothing checks the bytes. Add Subresource Integrity.
Manifest is writable by anyone with bucket access Trust concentrated with no verification. Sign the manifest in the publishing pipeline.
Third-party remote reads the session token Same realm, same origin, no boundary. Isolate it in an iframe instead.
Policy works in staging, breaks in production Staging serves remotes from one origin, production from several. Enumerate every production origin.

FAQ #

Does Module Federation work with a strict, nonce-based CSP?

It can, but the nonce must reach every script element the runtime creates. Webpack supports this through __webpack_nonce__, set before any remote loads. Without it the runtime’s injected scripts are blocked and the remote never initialises. A source-list policy is simpler and, for federation specifically, gives most of the benefit.

Is Subresource Integrity worth it if remotes are on our own CDN?

Yes, for a different reason than you might expect. The common benefit is not defending against a CDN compromise but catching accidental mutation — a bucket overwritten by the wrong pipeline, a cache serving a partial file, a deploy that raced another. Integrity turns those into a clean, reported failure instead of a subtly broken page.

Can we sandbox a remote without an iframe?

Not meaningfully. Proposals for in-realm isolation exist, but nothing shipping today prevents same-realm JavaScript from reaching the host’s globals. If the threat model includes the remote’s code being hostile, the remote needs its own realm — which in practice means an iframe with sandbox, communicating over postMessage.

What should the host do when a remote fails verification?

Render without it, report loudly, and never fall back to loading it unverified. This is the same containment discipline as an error boundary: the page survives, one region does not, and telemetry names the remote and the reason.

Do these controls slow down page load?

Barely. CSP is a header. Integrity verification is a hash over bytes already downloaded. Manifest validation is a loop over a small object. The measurable cost is a couple of milliseconds, which is not the reason teams skip these.