Repository navigation
feat(secrets): the secrets key in AWS KMS; a store that cannot answer now fails the call - #271
Merged
Merged
Conversation
…ll in its words
The paths a refusing or silent custodian reached still died, read a failure as nothing or revoked
what the runtime had been given; each now fails the call in the store's words and keeps what it can.
- ClientRegistrations.latestAt, named and keep fail with { _tag, detail }, the store's tag and
words: makeClientRegistrations no longer reads a row it cannot read now as no registration, nor
dies on a keep. The consent client answers both as registration_unavailable: a start and a
finish as unavailable, "The client the runtime registered for the connection drive cannot be
kept or read now: <the store's words>", a renewal as a failed call that breaks nothing, and a
removal by logging that the tokens were not revoked.
- A registration made at the authorization server that the store cannot keep now is logged at WARN
by its issuer and client id, never its secret, held in memory by the consent client, and kept at
the next start at that issuer before anything is read or registered, so no second client is
registered there while the process runs. One the store refuses, secret_refused, is never held,
since it will never be kept: it is logged as staying registered there unused, and the start fails
as registration_refused, conflict in the refusal's words.
- SecretKeeping's failures carry their tag. A renewal whose replacement the store cannot make now
revokes nothing, holds the renewed tokens in memory and fails the call in the store's words; while
its claim lasts it serves the access token it holds, and the next renewal claims the row again and
keeps those tokens without asking the server, or takes the value another process kept. A value
the store refuses, secret_refused, still revokes. The renewals at the vendor and through Auto's
consent service keep through the same replace.
- start answers unavailable where its verifier cannot be kept; finish where the verifier cannot be
read, redeeming nothing, and where the tokens cannot be kept once the code is redeemed, revoking
them and saying the person starts again, since the code is spent; give_credential where the value
cannot be kept, leaving the link open. A removal logs tokens it cannot read now and goes on.
- The log of a credential that cannot be read now keys its cause by the failure's tag and kid, not
by its words.
- valuesInTheClear of @on.auto/secrets/testing refuses a value past its bound as a SecretRefused.
- Tests over a recording custodian told to refuse: registrations read and kept with the store's
tag, a start, a finish before and after the code is redeemed, give_credential, two removals,
renewals at the vendor and through the service, a row another process replaced before the next
claim, a row that cannot be read now, and the log of each cause; and a registration the store
refuses, registered again at the next start and answered as conflict.
- The connections and tool-servers READMEs, the HTTP reference's responses of a connection, and the
descriptions of give_credential and finish, with docs/assets/openapi.json written again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts probe and a 10 second bound
A data key can now be wrapped by a key that never leaves AWS KMS: each value keeps its own data key,
KMS wraps it at each keep and unwraps it at each read, and each use is an entry in the account's
trail under a context that names the purpose.
- @on.auto/secrets/aws, a subpath of its own over @aws-sdk/client-kms pinned to 3.1149.0, the
version of the other AWS clients, so that only a setting that names a key in KMS loads the KMS
client. kmsCustodian({ keyId, region, proxy, credentials, endpoint?, waits? }) makes the
custodian as an effect: for the ARN of an alias it asks DescribeKey once, within the store's
bound, and pins the key the alias names, so an alias pointed at another key makes the custodian
of another key; for the ARN of a key it asks nothing. A DescribeKey refused or not answered
fails the making in the service's words, with the alias as its kid.
- The custodian wraps with Encrypt under the pinned key's ARN, the data key as Plaintext and the
context { 'on.auto/runtime': 'secret data key' }, answering the ciphertext blob as base64, and
unwraps with Decrypt under the same key and context. Its kid is the key's id, the last path
segment of the key's ARN.
- Its client takes the settings of awsClientSettingsOf(proxy) as the S3 client does, and waits 5
seconds to connect and the store's 10 for an answer. A refusal that is the caller's fault,
DisabledException, KMSInvalidStateException, AccessDeniedException, NotFoundException,
InvalidCiphertextException or IncorrectKeyException, is custodian_refused in the service's words
with the kid. A request not answered, a throttled one, which is anything isThrottlingError of
@smithy/core/retry judges so, ThrottlingException and LimitExceededException among them, a
fault of the service or a credential not found is keeper_unavailable with the kid, "The
custodian of the key <kid> did not answer a request to wrap a data key: <words>", so the log of
each cause tells custodians apart. The account of every ARN is taken out of those words.
- The store's bound of a hold and a reveal is 10 seconds, the one rule in answer-bound.ts, shorter
than the fifteen seconds a renewal's claim keeps for its write.
- custodianProbed(custodian) wraps and unwraps one data key within that bound and zeroes both
copies, for the server to probe a custodian at start.
- serveFakeKms({ keys, aliases? }) of @on.auto/secrets/testing answers Encrypt, Decrypt and
DescribeKey of the KMS JSON protocol under X-Amz-Target, with a blob sealed under a key of its
own, which holds no plaintext in the clear and opens only under its key and context; it is told
a key is disabled, pending deletion, denied or stalling, made to answer an error such as a
throttling one, points an alias at another key, and records each call with its key id.
- storeBehaviour takes the custodians it runs over, local ones by default, and recordingCustodian
wraps any custodian; the suite proves no row holds a data key in the clear.
- Tests: the custodian's kid, its Encrypt and Decrypt with their key and context; an alias
resolved once and its key pinned, a custodian of another key once the alias names another, an
alias unknown, denied or silent; each refusal in the words of KMS without the account or a
credential; ThrottlingException, LimitExceededException and an HTTP 429 as keeper_unavailable
with the kid; a KMS that stalls or cannot be reached given up on within 100 ms waits;
storeBehaviour over keys in the fake; the probe answering, refused and silent at 10 seconds of
TestClock; the fake's blobs, contexts, descriptions and refusals; and storeBehaviour, an alias
pointed at another key, an unknown key and the context against a real server of the KMS API
when SECRETS_TEST_KMS_URL names one, skipped otherwise, which passes against moto's server mode.
- The README sets out the custodian of a key in KMS and of an alias, the bound and its words, the
fake and the server suite, and that only the aws subpath loads any part of the AWS SDK.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bed at start and rotated to
A runtime can name a key in AWS KMS in place of a key in hand: SECRETS_KEY_KMS and
SECRETS_KEY_KMS_PREVIOUS join the settings of the secrets key, the server makes the KMS custodian
where one names it, probes it before it reads anything, and rotates to it and from it as it rotates
between keys in hand.
- The value is the ARN of a symmetric key or of an alias, arn:aws:kms:<region>:<account>:key/<id>,
a multi-Region mrk- id among them, or arn:aws:kms:<region>:<account>:alias/<name>, refused in
words otherwise; the region is the ARN's. One of SECRETS_KEY, SECRETS_KEY_FILE and SECRETS_KEY_KMS
at most, the same for the previous key, which stands only beside a current one; a ledger in
PostgreSQL takes any of the three. secretsVariables lists all six, so connections.variables
refuses them.
- database/secret-store.ts makes a localCustodian of a key in hand, and database/kms-custodians.ts
loads kmsCustodian through import('@on.auto/secrets/aws') where a setting names a key in KMS,
with the region, the proxy settings and awsCredentialChain(proxy), so a runtime without the
setting loads no part of @aws-sdk/client-kms. ledgerStoresOpenedBy takes the loader, which the
tests replace. An alias is resolved to its key there, and a resolution that fails stops the
start, "The secrets settings are invalid. SECRETS_KEY_KMS: The KMS alias alias/<name> it names
could not be resolved to its key at start: <the custodian's words>".
- Before the rows open, a current key in KMS is probed with custodianProbed: a data key wrapped,
unwrapped and zeroed. A probe that fails stops the start, "The secrets settings are invalid.
SECRETS_KEY_KMS: The KMS key <id> it names could not wrap and unwrap a data key at start:
<the custodian's words>", never a credential or the account.
- The start's line gains its third form, "... sealed under the key <id> that the KMS key <arn>
holds, and <count> sealed under the previous key were sealed again under it", annotated
secrets_key_from: kms, and the words of an own key that cannot be read name
SECRETS_KEY_KMS_PREVIOUS. secretsKeysOf gives way to keyInHandOf, of one key in hand.
- Tests: the six variables, the ARNs of a key, a multi-Region key and an alias, eight values that
are no such ARN, the kinds of key refused side by side; no module of @aws-sdk/client-kms loaded
by a server whose key is in hand, from the environment or from the file, and the client loaded
once a setting names a key in KMS; the loader called for a current or a previous key in KMS with
its region, the credential chain and the proxy settings; the probe and its context, and the start
stopped for a key KMS does not know, disabled, pending deletion, denied or silent, against the
fake; an alias resolved before the probe, the start stopped for an alias KMS does not know, and
the rows of an alias pointed at another key wrapped again under it with the key before as the
previous one; the rotation from a key in hand to a key in KMS and back, two each way, with
nothing of a key, a data key or a credential in the log; a connection of a server whose key is
disabled meanwhile failing its call as connection_unavailable in the words of KMS and called
again once the key is enabled, through the SDK's own endpoint and credential variables; a
spawned server logging the KMS line at two starts and stopping for a disabled key; and the
page's settings, policy and quoted refusal.
- docs/self-host/production.md gains "Hold the secrets key in a KMS", with kms:DescribeKey for an
alias, how to move to another key, and what a first read and a renewal answer under a disabled
key; configuration.md has its rows, security.md's "Secrets at rest" its sentence and the wrapping
KMS makes, postgresql.md, docker.md, connections.md and the npm README the third setting where
they named two, and auto-runtime.example.yaml and .env.example SECRETS_KEY_KMS beside the others.
The server README sets out the secrets key, and the outbound README the custodian among its AWS
clients.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… under a key it makes there The custodian of a key in KMS is tried against a server of the KMS API on every pull request, and so is a runtime whose secrets that key holds. - The job kms starts moto's server mode, motoserver/moto 5.2.4 pinned by its digest, on the port 19760, and makes a key there through the KMS API, CreateKey, before anything else runs. - It runs the tests of @on.auto/secrets with SECRETS_TEST_KMS_URL, and checks that the suite against a server of the KMS API ran, that vitest's Test Files and Tests summary lines name nothing skipped, and that the credential is in no line of their output. - It starts the runtime twice under SECRETS_KEY_KMS, the ARN of that key, on the port 19761, and checks, as the PostgreSQL smoke checks its own, that each start logs "Secrets are kept in the ledger's database, sealed under the key <id> that the KMS key <arn> holds, and 0 sealed under the previous key", and that neither the secret access key nor the data key of the row the runtime keeps for itself, unwrapped by moto under the purpose's context and masked in both its forms before it is used, reaches its log. A step that fails ends the runtime it started and shows the log of each start. - ci-ok needs the job, and turbo passes SECRETS_TEST_KMS_URL to the tests, as it passes the variables of PostgreSQL and the object store. - scripts/test-groups.test.ts judges the groups that start PostgreSQL and the object store by the variables the test job sets, and checks that the job against moto runs the tests of exactly the packages that read the variables it sets, and that one of the two jobs sets each variable turbo passes to the tests. - The secrets README says what CI runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rami-hatoum
force-pushed
the
feat/the-secrets-key-in-a-kms
branch
from
October 20, 2026 19:37
6f27cc9 to
5b132fe
Compare
rami-hatoum
pushed a commit
that referenced
this pull request
Oct 21, 2026
🤖 I have created a release *beep* *boop* --- ## [0.16.0](v0.15.0...v0.16.0) (2026-10-21) ### Features * **api:** an agent asked to connect a service knows the runtime does it ([#277](#277)) ([d4e9d1f](d4e9d1f)) * **secrets:** secrets kept in AWS Parameter Store, moved there and back at start ([#275](#275)) ([eae2d70](eae2d70)) * **secrets:** the secrets key in AWS KMS; a store that cannot answer now fails the call ([#271](#271)) ([374c1b2](374c1b2)) * **server:** the agent's relay reaches its runtime again within seconds, on the protocol's means ([#278](#278)) ([92da8a1](92da8a1)) ### Bug Fixes * **server:** a ledger of another version says where it is, what to do and where to ask ([#273](#273)) ([168a756](168a756)) * **tool-servers:** a path argument that holds a slash is sent encoded as one segment, not refused ([#276](#276)) ([9942eda](9942eda)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: beonautobot[bot] <298957281+beonautobot[bot]@users.noreply.github.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.
A secret store whose custodian may refuse or stall reaches paths that still died, swallowed the failure or revoked tokens on a failure that is only now. A dynamic client registration the store cannot read now is a failure of the call and never "no registration": a start answers
unavailablein the store's words, and a renewal fails the call and breaks nothing. A renewal whose replacement the store cannot keep now holds the renewed tokens in memory under its claim, revokes nothing, fails the call, and keeps them at the next claim without asking the vendor again; a value the store refuses revokes, as before. A start whose verifier cannot be kept, a finish whose verifier cannot be read, a finish whose tokens cannot be kept after the code is redeemed, a credential that cannot be kept, and a removal whose tokens cannot be read each answerunavailablein the store's words, and the finish says the person starts again, since the code is spent. The store answers each call within ten seconds, shorter than the fifteen a renewal's claim keeps for the write, and the log's cause is keyed by the failure's tag and the key's id.The secrets key can now be held in AWS KMS.
SECRETS_KEY_KMSnames a symmetric key or an alias by its ARN; the custodian wraps each value's data key withEncryptunder the contexton.auto/runtime: secret data keyand unwraps it withDecrypt, over the same proxy, credential chain and waits as the object store's client, and the server loads it through the@on.auto/secrets/awssubpath only where the setting names it, so a runtime without it loads no part of the KMS client. At start the custodian is probed with one data key wrapped, unwrapped and zeroed, and a key that is disabled, pending deletion, unknown or denied stops the start in words that name the setting, the key's id and the service's own words, never a credential or the account. The start line says which key the ledger's secrets are sealed under, and rotation from a key in the environment or a file to a key in KMS, and back, is one restart with the previous setting. A key disabled while the runtime runs fails each call asconnection_unavailableand breaks nothing, and the calls succeed again once it is enabled. The package's fake answers the KMS protocol for the unit tests, and CI runs the same suite and a runtime started twice against moto's server, checking that no key, data key or credential reaches the output. The self-host pages say how to hold the key in a KMS, what the identity needs, what disabling the key does, and that KMS's own rotation of its key material needs nothing.