Skip to content

feat(kms): real S3 SSE-KMS + SSM SecureString encryption via KMS hook#742

Merged
vieiralucas merged 7 commits intomainfrom
worktree-integrations-batch6b-kms-ssm-s3
Apr 25, 2026
Merged

feat(kms): real S3 SSE-KMS + SSM SecureString encryption via KMS hook#742
vieiralucas merged 7 commits intomainfrom
worktree-integrations-batch6b-kms-ssm-s3

Conversation

@vieiralucas
Copy link
Copy Markdown
Member

@vieiralucas vieiralucas commented Apr 24, 2026

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.

  • S3 SSE-KMS: encryption context `{aws:s3:arn: arn:aws:s3:::}`. 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: }`. 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).

Test plan

  • `cargo test -p fakecloud-e2e --test ssm_kms` (2 new tests)
  • `cargo test -p fakecloud-e2e --test s3_sse_kms` (3 new tests, incl. ranged read on SSE-KMS object)
  • `cargo test -p fakecloud-ssm` (195 existing pass)
  • `cargo test -p fakecloud-s3 --lib` (280 existing pass)
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check` clean
  • README + service pages + cross-service guide updated

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

    • S3 SSE-KMS: PutObject encrypts with context {aws:s3:arn: arn:aws:s3:::}; GetObject decrypts and range reads slice plaintext; CopyObject decrypts source and re-encrypts for destination; default aws/s3 key auto-provisions and persists; KMS usage recorded at /_fakecloud/kms/usage; KMS encrypt/decrypt failures abort the request.
    • SSM SecureString: PutParameter encrypts with context {PARAMETER_ARN: }; GetParameter/GetParameters/GetParametersByPath and GetParameterHistory decrypt when WithDecryption=true; default alias/aws/ssm key auto-provisions and persists; KMS usage recorded.
    • IAM PassRole trust enforcement: Lambda CreateFunction and ECS RegisterTaskDefinition/RunTask overrides reject role ARNs whose trust policy doesn’t allow the service principal (lambda.amazonaws.com, ecs-tasks.amazonaws.com), returning AWS-shaped errors.
  • Migration

    • No breaking changes. Legacy non-enveloped snapshots continue to read. S3 SSE-KMS errors now surface on Put/Get/Copy. Ensure roles passed to Lambda/ECS trust the relevant service principal. Auto-provisioned aws/<service> keys persist automatically; no action needed.

Written for commit f669b92. Summary will update on new commits.

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.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/fakecloud-s3/src/service/mod.rs Outdated
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.
@vieiralucas vieiralucas merged commit 4d9ba61 into main Apr 25, 2026
48 checks passed
@vieiralucas vieiralucas deleted the worktree-integrations-batch6b-kms-ssm-s3 branch April 25, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant