feat(kms): real S3 SSE-KMS + SSM SecureString encryption via KMS hook#742
Merged
vieiralucas merged 7 commits intomainfrom Apr 25, 2026
Merged
feat(kms): real S3 SSE-KMS + SSM SecureString encryption via KMS hook#742vieiralucas merged 7 commits intomainfrom
vieiralucas merged 7 commits intomainfrom
Conversation
Real AWS rejects CreateFunction / RegisterTaskDefinition / RunTask with InvalidParameterValueException when the supplied role's AssumeRolePolicyDocument doesn't list the calling service principal, regardless of the caller's identity policy. fakecloud now performs the same check at the service boundary. - New `RoleTrustValidator` trait in fakecloud-core/src/auth.rs so service crates surface a wire-shaped error without depending on fakecloud-iam directly. - `IamRoleTrustValidator` impl in fakecloud-iam/src/pass_role.rs parses the trust policy, walks Allow statements, and matches Principal.Service against the supplied service principal. - LambdaService.create_function rejects roles that don't trust lambda.amazonaws.com. - EcsService.register_task_definition + run_task overrides reject roles that don't trust ecs-tasks.amazonaws.com. - E2E coverage in iam_pass_role.rs (4 happy-path + negative tests) plus 5 unit tests for trust-policy parsing. The identity-policy half of `iam:PassRole` (caller permission) lives in the existing IAM evaluator and is invoked separately at the IAM evaluator boundary.
Cubic flagged that `"Principal": "*"` and `{"AWS": "*"}` (wildcard
trust policies) were being rejected because `principal_service_includes`
only inspected the `Service` key. Match real AWS:
- bare `"Principal": "*"` allows any service principal,
- `{"AWS": "*"}` allows any service principal,
- `{"Service": "*"}` allows any service principal,
- existing exact-match behavior for explicit service principals
is preserved.
Also dropped an inaccurate `UpdateFunctionConfiguration` mention
from the website docs — fakecloud only enforces the trust-policy
check on `CreateFunction` (the existing tests cover this).
The strict PassRole check broke long-standing fakecloud Lambda E2E tests that pass arbitrary role ARNs without first creating the IAM role. Real AWS does require the role to exist for CreateFunction, but fakecloud's test culture has historically been more permissive. Make `IamRoleTrustValidator` return `Ok(())` when: - the ARN doesn't parse as an IAM role ARN, or - the account isn't in IAM state, or - the role doesn't exist, or - the trust policy doesn't parse as JSON. Only the high-signal failure mode — role exists with a trust policy that explicitly excludes the calling service principal — still produces `InvalidParameterValueException` / `InvalidParameterException`. E2E coverage of both branches preserved: - positive: role with matching trust policy is accepted, - negative: role exists with mismatched trust policy is rejected.
Cubic flagged that the previous permissive change made roles with malformed `AssumeRolePolicyDocument` JSON pass through silently. The intent of permissive mode was only to skip the check when the role itself doesn't exist (preserving fakecloud's test culture of arbitrary role ARNs); existing roles with broken trust policies should still fail validation so misconfigurations don't slip through. Restore `InvalidTrustPolicy` as a hard error path on the parse step. RoleNotFound / ARN-doesn't-parse / no IAM state for account remain permissive.
Batch 6b extends the cross-service KMS hook landed in 6a to S3 PutObject /
GetObject (SSE-KMS) and SSM PutParameter / GetParameter* (SecureString).
Both services now generate a real ciphertext envelope through the hook on
write, decrypt on read, and record the GenerateDataKey + Decrypt calls in
/_fakecloud/kms/usage with the AWS-shaped encryption context the upstream
services use:
- S3 SSE-KMS: encryption context {aws:s3:arn: arn:aws:s3:::<bucket>}.
Stored body is a fakecloud-kms envelope; ranged reads slice plaintext
rather than ciphertext so callers never see the envelope. CopyObject
decrypts the source and re-encrypts for the destination's bucket arn,
avoiding ciphertext-of-ciphertext when both ends are SSE-KMS.
- SSM SecureString: encryption context {PARAMETER_ARN: <arn>}. Decrypts
via render wrappers in get_parameter / get_parameters /
get_parameters_by_path / get_parameter_history when WithDecryption=true.
Default key is alias/aws/ssm; auto-provisions on first use.
Both round-trips degrade gracefully on legacy snapshots (decrypt failure
returns the stored bytes unchanged). E2E tests cover round-trip,
introspection records, ranged SSE-KMS reads, and the no-KMS no-op case.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
1 issue found across 18 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fakecloud-s3/src/service/mod.rs">
<violation number="1" location="crates/fakecloud-s3/src/service/mod.rs:133">
P1: Do not store plaintext when SSE-KMS encryption fails; return an error so SSE-KMS writes fail closed.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Cubic flagged that SSE-KMS PutObject would silently store plaintext if the KMS encrypt call errored. Real S3 surfaces KMS errors back to the caller (KMS.NotFoundException, AccessDenied, etc.) — fakecloud should too, otherwise tests passing against fakecloud would mask broken KMS key policies that break in prod. `encrypt_object_body` and `decrypt_object_body` now return `Result<Bytes, AwsServiceError>`. PutObject, GetObject (full + ranged), and CopyObject propagate the error so KMS failures abort the write/read with `KMS.InternalFailureException` instead of silently degrading. When no KMS hook is wired (legacy / in-process tests), the bytes pass through unchanged so existing tests keep working.
… tests The SSE-KMS round-trip in batch 6b broke two existing tests. Both fixes preserve the new ciphertext-at-rest behavior: - KmsHookAdapter now persists the KMS snapshot whenever the hook auto-provisions a fresh `aws/<service>` AWS-managed key. Without this, a server restart loses the key and SecureString / SSE-KMS ciphertext can't be decrypted on the next call. The persistence_secure_string_and_delete_survive_restart e2e regression caught the gap. - ssm_secure_string_with_decryption asserted the pre-hook plaintext placeholder (`kms:alias/aws/ssm:super-secret-123`). With real KMS encryption the un-decrypted value is an opaque ciphertext envelope under that prefix, matching real AWS. The assertion now checks the shape and the absence of plaintext rather than the literal old value.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Batch 6b extends the cross-service KMS hook landed in 6a to S3 PutObject/GetObject (SSE-KMS) and SSM PutParameter/GetParameter* (SecureString). Both services now generate a real ciphertext envelope through the hook on write, decrypt on read, and record GenerateDataKey + Decrypt calls in /_fakecloud/kms/usage with the AWS-shaped encryption context the upstream services use.
Both round-trips degrade gracefully on legacy snapshots (decrypt failure returns the stored bytes unchanged).
Test plan
Summary by cubic
Adds real KMS-backed encryption for S3 SSE-KMS and SSM SecureString, plus IAM PassRole trust enforcement for Lambda and ECS. KMS errors now fail closed on S3 reads/writes, and auto-provisioned
aws/<service>keys persist so ciphertext survives restarts; legacy snapshots still read.New Features
aws/s3key auto-provisions and persists; KMS usage recorded at /_fakecloud/kms/usage; KMS encrypt/decrypt failures abort the request.alias/aws/ssmkey auto-provisions and persists; KMS usage recorded.lambda.amazonaws.com,ecs-tasks.amazonaws.com), returning AWS-shaped errors.Migration
aws/<service>keys persist automatically; no action needed.Written for commit f669b92. Summary will update on new commits.