Repository navigation
feat(secrets): secrets kept in AWS Parameter Store, moved there and back at start - #275
Merged
Merged
Conversation
… its fake and the move
A value can now rest outside the ledger's database: the keeper of AWS Systems Manager Parameter
Store keeps each one as it is, a SecureString of its own, and the row keeps the name and version.
- parameterStoreKeeper({ prefix, region, proxy, credentials, endpoint?, waits? }) of
@on.auto/secrets/aws, over @aws-sdk/client-ssm pinned to 3.1149.0, the version of the other AWS
clients. hold(id, value) is PutParameter of <prefix>/<id>, SecureString, Overwrite, the value as
it is, answering { keptBy: 'aws-parameter-store', held: '<name>:<version>' } with the version the
write returned; reveal is GetParameter of that name and version with decryption; release is
DeleteParameter of the name, which takes every version.
- The tier is one rule: a value of more than 4096 bytes of UTF-8 names the advanced tier, and a
smaller one names none, so a new parameter takes the account's default tier and an advanced one
keeps its tier, which the service lets no parameter leave.
- The client retries in the SDK's adaptive mode, which slows its requests once the service
throttles them, unless AWS_RETRY_MODE or the profile names another; AWS_MAX_ATTEMPTS is read as
the SDK reads it.
- ParameterNotFound and ParameterVersionNotFound are a value gone for good: reveal answers nothing,
and the store's read answers nothing, as for a row that is gone.
- A value the service refuses to keep is SecretRefused in words that lead with the parameter, "AWS
Parameter Store refused to keep the parameter <name>: <the service's words>", since the probe's
value is a UUID: over_the_keepers_quota for ParameterLimitExceeded, the account's, and
refused_by_the_keeper for PoliciesLimitExceededException, ParameterMaxVersionLimitExceeded and a
ValidationException of a write, such as a value that holds a reference in double braces. The hold
of the keeper port answers SecretRefused or KeepingFailure, so keep and replace do.
- Any other failure, a denied identity, a throttle the SDK's retries did not wait out, a timeout, a
port that cannot be reached or a fault of the service, is keeper_unavailable in the service's
words, the account of every ARN left out.
- awsWaits, 5 seconds to connect and the store's 10 for an answer, and the words without the
account are written once in src/aws/aws-requests.ts for both AWS adapters; kmsWaits and KmsWaits
become awsWaits and AwsWaits.
- The store takes keepers, { current, previous? }, and custodians only where a keeper seals: it
keeps and replaces with the current keeper, reads with the keeper a row names, a row of a keeper
neither current nor previous being keeper_unavailable in words that name it, and forgets a row
whose release fails or whose keeper is unknown, logging where its value is left.
sealedSecretStoreOn(rows, custodians) is the store whose one keeper seals under the custodians;
the server's store and the stores the tests of the connections and tool servers use are it.
- moved() moves what the previous keeper holds, 100 rows at a time after the last id of the batch
before: each value revealed, held by the current keeper under the row's id, re-held while the
row's version and holding are the ones read, and released by the previous keeper once the row
took it. A replacement made meanwhile is left as it is and nothing of it is released; a row whose
value is gone is left where it is. A row sealed under a key neither custodian holds is left where
it is and logged once for that key's kid, a value the current keeper refuses is left where it is
and logged with the secret's id, and the count skips both. A refusal over the keeper's quota
would meet every later row, so it fails the move, the rows moved before it moved. heldBy of the
rows port takes that id, in memory, on SQLite and on PostgreSQL.
- keeperProbed(keeper) holds one value under the id probe-<uuidv7>, which no secret's id can be,
reads it back and releases it, within the store's bound.
- serveFakeParameterStore() of @on.auto/secrets/testing answers PutParameter, GetParameter and
DeleteParameter of the SSM JSON protocol under X-Amz-Target through one switch, keeps the
versions and the tier of each parameter, answers a name:version selector, refuses a version or a
name it does not have, a standard-tier value over 4 KB, any value over 8 KB, a value with a
reference in double braces and a move of an advanced parameter to the standard tier, is told a
name is gone, the next writes throttled or refused under an error's name, how many parameters it
keeps, or an operation denied, stalling or faulting, and records each call without its value,
with the retry mode its user agent names. The fakes of KMS and Parameter Store and a keeper in
memory, with the TestKeepers it is made by, live in src/fakes, which imports nothing from
src/testing.
- keeperBehaviour runs the store over any keeper: a value kept as it is and no value in the row, a
replacement with the earlier version still read, a forget that leaves no version, the refusals
before the keeper is asked, and the move from the ledger and back past a hundred rows, a
replacement made meanwhile either way, a hundred values gone, rows under a key not given and
moved once it is, a third keeper's rows and a previous keeper that cannot reveal. storeBehaviour
runs it over a keeper in memory, so the ledger runs it over SQLite and PostgreSQL.
- Tests of the keeper over the fake: the call it sends, the tier at 4 KB exactly and one byte over
in ASCII and in UTF-8, an earlier version read after a later write, a release of every version, a
parameter and a version gone, a write throttled once and retried in the adaptive mode, the
standard mode where AWS_RETRY_MODE names it, writes still throttled, each value the service
refuses, the probe stopped at a parameter it refuses, a move past the parameters the account
keeps stopped and later resumed, a write, a read and a delete denied without the account or a
credential, and a service that stalls, faults or cannot be reached. A suite against a real server
runs when SECRETS_TEST_AWS_URL names one, with a write it throttles on its way there.
- The README of the package and the ledger's README say the keeper, the move and the fakes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…R names it, probed and moved
An operator can now keep no secret in the runtime's database: SECRETS_KEEPER names a prefix in AWS
Systems Manager Parameter Store, and a runtime with no other keeper needs no key at all.
- SECRETS_KEEPER is ledger by default, or aws-parameter-store:<prefix>, the prefix an absolute
parameter path of at most 14 segments of letters, digits, '.', '-' and '_', the first beginning
with neither aws nor ssm, and no slash at its end; anything else stops the start in words. Its
region is AWS_REGION's, or AWS_DEFAULT_REGION's where that is not set, kept with the name of the
variable it came from, and a keeper with neither stops the start naming SECRETS_KEEPER and
AWS_REGION.
- SECRETS_KEEPER_PREVIOUS is read only beside SECRETS_KEEPER, and names the other of the two.
- A custodian is read exactly where the ledger keeper is current or previous: with Parameter Store
alone a key setting stops the start in words that say no keeper would use it, and no key file is
made, beside a SQLite ledger or a PostgreSQL one. The rule that a ledger in PostgreSQL needs a
key keeps its words while the ledger keeps secrets.
- secretsVariables lists SECRETS_KEEPER and SECRETS_KEEPER_PREVIOUS, and the variables the runtime
reads for itself AWS_DEFAULT_REGION, so no connection may name them.
- The keeper is made through import('@on.auto/secrets/aws'), the one loader of the AWS adapters, so
the SSM client is loaded where a keeper in Parameter Store or a key in KMS is named, with the KMS
client, and a runtime that names neither loads no AWS client.
- At start the server probes a current keeper in Parameter Store with one parameter,
<prefix>/probe-<uuidv7>, written, read and deleted, a probe that fails stopping the start with
the setting, the prefix and the words of the service; then moves what the previous keeper keeps,
a move that fails stopping the start with SECRETS_KEEPER_PREVIOUS; then rotates, and logs
"Secrets are kept in AWS Parameter Store under <prefix> in <region>, and <count> kept by the
previous keeper were moved there", or the ledger's line as it was, with that count after it where
a previous keeper is named.
- The key the runtime keeps for itself moves with the rest. Under the manager each start logs "The
runtime read the key it keeps for retry_state in AWS Parameter Store under <prefix> in <region>",
its own_key_id the id that names the key's parameter, and a start that finds the parameter gone
stops with "... is no longer in AWS Parameter Store under <prefix> in <region>, the region
AWS_REGION names, so the server does not start", as after a change of region or account. Its
other failures name the manager, its prefix and its region.
- The composition of the secret store, its custodians, keepers, start and own keys, moves from
src/database to src/secrets.
- Tests: the settings and each refusal; the custodians and keepers each setting makes and what they
load; no AWS client loaded for a key in hand, and the SSM client for a keeper in Parameter Store;
the probe and a prefix the identity may not write, read or delete, or a manager that stalls; the
move into the manager and back with its counts, one that cannot read, and one past the parameters
the account keeps, stopped with the secrets moved before it moved; a server started twice under
the manager alone with no key and no key file, its own key kept there; that key moved from the
ledger; its line and its id at start, a start stopped once its parameter is deleted, and a start
in another region stopped at it after a probe that passes, serving nothing and writing nothing
there but the probe, and started again once the region is set back; and no value, key or
credential in a log line.
- A page, Keep secrets in your secret manager, under Deploy in the navigation, with the setting and
what a change of region or account does, the parameters, the identity's permissions, the probe,
the key the runtime keeps for itself, the move and back, with the settings removed only once
nothing is left in the ledger and what to do where a warning says something is, what the operator
sees and may delete there, which never includes the parameter of that key, and the limits: the
time a move takes, the SDK's adaptive retry and its two variables, a throttled move resumed by
the next start, the parameters an account keeps, past which a move stops, and the values the
service refuses, with how a pasted credential, a consent and a renewal answer them. Its examples
and quoted words are checked by tests. The configuration reference, the security page's secrets
at rest, the production, PostgreSQL and connections pages and the overview point at it, the
READMEs follow, and .env.example says a ledger in PostgreSQL needs no key where SECRETS_KEEPER
names Parameter Store.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the runtime under it The keeper of AWS Parameter Store is tried against a server of the Parameter Store API on every pull request, beside the custodian of a key in KMS, and so is a runtime that keeps no secret in its database. - The job kms becomes secrets-aws: one moto server, motoserver/moto 5.2.4 pinned by its digest, on the port 19760, answers KMS and Parameter Store, and ci-ok needs the job by its new name. - One variable, SECRETS_TEST_AWS_URL, replaces SECRETS_TEST_KMS_URL in turbo, in the job and in the KMS suite, which the keeper's suite takes too; the job checks that both suites ran, that vitest's summary lines name nothing skipped, and that the credential is in no line of the output. - A step starts the runtime twice under SECRETS_KEEPER=aws-parameter-store:/auto-runtime/secrets with no SECRETS_KEY, on the port 19762, keeps the pasted value of a connection through the HTTP API and reads its ending back through it, shows two parameters under the prefix, the value once among them, two rows of the ledger kept by the keeper that hold no value, wrapped key or kid, and no key file, then removes the connection and shows its parameter gone. - It then starts a runtime under the ledger and a key, and once more with Parameter Store as the keeper and the ledger as the previous one, and checks that the key it keeps for itself was moved and counted. Each start line is checked, with the prefix and the region, and so is the line of the key the runtime keeps for itself at both starts under the manager alone; no value, key, credential or value read back from moto reaches the log, and a step that fails ends the runtime it started and shows each log. - scripts/test-groups.test.ts follows the job's new name, and the secrets README says what CI runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Secrets can now rest in the operator's own secret manager instead of the ledger's database.
SECRETS_KEEPER=aws-parameter-store:<prefix>names AWS Systems Manager Parameter Store, and each value is then aSecureStringparameter named<prefix>/<id>, kept as it is under the manager's own encryption, so whoever runs the runtime reads, rotates and tracks it there: the standard tier up to 4 KB and the advanced tier beyond, one rule written once. A row of the ledger'skept_secretstable keeps only the parameter's name and version, so a reader reads exactly the version its row names, whatever a later write left as the latest; a replacement writes the manager first and the row second, and a release deletes the parameter with every version. A parameter gone from the manager is the value gone for good; a denied identity, a throttle the client's own retries did not wait out, a timeout or a fault of the service fails the call asconnection_unavailablein the manager's words and breaks nothing. With the manager as the only keeper, noSECRETS_KEYis needed and no key file is made, and a key setting given anyway stops the start in words; with the ledger as the current or the previous keeper, the custodian rules are as they were.At start the manager is probed as the object store's bucket is, one parameter under the prefix written, read and deleted, so a prefix the identity may not write, read or delete stops the start, naming the setting, the prefix and the manager's answer.
SECRETS_KEEPER_PREVIOUSmoves every value in one restart, from the ledger to the manager or back: each row the previous keeper holds is revealed by it, held by the current, re-held only while its version and holding are the ones read, and released by the previous once the row took it, so a replacement made meanwhile is never written over, and the runtime's own retry-state key moves with the rest; the start line saysSecrets are kept in AWS Parameter Store under <prefix> in <region>, and <count> kept by the previous keeper were moved there, and never a key or a value; a row sealed under a key neither custodian holds, or whose value the manager refuses, is left where it is and logged, and a move past the account's quota of parameters stops the start with the rows moved so far staying moved. The region comes fromAWS_REGIONorAWS_DEFAULT_REGION, which no connection may name any more. The keeper is loaded through@on.auto/secrets/awsonly where a setting names an AWS keeper or custodian, and the package's fake answers the Parameter Store protocol for the unit tests. CI runs the KMS and Parameter Store suites against one moto server, then a runtime twice under the manager alone with no key, a connection's credential given and read back, the parameters and the rows inspected, and a move from the ledger counted. The new self-host page, Keep secrets in your secret manager, says the setting, the identity's permissions, the probe, the move, the limits and what a parameter deleted in the manager does to its connection.