AWS
An environment answers AWS calls itself, with no endpoint override in the
application. Every name resolves to the sidecar, the sidecar terminates TLS
with the certificate authority the environment already trusts, and it answers
for s3.amazonaws.com itself. The code that runs is the code that ships: no
AWS_ENDPOINT_URL, no client constructed differently in tests, no branch on an
environment variable. Select the emulator in the manifest’s egress rules.
Select the AWS emulator
Section titled “Select the AWS emulator”egress: default: block rules: - host: s3.amazonaws.com mode: emulate emulator: aws - host: '*.s3.amazonaws.com' mode: emulate emulator: awsAdd rules for the covered hosts your application uses. The Docker runtime starts the registered emulator on the contained network and the sidecar routes matching requests to it. No live AWS account is needed for this emulator.
What exists: engine/pkg/emulator holds the declaration, with the hosts, the
pinned digest and the licence; a build registers it into the extension registry
at startup, which is the only place the engine ever resolves an emulator from;
THIRD_PARTY_NOTICES.md is generated from that same declaration; and
tools/emulatorcheck drives the AWS SDK for Go and the AWS SDK for JavaScript
at the pinned image on every run of CI, with zero endpoint overrides, which is
what makes the numbers on this page measurements rather than claims.
The SDK suite uses a focused routing fixture. Separate Docker runtime tests prove unchanged-application routing, containment and teardown through the real runtime. Neither suite establishes equivalence with every live AWS API.
The emulator behind it is LocalStack. Antifailure does not write emulators. S3 alone has a decade of edge cases in it, a hand written replacement would be worse on day one and probably for two years, and nobody buys this product because its S3 emulator is good. What is worth building is the part people hate about using an emulator, which is changing the application to reach it.
The surface
Section titled “The surface”This table is the surface. An AWS host that is not in it is not routed to
the emulator: it falls through to the environment’s egress policy, whose default
is block, and it is refused. That is deliberate. A silent wrong answer from an
emulator is worse than a refusal, because the wrong answer will be trusted.
| Service | Hosts answered | Proved by |
|---|---|---|
| Amazon S3 | s3.amazonaws.com, s3.*.amazonaws.com, *.s3.amazonaws.com, *.s3.*.amazonaws.com |
CreateBucket, PutObject and GetObject, in both addressing styles |
| Amazon SQS | sqs.*.amazonaws.com |
CreateQueue, SendMessage and ReceiveMessage |
| Amazon SNS | sns.*.amazonaws.com |
CreateTopic and Publish |
| Amazon DynamoDB | dynamodb.*.amazonaws.com, streams.dynamodb.*.amazonaws.com |
CreateTable, PutItem and GetItem |
| Amazon Kinesis | kinesis.*.amazonaws.com |
CreateStream and PutRecord |
| Amazon EventBridge | events.*.amazonaws.com |
PutRule and PutEvents |
| AWS Secrets Manager | secretsmanager.*.amazonaws.com |
CreateSecret and GetSecretValue |
| AWS Systems Manager Parameter Store | ssm.*.amazonaws.com |
PutParameter and GetParameter |
| AWS STS | sts.amazonaws.com, sts.*.amazonaws.com |
GetCallerIdentity and AssumeRole |
A star stands for one whole label, so sqs.*.amazonaws.com is every region and
*.s3.*.amazonaws.com is a virtual hosted bucket in every region. The leading
star covers one label or more, which is what makes a bucket whose name contains
a dot reachable.
The “proved by” column is not decoration. Each of those calls is made by the vendor’s own SDK against a running emulator in this repository’s own test suite. A service listed with nothing proving it is a claim, and a claim in a table somebody trusts is the failure this table exists to avoid.
STS is in the surface on purpose
Section titled “STS is in the surface on purpose”Most AWS SDKs resolve credentials before the first real call, and several
credential chains call sts.amazonaws.com to do it. An emulated surface without
STS fails at startup, with an error naming the credential chain rather than the
service anybody was trying to reach, and the person reading it goes looking at
S3.
What is outside it, and why
Section titled “What is outside it, and why”| Not answered | Why |
|---|---|
| AWS Lambda, ECS, EKS, Batch and Step Functions | LocalStack runs these by starting further containers through the Docker socket. An environment does not hand a container the Docker socket, so this is refused rather than half answered. |
| Amazon RDS, Aurora, ElastiCache and OpenSearch | A datastore is not emulated. Postgres is branched from a golden, and a second store is declared in the manifest with a stance. An emulator with an empty schema in it is a worse answer than either. |
| Amazon SES and SESv2 | Mail is captured into the environment’s inbox, where an agent can read it and no real address receives anything. An emulator would swallow it instead. |
| Amazon API Gateway, CloudFormation, IAM, CloudWatch and everything else AWS runs | Outside the surface, and refused by the egress policy rather than answered. |
| S3 dualstack, transfer acceleration and S3 Express One Zone | Further spellings of the S3 endpoint that resolve under different names. They reach nothing, and the refusal says no rule matches rather than naming S3. |
Where the refusal actually happens
Section titled “Where the refusal actually happens”The refusal is in the ROUTING, and it is worth being precise about that rather than claiming a second wall that does not exist.
The container is started with SERVICES listing the nine and
STRICT_SERVICE_LOADING set. Measured against the pinned digest on 2026-09-08,
that leaves 23 of the 35 services LocalStack knows about reporting disabled
and twelve reporting available: the nine above, DynamoDB Streams which the
surface routes, and KMS and Lambda, which load because services in the list
depend on them. A GET to /2015-03-31/functions with a Lambda Host header is
then answered 200 {"Functions": []} by the container.
That is exactly the silent wrong answer a declared surface exists to prevent,
and what prevents it is that lambda.*.amazonaws.com is not a host any covered
service claims. Nothing routes the request to the emulator, so the environment’s
egress policy decides it, and the default is block. The container allowlist is
a smaller attack surface and a shorter start, not the refusal.
How the application reaches it, which is DNS and not a proxy variable
Section titled “How the application reaches it, which is DNS and not a proxy variable”Zero endpoint overrides is achieved by DNS interception, not by proxy configuration. It is worth reading that sentence twice if you were planning around the proxy variables, because one of the two SDKs below ignores them completely.
An environment reaches the sidecar two ways. The proxy variables are the weaker
one: a library is free to ignore them, and the AWS SDK for JavaScript ignores
them entirely, so HTTPS_PROXY does nothing for a Node application. The one
that always holds is the network. Every external name resolves to the sidecar,
the sidecar terminates TLS with a certificate authority the environment already
trusts, and a client that reads no variable at all still arrives there. A
service that somehow bypassed both has nowhere to send the packet, because the
inner network has no route out.
The suite that proves this drives both paths on purpose. The AWS SDK for Go is driven through the proxy variables, and the AWS SDK for JavaScript is driven through DNS, on an internal Docker network with a router answering on 443 and one name mapped per hostname. Neither application names an endpoint.
What the sidecar rewrites, and what it does not
Section titled “What the sidecar rewrites, and what it does not”The destination is rewritten. The Host header and the Authorization header
are preserved. Both of those are facts about the protocols rather than
preferences:
- Virtual hosted S3 addressing carries the bucket name in the
Hostheader, and that is where LocalStack reads it from. RewritingHostdestroys the bucket name and breaks the case this guide is loudest about. - SigV4 signs the
Hostheader. RewritingAuthorizationwithout re-signing produces a signature that disagrees with its own request, which is fragile against any emulator that parses the key id.
The credential cannot escape regardless of what the header holds, and that is a
property of the network rather than a promise: the emulator is attached to the
environment’s inner network only, which Docker creates with internal set, so
it has no route out. The sidecar refuses a request signed with a key that
livekey recognises as a live one, so a real AKIA key does
not reach the emulator either.
The LocalStack image, and a fact worth reading before you plan around it
Section titled “The LocalStack image, and a fact worth reading before you plan around it”LocalStack’s Community edition was archived in March 2026. The project moved
to a single “LocalStack for AWS” image which requires an auth token, and the
final community build is published as the community-archive tag. The image
this build starts is that final community build, pinned by digest:
localstack/localstack@sha256:6b6172cfceb04b4fbc35097a55f717c365a35fafa572be49f7341771cf9023edIt is pinned by digest rather than by tag because an emulator is the thing answering for production’s API, and a tag that moves changes what an environment was tested against with nothing in this repository changing. A tag is refused by the registry’s validation.
What that means in practice:
- Running the suite needs no LocalStack account and no token. The archived community image starts offline and answers for the nine services above.
- The archived image does not gain new AWS behaviour. When AWS changes an API in a way the archive predates, this surface is what it is, and the gap register is where that is recorded rather than discovered.
- An organisation with a LocalStack licence can point the environment at the
supported image instead, by registering an emulator named
awsfrom a build of their own throughextension.AddEmulator. The registry refuses two emulators under one name, so that is a replacement rather than a shadow.
LocalStack is licensed under the Apache License 2.0 and is recorded in
THIRD_PARTY_NOTICES.md, which is generated from the same declaration the
engine starts the container from.
The emulator starts empty, and what fills it
Section titled “The emulator starts empty, and what fills it”LocalStack is started with PERSISTENCE off, so nothing an environment does to
it survives that environment. That is deliberate: a twin that inherited the last
twin’s buckets would be reproducible only by accident. It also means a bucket, a
queue, a topic, a table, a stream, a parameter or a secret that exists in
production exists nowhere in the twin until something puts it there, and an
application that reads its own bucket on startup meets an emulator that has
none.
af up creates the resources production’s infrastructure as code declares,
inside the emulator, before any service starts. The requests go through the
environment’s own sidecar at the provider’s own hostname, so what is exercised
is the route the application has. A hostname the egress policy does not route to
this emulator is reported refused rather than created somewhere else, because
the application would be refused at that hostname too.
These are the AWS resource types it creates:
| Resource type | What is created |
|---|---|
aws_s3_bucket |
the bucket, and versioning when it is declared |
aws_sqs_queue |
the queue, FIFO, visibility timeout, retention, delay, maximum message size, receive wait |
aws_sns_topic |
the topic, FIFO |
aws_dynamodb_table |
the table, its partition key and its sort key |
aws_kinesis_stream |
the stream and its shard count |
aws_ssm_parameter |
the parameter, holding a placeholder |
aws_secretsmanager_secret |
the secret, holding a placeholder |
aws_cloudwatch_event_bus |
the event bus |
Nothing is called reproduced until it has been read back out of the emulator. A create the emulator answered is not evidence that anything exists, so every one of the rows above ends with a read that finds it, and a read that does not find it reports the resource absent with what the emulator said.
What it does not reproduce is named
Section titled “What it does not reproduce is named”Every attribute a declaration carries is accounted for, and the accounting is by subtraction: an attribute this build does not put into the emulator is reported with the reason, whether or not anybody anticipated it. So a run says which of these it met, and a run in which everything reproduced prints no caveat at all.
- A secret and a parameter hold a placeholder, and are reported as substituted rather than reproduced. Production’s value must never be copied into a container running a third party image, and reading “the secret is in the twin” as “the secret says what production says” is the most dangerous sentence this could produce.
- A
SecureStringparameter is created as a plainString. The surface does not answer for KMS, so aSecureStringhere would be a parameter the application cannot decrypt. - Anything encrypted with a KMS key is created without one, for the same reason.
- A lifecycle rule is not created. LocalStack stores a lifecycle configuration and never expires an object, so a rule reproduced here would be a rule that does nothing.
- A secondary index is not created, so a query against one does not find it.
- The region is a hostname here and a property in production. One LocalStack answers for every region at once, so a declaration’s region decides which hostname the request goes to and therefore which egress rule must route it. It is not a property the twin holds.