Here is a story I keep seeing, in one shape or another.
A team decides to build a micro-frontend platform. They start the smart way: a composition-first, builder approach. A shell app loads independent remotes at runtime, each team ships its own piece, everyone moves fast. Over the following months they keep extending it — more remotes, per-tenant behavior, a clean shared component library, a slick deployment pipeline. The architecture genuinely improves as they go.
Then someone finally asks for a security review. And that is when it gets uncomfortable — because security was never actually in the loop. It was assumed, not designed.
I wrote a broader piece on how to build a micro-frontend platform on AWS — the architecture, the patterns, where each AWS service fits. This post is the companion: the security half, told as field notes. Same platform, uncomfortable questions.
First, one mental shift
Most of the trouble comes from a single wrong assumption:
A remote is config you load.
It isn’t. A remote is code that runs as you, in your origin, for every user.
When the shell pulls in a remote (a remoteEntry.js, a Module Federation manifest, whatever your setup uses), it doesn’t sandbox it into a neat little box. That code executes with the same privileges as the shell itself. Same origin. Same access to the DOM, to cookies, to anything stored in the browser. If one remote is compromised, it is compromised for everyone who loads it — and it looks exactly like first-party code, because it is.
Once you see remotes that way, the gaps below stop being abstract. They become obvious.
Gap 1 — The shell runs remote code it never verified
This is the one that made me sit up.
The shell fetches a remote’s entry file from a URL and executes it. No check that the file is the one your build produced. No check that it hasn’t been swapped. If an attacker can write to where that file lives, or sit in the path between the CDN and the browser, they get to run trusted code in your app.
Websites solved this years ago with Subresource Integrity — the integrity="sha384-..." attribute on a <script> tag. The browser refuses to run a script whose hash doesn’t match. But Module Federation loads its chunks dynamically, and it skips that mechanism entirely. So the protection everyone relies on for a plain script tag quietly disappears the moment you go micro-frontend.
The fix: verify before you execute.
Your build emits a signed manifest — a small file mapping each module to its URL and a sha384 hash of its contents. The manifest itself is signed. Before the shell runs a remote, it checks the signature, then checks that the file it fetched matches the hash. Anything that doesn’t match doesn’t run. It is SRI, rebuilt for the way micro-frontends actually load code.

On AWS the pieces line up cleanly:
- S3 + CloudFront serve the chunks. Use immutable, content-hashed filenames so a given URL always means exactly one build. Do not move your code delivery onto a REST/Lambda hot path to “add checks” — keep the fast static substrate and add integrity around it.
- Lambda@Edge on the origin-response verifies and stamps the manifest at the edge.
- AWS Signer holds the code-signing keys, so signing isn’t a secret sitting in a build script.
A useful rule of thumb: CloudFront Functions for sub-millisecond header and URL work, Lambda@Edge when you need a network call or real logic like signature verification.
Gap 2 — Every token lives in localStorage
Walk up to almost any micro-frontend and check where the auth token is. Nine times out of ten it’s in localStorage or sessionStorage, because that’s the easy default and it “just works” across remotes.
Here’s the problem. That storage is shared across the whole origin. Every remote can read it. So can any script that ends up running in the origin. Which means a single cross-site scripting bug — in any one of your remotes, including a third-party one you don’t control — reads every token for every user. You didn’t have one weak link; you had one shared vault with a glass door.
The fix: stop putting tokens where any script can grab them.
Pick the option that fits your platform:
- Shell-owned closure — the shell holds the token in memory, in a scope no remote can reach, and hands out only what’s needed.
- Service Worker — a worker injects the token onto outgoing requests for an allow-listed set of domains, so the token never touches page-reachable storage.
- BFF (backend-for-frontend) — the token stays server-side in a session; the browser holds only an opaque cookie.
All three share the same principle: remotes get the ability to make an authorized call, never the raw credential.
Gap 3 — The shell guards the front door; the back doors are wide open
This is the most reassuring gap, which is exactly why it’s dangerous. The team gates routes in the shell — check the user’s role, hide the admin remote, redirect if they’re not allowed — and it feels secure. The UI does the right thing.
But every remote is a real, deployed application at its own URL. The admin remote’s API is reachable directly. An attacker doesn’t navigate your shell and get politely turned away; they call the API straight, skipping the shell entirely. If the API trusts “well, the shell wouldn’t have sent this unless you were allowed,” it’s not enforcing anything. It’s hoping.
The fix: enforce authorization at the API, never at the shell alone.
The shell’s route guards are for user experience — don’t show people doors they can’t open. The real gate is the API, and it has to check every request on its own merits.
On AWS, Amazon Cognito with a pre-token-generation Lambda does the heavy lifting: at login it injects the user’s entitlement claims into a signed JWT. Every API then validates that token and checks the claims itself. The shell can hide the button; the API is what actually says no.
Where each piece fits on AWS
| Concern | AWS building block | What it does |
|---|---|---|
| Static asset delivery | S3 + CloudFront | Immutable, content-hashed chunks, edge-cached |
| Edge integrity + version resolution | Lambda@Edge | Verify and stamp the signed manifest; canary / per-tenant routing |
| Header hardening | CloudFront Functions | CSP + Trusted Types, cache-key normalization |
| Manifest signing | AWS Signer | Manages the code-signing keys |
| Identity core | Amazon Cognito + pre-token Lambda | Credentials, SAML, entitlement claims in a signed JWT |
| Write governance | IAM + S3 versioning / Object Lock + EventBridge | Only the deploy role can publish; alert on off-window writes |
| Session state (hot path) | ElastiCache / DynamoDB | Tier-0 lookups, short-TTL revocation cache |
| Per-product API shaping | API Gateway / ALB + BFF | Enforce authz, keep tokens server-side |
| Global routing | Route 53 + CloudFront | Entry point and edge distribution |
Where to start
Don’t try to fix all of this at once — you’ll stall.
Take one vertical slice: a single remote, from build to edge to shell to API, and make that path correct end to end. And within that slice, do verify-before-execute first. It’s the gap that turns a compromise anywhere in your supply chain into trusted code running for every user, and everything else is easier to reason about once the shell only runs code it has checked.
Then repeat the slice.
The rest of the architecture — how to compose remotes, route them, share state without coupling teams — is in the AWS Builder post. Read that for the what; treat this one as the don’t ship it without this.
The pattern I opened with — build fast, improve the design, review security late — isn’t a failure of the team. They did the hard part well. Security just needs to be one of the design questions from the start, not a review at the end.
Sylvain Leroy is a Migration and Modernization Solutions Architect at AWS with 20 years of experience across frontend, backend, and cloud architecture. He works with enterprise teams on distributed systems, application modernization, and secure platform design.
