SigV4 verification and IAM enforcement

Opt-in security features: real SigV4 signature checking and IAM identity-policy evaluation with Condition block support.

By default, fakecloud parses SigV4 headers for routing but doesn't verify signatures, and stores IAM policies without evaluating them. That's the right default for "tests just work" — real signature verification and policy enforcement get in the way of the happy path.

When you explicitly need them, two orthogonal flags flip them on:

FAKECLOUD_VERIFY_SIGV4=true    # or --verify-sigv4
FAKECLOUD_IAM=off|soft|strict  # or --iam off|soft|strict

They're independent: you can turn on SigV4 verification without touching IAM, or the other way around.

The reserved root identity

The credential pair test/test (and any access key starting with test) is treated as the de-facto root bypass. It skips both SigV4 verification and IAM enforcement, matching the community convention that LocalStack and other emulators use for local development.

When either opt-in feature is enabled, fakecloud emits a one-time WARN at startup noting this bypass so you don't silently get false-positive "my policies work" results from unsigned test clients.

--verify-sigv4

When on, every incoming request is cryptographically verified:

  1. Canonical request rebuilt per the AWS SigV4 spec (double-encoded path for non-S3, single-encoded for S3; sorted, URL-encoded query string; lowercased + sorted headers; payload hash from X-Amz-Content-Sha256 when present, otherwise sha256(body)).
  2. Signing key derived via the four-step HMAC chain AWS4 -> date -> region -> service -> aws4_request.
  3. Constant-time comparison against the signature the client sent.
  4. Clock skew window of ±15 minutes, matching AWS.

Verification failures return protocol-correct AWS errors before business logic runs:

FailureAWS error
Wrong signatureSignatureDoesNotMatch
Unknown access keyInvalidClientTokenId
Clock skew > 15 minRequestTimeTooSkewed
Malformed auth headerIncompleteSignature

Header-based Authorization: AWS4-HMAC-SHA256 ... and query-string (presigned URL) signatures are both supported. STS temporary credentials from AssumeRole, GetSessionToken, and GetFederationToken are persisted per-request and verified against the secret key the client received when it called STS.

--iam off|soft|strict

Three modes, in order of aggressiveness:

  • off (default): policies are stored but never consulted. Zero behavior change from unconfigured fakecloud.
  • soft: policies are evaluated and each deny is logged on the fakecloud::iam::audit tracing target, but the request is allowed through. Useful for onboarding: you can see which statements would fire without breaking your test suite.
  • strict: policies are evaluated and denied requests fail with a protocol-correct AccessDeniedException before the service handler runs.

Filter the audit events with RUST_LOG=fakecloud::iam::audit=warn.

Root is always allowed

The account's IAM root identity (arn:aws:iam::<account>:root) and the reserved test* bypass AKIDs always pass enforcement, matching AWS's own behavior where root bypasses identity-based policies.

Enforced services

Opt-in enforcement covers the services most commonly subject to real IAM policies:

ServiceCovered actionsResource ARN shape
IAMAll 128 supported actionsarn:aws:iam::<account>:{user,role,group,policy,instance-profile,mfa,server-certificate,saml-provider,oidc-provider}/<name>
STSAll 8 supported actions* (or RoleArn for AssumeRole*)
SQSAll 20 supported actionsarn:aws:sqs:<region>:<account>:<queue-name>
SNSAll 34 supported actionsTopic / subscription / platform-app / endpoint ARNs
S3All 74 supported actionsarn:aws:s3:::<bucket>[/<key>] (object actions include the key; bucket actions don't)
KMSAll 47 supported actionsarn:aws:kms:<region>:<account>:key/<key-id> (key-targeted actions) or * (account-level actions like CreateKey, ListKeys)

Other services are not enforced even with FAKECLOUD_IAM=strict. The startup log enumerates which services are enforced vs. skipped so you always know the current surface. If a service you need is missing, open an issue — the wiring is straightforward per-service.

Evaluator scope

The policy evaluator implements the essentials of AWS's identity-based policy evaluation. It deliberately stops short of the features listed below so you don't build false mental models from a half-evaluator.

Implemented

  • Effect: "Allow" / Effect: "Deny" with Deny precedence (any matching deny wins).
  • Action / NotAction with * and ? wildcards. Service prefix match is case-insensitive; action names are case-sensitive (matches AWS).
  • Resource / NotResource with * and ? wildcards.
  • Condition blocks: all 28 operators AWS defines, plus the ...IfExists suffix and the ForAllValues: / ForAnyValue: qualifiers. See the next section for details.
  • Identity policies attached to:
    • IAM users (inline + managed + via group membership, inline and managed)
    • IAM roles (inline + managed, for assumed-role sessions)
  • Empty effective policy set -> implicit deny.

Condition block evaluation

A statement with a Condition block only applies when every entry in the block evaluates to true against the request-time context. Multiple operators inside a single Condition are AND-combined; multiple keys inside one operator are AND-combined; multiple values for one key are OR-combined (modulo the ForAllValues qualifier).

Supported operators:

CategoryOperators
StringStringEquals, StringNotEquals, StringEqualsIgnoreCase, StringNotEqualsIgnoreCase, StringLike, StringNotLike
NumericNumericEquals, NumericNotEquals, NumericLessThan, NumericLessThanEquals, NumericGreaterThan, NumericGreaterThanEquals
DateDateEquals, DateNotEquals, DateLessThan, DateLessThanEquals, DateGreaterThan, DateGreaterThanEquals (RFC3339 and epoch seconds both accepted)
BooleanBool
BinaryBinaryEquals
IP addressIpAddress, NotIpAddress (v4 and v6 CIDR, plus bare addresses)
ARNArnEquals, ArnNotEquals, ArnLike, ArnNotLike
ExistenceNull

Every operator supports the ...IfExists suffix (missing key evaluates to true) and the ForAllValues: / ForAnyValue: set-qualifier prefixes.

Supported global condition keys:

KeySource
aws:usernameLast segment of the IAM user ARN; unset for assumed-role / federated principals, matching AWS
aws:useridPrincipal.user_id (e.g. AIDA..., AROA...:<session>)
aws:PrincipalArnFull principal ARN
aws:PrincipalAccount12-digit account ID sourced from the credential (#381 alignment), not global config
aws:PrincipalTypePascalCase label: User / AssumedRole / FederatedUser / Account
aws:SourceIpRemote address of the incoming HTTP connection
aws:CurrentTimeServer-side evaluation timestamp (UTC)
aws:EpochTimeSame moment as aws:CurrentTime, in seconds since the Unix epoch
aws:SecureTransporttrue iff the request carries x-forwarded-proto: https (the fakecloud server itself speaks HTTP; set this header from an upstream TLS terminator to test)
aws:RequestedRegionRegion extracted from SigV4 / config

Supported service-specific condition keys:

KeyPopulated onSource
s3:prefixs3:ListObjects, s3:ListObjectsV2?prefix= query param
s3:delimiters3:ListObjects, s3:ListObjectsV2?delimiter= query param
s3:max-keyss3:ListObjects, s3:ListObjectsV2?max-keys= query param
sns:Protocolsns:SubscribeProtocol request parameter
sns:Endpointsns:SubscribeEndpoint request parameter
lambda:FunctionArnlambda:AddPermissionTarget function ARN resolved from the path
lambda:Principallambda:AddPermissionPrincipal field from the JSON body
sqs:MessageAttribute.<Name>sqs:SendMessageEach named MessageAttribute's StringValue (Binary / Number attributes fall back to the data type)

New services plug in by implementing iam_condition_keys_for on their AwsService impl; the dispatcher merges the result into the shared context before the evaluator runs.

Safe-fail semantics. Any unimplemented operator, unknown key, or parse failure (malformed date, invalid CIDR, non-numeric value where numeric expected) is logged to fakecloud::iam::audit at debug level and evaluates to false — i.e. the statement does not apply. The evaluator will never silently treat an unrecognized condition as a match. If you see unexpected denies, raise the fakecloud::iam::audit log level to see which statements were skipped.

Resource-based policies

S3 bucket policies, SNS topic policies, Lambda function policies, and KMS key policies are fully wired into the evaluator. When enforcement is on and a resource has a policy attached, dispatch fetches it and hands it to the evaluator alongside the caller's identity policies; the evaluator combines the two using AWS's cross-account semantics:

  • Explicit Deny from either the identity policy or the resource policy wins immediately.
  • Same-account callers (principal account ID equals the resource's owning account): the request is allowed if the identity policy or the resource policy grants it.
  • Cross-account callers: the request is allowed only if the identity policy and the resource policy both grant it.

The resource's owning account is parsed from the ARN; S3 ARNs have an empty account segment, so fakecloud falls back to the server's configured account ID (#381 multi-account alignment — the decision is per-ARN, not a global config knob).

Where policies come from.

  • S3 bucket policies are stored by PutBucketPolicy and updated by DeleteBucketPolicy. GetBucketPolicy returns the raw JSON.
  • SNS topic policies are stored in the topic's Policy attribute by SetTopicAttributes (full document) or by AddPermission / RemovePermission (incremental statements). GetTopicAttributes returns them.
  • Lambda function policies are built incrementally by AddPermission: fakecloud composes a canonical {"Version":"2012-10-17","Statement":[...]} document from (StatementId, Action, Principal, SourceArn?, SourceAccount?) so the existing evaluator reads it without a Lambda-specific fork. SourceArn becomes an ArnLike Condition on aws:SourceArn, and SourceAccount becomes a StringEquals Condition on aws:SourceAccount — both are already in the operator set. GetPolicy returns the composed document; RemovePermission strips the matching Sid and leaves an empty Statement array behind, matching AWS.

Principal matching. Resource policies use Principal / NotPrincipal keys that identity policies don't. The evaluator supports the shapes resource policies actually use in practice:

ShapeMeaning
"Principal": "*"Any authenticated principal
"Principal": {"AWS": "*"}Same as above
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"}Any principal whose account ID is ACCOUNT
"Principal": {"AWS": "ACCOUNT"}12-digit account ID shorthand, equivalent to ...:root
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:user/alice"}Exact principal ARN match
"Principal": {"AWS": [...]}List form — any entry matches
"Principal": {"Service": "events.amazonaws.com"}Matches an assumed-role principal whose ARN contains the service host (covers EventBridge -> SNS and similar service-linked role scenarios)

NotPrincipal is fully evaluated — a statement with NotPrincipal applies to all callers except those matching any entry in the list (the exact inverse of Principal). The classic AWS pattern Deny + NotPrincipal ("deny everyone except this user") works correctly. NotPrincipal entries with unrecognized principal types (Federated, CanonicalUser) are dropped from the match list; if all entries are unrecognized the statement is skipped with a fakecloud::iam::audit debug log and never silently grants.

Principal types other than AWS / Service (Federated, CanonicalUser) fall through to "doesn't match" for the same reason.

Condition blocks on resource-policy statements are evaluated with the same operator set and global condition keys as identity policies — the condition entry points are shared.

Not implemented

Ongoing coverage work:

  • Service-specific condition keys for services / operations beyond the ones listed in the table above. The hook is AwsService::iam_condition_keys_for; extending coverage is additive and requires no signature changes.

Permission boundaries

A managed policy attached to a user or role that caps the maximum permissions that identity can ever be granted. The effective permissions of a user with a boundary are the intersection of identity policies and the boundary: both must allow, and an explicit Deny in either layer wins.

  • Attached via PutUserPermissionsBoundary / PutRolePermissionsBoundary, removed via DeleteUserPermissionsBoundary / DeleteRolePermissionsBoundary.
  • Dangling boundary ARN (the managed policy was deleted while still attached): the principal can perform no action until the boundary is removed or re-created — matches AWS behavior. Logged to fakecloud::iam::audit at debug level.
  • Bypass rules: the account root and service-linked roles (role name starts with AWSServiceRoleFor) are exempt from boundary evaluation. Within evaluate_with_resource_policy, the boundary gates only the identity side — same-account resource-policy grants stand on their own.

Session policies

Inline policies passed to AssumeRole, AssumeRoleWithWebIdentity, AssumeRoleWithSAML, or GetFederationToken via the Policy parameter (and PolicyArns) that further restrict the resulting temporary credentials below the role's own policies.

  • Session policies are persisted on the STS temporary credential and evaluated as a third intersection layer: effective permission = identity ∩ boundary ∩ session. Each layer is evaluated independently; an explicit Deny in any layer wins.
  • An STS call with no Policy / PolicyArns produces a credential with no session-policy gate (pass-through), preserving Phase 2 behavior.
  • GetSessionToken does not accept a Policy parameter per AWS docs, so session policies do not apply to credentials minted by that operation.

Phase 4 — ABAC (tag-based conditions).

Tag-based access control via four condition key families:

Condition keyDescriptionEnforced services
aws:ResourceTag/<key>Tags on the target resourceS3, SQS, SNS, IAM, KMS
aws:RequestTag/<key>Tags sent in the request (e.g. on CreateQueue, PutObject)S3, SQS, SNS, IAM, KMS
aws:TagKeysList of tag keys in the request (for ForAllValues/ForAnyValue)S3, SQS, SNS, IAM, KMS
aws:PrincipalTag/<key>Tags on the calling IAM user or assumed roleAll enforced services

Key semantics:

  • The condition key prefix (aws:ResourceTag/) is matched case-insensitively per AWS. The tag key part after the slash (Environment) is matched case-sensitivelyaws:ResourceTag/Environment and aws:ResourceTag/environment reference different tags.
  • aws:PrincipalTag/<key> is populated from the IAM user's or assumed role's tags at credential resolution time.
  • Services that don't implement ABAC yet (Lambda, Step Functions, etc.) gracefully skip tag evaluation with a fakecloud::iam::audit debug log. No fake tag values are ever returned.
  • Adding ABAC support to a new service requires implementing two trait methods: resource_tags_for() and request_tags_from().

Will not ship.

  • Service control policies (SCPs). An AWS Organizations construct that gates permissions across accounts in an org. fakecloud is a single-account model, so there is no org boundary for an SCP to attach to.

If you need any of these for your test scenarios, use real AWS. fakecloud is a test tool, not a full IAM simulator.

Practical example

Bootstrap a user with root credentials, attach a resource-scoped policy, then hit the service with their own access key:

# Start fakecloud with enforcement on.
FAKECLOUD_VERIFY_SIGV4=true FAKECLOUD_IAM=strict ./fakecloud

# Root-bypass bootstrap.
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
  aws --endpoint-url http://localhost:4566 iam create-user --user-name alice
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
  aws --endpoint-url http://localhost:4566 iam create-access-key --user-name alice
# -> emits AKIA..., SECRET...

AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
  aws --endpoint-url http://localhost:4566 iam put-user-policy \
    --user-name alice \
    --policy-name ReadSelf \
    --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"iam:GetUser","Resource":"arn:aws:iam::123456789012:user/alice"}]}'

# Alice can read herself...
AWS_ACCESS_KEY_ID=<alice-akid> AWS_SECRET_ACCESS_KEY=<alice-secret> \
  aws --endpoint-url http://localhost:4566 iam get-user --user-name alice
# -> success

# ...but not anyone else.
AWS_ACCESS_KEY_ID=<alice-akid> AWS_SECRET_ACCESS_KEY=<alice-secret> \
  aws --endpoint-url http://localhost:4566 iam get-user --user-name root
# -> AccessDeniedException

See also