Repository navigation
Conversation
Add negative_caching.md: how a Tenant Hierarchical Keyring stops calling Amazon DynamoDB and AWS KMS for a Branch Key ID that AWS KMS has denied, for a configurable time, using only the public extension interfaces. - Trace the Hierarchical Keyring's cache and KMS paths (spec, MPL 1.11.3). - Detect Denials in the Tenant KMS Client; enforce them in a per keyring Cache Guard in front of the shared cache, checked before it. - Key Denials by Branch Key ID, not KMS Key ARN, as KMS can deny some Branch Keys of one KMS Key by encryption context; supports Discovery. - A Tenant Keyring Factory replaces the Branch Key ID Supplier. - Revisit the Partition ID: refreshed credentials share no constant part, so it should derive from a stable Tenant identity. Note in design.md 10.2 that the Partition ID decision is revisited; the example's fix is deferred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
texastony
commented
Nov 5, 2026
- Requirement 4: expired credentials are a transient failure. - Requirement 8: the factory and what it vends MUST be thread safe. - Negative TTL: configured per factory, default 5 minutes. - The Cache Guard passes through the KMS error it received, as is, and is a lightweight object despite being a cache. - Encrypt and decrypt Denials are treated the same. - Move NotFoundException propagation to Design Question 10, noting that a Branch Key's KMS Key is mutable (spec PR aws#287; MPL mutations/mutations TerminalKmsArn). - Resolve the open questions with UPDATEs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
botocore only refreshes credentials proactively (advisory and mandatory windows before expiry) and never in response to an expired token error, so an expired, unrecognized, or mis-signed credential reaching AWS KMS will not resolve by retrying; left undenied, every request would call Amazon DynamoDB and AWS KMS. - Add ExpiredTokenException, UnrecognizedClientException, and InvalidSignatureException as Denials (5.1, 5.2). - Record a probe of AWS KMS through the Tenant KMS Client, varying one part of the credentials at a time. - Note these are usually the application's credential handling, not the Tenant's; the passed through error keeps the cause visible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A GenerateDataKey request signed with sts:GetSessionToken credentials one minute after they expired returned HTTP 400 ExpiredTokenException; replace the note in negative_caching.md 5.1 that asked for this to be confirmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forks have no CI_AWS_ROLE_ARN, CodeBuild project, or Slack webhooks, so those jobs could only fail there; and the ubuntu-latest runner no longer provides Python 3.8 or 3.9, whose failures cancelled most of the test matrix (fail-fast). - Skip codebuild_batch, codebuild_tests, and decrypt_oracle unless the repository owner is aws (pull.yml, push.yml, daily_ci.yml), and guard ci_codebuild-tests.yml's own push and schedule triggers. daily_ci.yml's guards previously covered only its schedule. - Allow those skips in pull.yml's alls-green gate. - Skip the Slack notifications on forks. - ci_tests.yaml: Python 3.12 on ubuntu-latest only (3.8-3.10 are end of life); nocmk on 3.12; drop test-upstream-requirements-py311, which pins Python 3.11. fail-fast stays true. Static analysis stays on 3.10, as the pinned linters predate 3.12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Examples tests only ran in CodeBuild, which forks cannot use, and all were marked examples, so the offline tests could not be selected apart from the integration tests. - Mark the offline examples tests pytest.mark.local, alongside examples; they need no network access or credentials. - Add tox factors exampleslocal (examples/test/legacy/, without the MPL) and mplexampleslocal (examples/test/, with the MPL), each -m local. - Run exampleslocal and mplexampleslocal-mpl in ci_tests.yaml. Locally with tox 4.25.0: exampleslocal 11 passed; mplexampleslocal-mpl 52 passed; the integration tests are deselected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hierarchical Keyring paths through the AWS KMS Key Store (CreateKey, VersionKey, and cache misses: DynamoDB plus GenerateDataKeyWithoutPlaintext, ReEncrypt, and Decrypt) had only run against AWS, with permissions the test identity lacks. DynamoDB Local, AWS's emulator, with the fake KMS, runs them with no AWS credentials, so they can run on forks. - Register a ddblocal pytest marker; the new tests are marked only ddblocal, so the examples (CodeBuild) and local runs do not select them. - examples/test/multi_tenant/test_ddblocal_hierarchical.py: CreateKey items and KMS calls; the multi-tenant example end to end, per tenant signing; VersionKey and cleanup of every version; and, without negative caching, every denied attempt calls DynamoDB GetItem and kms:Decrypt. - tox: mplddblocal factor; pass DDB_LOCAL_ENDPOINT through. - ci_tests.yaml: a ddblocal job on Python 3.12 with amazon/dynamodb-local 3.3.2 as a service container (its default command is -inMemory). Locally with podman and tox 4.25.0: mplddblocal-mpl 4 passed; no tables left behind; the local and examples markers select none of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Revert daily_ci.yml to examples/credentials. Its pull_request trigger only fires for a PR that edits daily_ci.yml, so editing it re-ran Daily CI on every push; and on a fork's schedule, its original guards already skip every job. - ci_codebuild-tests.yml: drop the push and schedule triggers, keeping workflow_call; it is only called by pull.yml, push.yml, and daily_ci.yml. Temporary, for development on a fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task
…aching Negative caching (negative_caching.md): while AWS KMS has Denied a Branch Key ID, its Tenant Hierarchical Keyring fails fast, without calling Amazon DynamoDB or AWS KMS, for a Negative TTL. negative_cache.py (no MPL import): - DENIAL_ERROR_CODES, is_denial, and error_code (Design Question 5). - BranchKeyNegativeCache: the Negative Cache Entry of one Branch Key ID, with record, get, and clear. One lock is held across checking expiry and discarding, so get() never erases a Denial recorded concurrently. Rejects an empty Branch Key ID and a non-positive or NaN Negative TTL. - record() logs a warning naming the Branch Key ID, the KMS error code, the Negative TTL, and the error's text. cache_guard.py: - CacheGuard, an ICryptographicMaterialsCache wrapping the cache shared by every Tenant. While Denied, it raises OpaqueWithTextError(obj=error, obj_message=str(error)) before consulting the shared cache; the Hierarchical Keyring then fails without calling the AWS KMS Key Store. Otherwise it delegates. - OpaqueWithTextError keeps the original error, the same object, and its code through the MPL's conversion to and from Dafny, for all eight Denial codes; a ComAmazonawsKms error loses both for the KMS errors the MPL models. A new wrapper per raise stops the error's traceback growing. Tests (pytest.mark.local), in examples/test/multi_tenant/: classification, expiry, replacement, logging, validation; a deterministic expiry race test; thread safety; the guard's delegation and refusal of cached hits; and end to end, a real Hierarchical Keyring on the Cache Guard, seeded with the specification's cache identifiers, failing encrypt and decrypt with zero DynamoDB and KMS calls while Denied. FakeClock and kms_error are shared in helpers.py. negative_caching.md 4.3 and 5.2 record the error wrapping, and that the first Denial (a real cache miss, confirmed against DynamoDB Local) loses the code of the KMS errors the MPL models, so the code is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Completes negative caching (examples/src/multi_tenant/negative_caching.md, Design Questions 3.3, 6, 7.2, and 8). TenantKmsClient(..., negative_cache=None): - Records a KMS ClientError in its BranchKeyNegativeCache, as the same object, only for a Decrypt, only for a Denial (is_denial), and only if the encryption context has no branch-key-id or names the negative cache's Branch Key ID. Assumes hierarchy version 1 Branch Keys, the only version shipped. The error is always re-raised unchanged. TenantKeyringFactory: - Holds what every Tenant Hierarchical Keyring shares: the KmsTransport, the DynamoDB client, the AWS KMS Key Store configuration, the cache limit TTL, the Negative TTL, and the storm tracking cache, which it creates from settings (default_storm_tracking_cache(), design.md 10.1.1) so it can require the grace period be less than ttl_seconds. - negative_cache_for: one negative cache per (Tenant, Branch Key ID), so a Tenant misconfigured with another's Branch Key ID cannot deny it to its owner. - keyring_for: a Tenant KMS Client bound to that negative cache, an AWS KMS Key Store, and a Hierarchical Keyring with a Cache Guard over the shared cache and the Tenant ID, as is, as its Partition ID. negative_caching.md: - Negative caches per Tenant and Branch Key ID; the factory owns the storm tracking cache; the Partition ID is the Tenant ID as is. - The Tenant ID is the authorization boundary of cached materials: vended keyrings need not be cached, and the application MUST pass the Tenant it authenticated and the Branch Key ID it assigned. Documents the alternative of a credentials Partition ID with cache deletion on Denial and on keyring cleanup. - Hierarchy version 2 was never shipped. Open Question 5: lock contention. Tests: - Denial recording, the factory offline (zero remote calls, isolation, thread safety, the grace period rule), and end to end on DynamoDB Local. - Stubbed DynamoDB calls are counted on provide-client-params: botocore's Stubber rejects a call before before-call, so the Cache Guard's and the factory's "no DynamoDB calls" asserts had held vacuously. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Issue #, if available:
Description of changes:
Goal. Build a more complete multi-tenant solution with the AWS KMS Hierarchical Keyring: one that handles a tenant's KMS key becoming unavailable without flooding requests for it. When AWS KMS denies a Branch Key (for example, a 400
AccessDeniedException), the failure is cached. For a configurable timeout, the cache and the components above it stop calling Amazon DynamoDB and AWS KMS for that Branch Key.Constraint. No changes to the AWS Encryption SDK for Python or the MPL source. Only custom implementations of their public, extendable interfaces are allowed: Keyrings, Master Key Providers, Cryptographic Materials Managers, and Cryptographic Materials Caches. Only the Hierarchical Keyring's behavior is in scope, because it is the multi-tenant solution.
This PR (draft): the design doc only.
examples/src/multi_tenant/negative_caching.mdrecords the options considered and the decisions made. Implementation follows in later commits.Traces the Hierarchical Keyring's cache and KMS request paths on encrypt and decrypt, through the specification and MPL 1.11.3. Findings:
GetItem.EntryDoesNotExistfails the keyring before the Key Store is called. This is the hook the design relies on.A custom cache alone is not enough. It cannot see failures, and cannot tell which Branch Key a cache identifier is for. The design pairs:
Denials are keyed by Branch Key ID, not KMS Key ARN. One KMS key can protect many Branch Keys, and KMS can deny some of them based on their encryption context. A Branch Key's KMS key is also mutable (aws-encryption-sdk-specification#287; the MPL's
mutations/mutationsTerminalKmsArn). Keying by Branch Key ID also supports theDiscoveryandMRDiscoveryKey Store configurations.What counts as a Denial:
AccessDeniedException,DisabledException,KMSInvalidStateException,NotFoundException,IncorrectKeyExceptionExpiredTokenException,UnrecognizedClientException,InvalidSignatureExceptionbotocore only refreshes credentials proactively, never in response to an expired-token error, so a credential failure that reaches KMS will not resolve by retrying. The doc records a probe of AWS KMS that varies one part of the credentials at a time, and confirms that AWS KMS returns HTTP 400
ExpiredTokenExceptionfor expired temporary credentials. Only throttling, server errors, and transport failures are transient.Encrypt and decrypt Denials are treated the same, for every version of the Branch Key.
Negative TTL: configured per factory; the default is 5 minutes.
A Tenant Keyring Factory replaces the Branch Key ID Supplier. It holds the shared transport, cache, DynamoDB client, Key Store settings, and Negative TTL. Given a Branch Key ID and Tenant Credentials, it vends a Tenant Hierarchical Keyring wired with a Cache Guard and a Tenant KMS Client. The factory and everything it vends MUST be thread safe, and this SHOULD be tested.
The Partition ID is revisited. Refreshed credentials share no constant part, so a Partition ID derived from them changes on every refresh. It should be derived from a stable tenant identity instead.
design.md10.2 now notes this; the example's fix is deferred.Temporary CI changes (fork only). The
ci:commits on this branch exist only to allow development on this fork. Forks have no AWS role, CodeBuild project, or Slack webhooks, and theubuntu-latestrunner no longer provides Python 3.8 or 3.9. These commits are temporary and MUST be reverted before this work is proposed upstream.912ae02:decrypt_oracle, and the Slack notifications unless the repository owner isaws, and letspull.yml's gate accept those skips (itsdaily_ci.ymlchange is reverted byded3702)ci_tests.yamlon Python 3.12 and Ubuntu only (static analysis stays on 3.10, because the pinned linters predate 3.12)9787429: marks the offline examples testslocaland runs them in CI (exampleslocal,mplexampleslocal).46359a2: adds addblocalmarker and CI job. It runs the Hierarchical Keyring examples against DynamoDB Local (amazon/dynamodb-local:3.3.2, as a service container) with the fake KMS, with no AWS credentials.ded3702: only the Pull Request Workflow runs on this fork's PRs.daily_ci.ymlis reverted. Editing it made its PR trigger re-run Daily CI on every push; on a fork's schedule, its original guards already skip every job.ci_codebuild-tests.ymlkeeps onlyworkflow_call, dropping its ownpushandscheduletriggers.The tests and tox factors from
9787429and46359a2may be worth proposing upstream on their own; the fork guards and the reduced Python and OS matrix should not be.Out of scope: other keyrings, the KMS Master Key Providers, and the Caching CMM; Amazon DynamoDB failures (the application owns that resource). Propagating a
NotFoundExceptionacross Branch Key IDs is decided against in Design Question 10.This branch is based on
examples/credentials(aws#827).By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
Check any applicable:
🤖 Generated with Claude Code