AWS Secrets Manager
The AWS Secrets Manager provider: a local vault connection to secrets under a prefix, the default credential chain, how versions, tags and deletion map onto the service, and what differs from a local vault.
A vault’s aws-secrets-manager connection keeps secrets under
one prefix in one region of
AWS Secrets Manager (opens in a new tab). The
crate is sourceorrery-provider-aws, on by default in the CLI, the
sourceorrery crate, the C ABI and every SDK (Cargo feature
provider-aws, part of remote).
sourceorrery vault create production --recovery-key-out production.recovery-key \ --aws-secrets-manager eu-west-1 \ --aws-secrets-manager-prefix myapp/ \ --aws-secrets-manager-endpoint http://localhost:4566 \ --aws-access-key-id AKIA... --aws-secret-access-key-file key.txt # optional: store an access keyThe prefix is optional: every secret name is placed under it. The endpoint
is optional too, for a private endpoint or an emulator (https://, or
http:// for this machine). Without a stored access key the connection
uses the AWS default credential chain.
A prefix should end in a separator (myapp/): names never start with a
hyphen, but myapp alone would place myapp and myapp-other under one
roof.
What the service’s tags decide
Section titled “What the service’s tags decide”A version’s enabled flag, its time window and content type, and the
rotation state live in the secret’s tags, which Secrets Manager does not
authenticate: an identity allowed secretsmanager:TagResource can
re-enable a disabled version, clear a window or mark a retired version as
a validated rotation candidate, and the client cannot tell. Treat
TagResource and UntagResource on these secrets as policy
administration; the local vault’s tags and checkpoints have no
counterpart here (threat model, known gap 14).
SourceOrrery’s policy binds what SourceOrrery returns; an identity with
secretsmanager:GetSecretValue reads any version through AWS’s own tools,
so deny it in IAM (or with tag-based conditions) where that matters.
How a version is published
Section titled “How a version is published”A new version is never current under a looser policy than it was written with, and before it is current the old one is never under a looser policy than it had (ADR 0011):
- The secret is described (its tags) and the request is checked: nothing is sent for tags, a content type or a value the service would refuse (below).
- When the stricter of the old and new policy (disabled if either is,
the later
not_before, the earlierexpires_on) is stricter than the old one, the secret is tagged with it. PutSecretValuemakes the new version current.- The tags reach the new properties: stale tags that are not policy are removed, every wanted tag is written, and stale policy tags are removed last, so the policy never passes below the new one.
A refused PutSecretValue puts the old policy back and returns the
refusal. A lost answer, or a failure in step 4, is outcome_unknown, with
the secret held at least as strict as asked. update_secret_properties
runs step 4 alone. A new secret gets its value and tags in one
CreateSecret. Secrets Manager has no conditional write, so this holds
for one writer at a time per secret: two writers changing one secret’s
policy at the same moment can see each other’s tags until the last step
lands.
Credentials
Section titled “Credentials”An access key stored in the connection is used when there is one.
Otherwise the official AWS SDK’s default credential chain supplies the
credentials, in its usual order: the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN), the shared config and credentials files with
AWS_PROFILE, SSO, web identity (AWS_WEB_IDENTITY_TOKEN_FILE and
AWS_ROLE_ARN, the path GitHub Actions and Kubernetes use), the ECS task
role and the EC2 instance role. Credentials are resolved once when the
connection is first used: a process the chain finds nothing for fails there with
unauthenticated, before any request. Credentials the service rejects
(UnrecognizedClientException, an expired token) are unauthenticated
too; a policy that denies the call is permission_denied.
Mapping
Section titled “Mapping”| SourceOrrery | Secrets Manager |
|---|---|
secret name |
the secret <prefix><name> |
| version id | the VersionId. Versions this provider writes get a time-ordered 32-hex-digit id (the ClientRequestToken), so they sort without relying on timestamps; versions other tools wrote keep their ids and sort by creation time |
| newest version | the version staged AWSCURRENT |
| value | SecretString when the bytes are UTF-8 without NUL, SecretBinary otherwise. Nothing is wrapped around the value |
enabled, content_type, not_before, expires_on, updated_on |
tags sourceorrery:disabled, sourceorrery:content_type, sourceorrery:not_before, sourceorrery:expires_on, sourceorrery:updated_on on the secret |
tags |
every other tag of the secret, except AWS’s own (aws:), which are left alone |
created_on |
the version’s CreatedDate |
set_secret |
CreateSecret the first time; after that the tags narrowed to the stricter policy, PutSecretValue, then the tags (above). The options’ properties become the secret’s tags (user tags are replaced only when the options carry some) |
get_secret(name, None) |
DescribeSecret and ListSecretVersionIds, then the core’s policy over the versions, then GetSecretValue by version id |
update_secret_properties |
UntagResource of stale tags that are not policy, TagResource, then UntagResource of stale policy tags; the newest version only (below) |
delete_secret |
DeleteSecret with a 30-day recovery window |
recover_deleted_secret |
RestoreSecret |
purge_deleted_secret |
DeleteSecret with ForceDeleteWithoutRecovery |
list_* |
ListSecrets filtered by the name prefix, planned deletions included, then ListSecretVersionIds per secret |
audit_events |
unsupported: AWS CloudTrail holds the trail |
| descriptor | backend aws-secrets-manager, id <region>:<prefix>, created unknown (shown as -), recovery recoverable, retention 30 days |
Every policy decision is made by the core from the properties above, exactly as for a local vault.
What differs from a local vault
Section titled “What differs from a local vault”- Properties belong to the secret, not to a version. Secrets Manager
has no per-version metadata, so
enabled,content_type,not_beforeandexpires_onapply to every version at once: disabling the newest version disables the secret, with no fall-back to an older version, and asetwith new options redefines the secret’s properties. For that reasonupdate_secret_propertiesaccepts only the newest version and answersunsupportedfor any other, saying so. The lifecycle conformance suite runs without thePerVersionPropertiescapability (ADR 0008), skipping the one scenario that needs it; the provider’s own test covers what happens instead. - No rotation workflow.
sourceorrery secret rotateneeds a disabled candidate next to the enabled current version, which per-secret properties cannot hold; the provider reports noPerVersionPropertiescapability and the workflow answersunsupportedbefore writing anything (rotation). Secrets Manager’s own rotation (Lambda rotation functions) is the mechanism there. - Tags are bounded, and checked first. A secret carries at most 50
tags (AWS’s own
aws:tags not counted), keys of 1 to 128 and values of at most 256 characters, made of letters, digits, spaces and_ . : / = + - @(AWS’s tagging rules); the properties above take up to five of them, and a write counts every tag the secret holds at once while it runs. A key starting withaws:orsourceorrery:(in any case) is refused, as is a content type with another character (text/plain; charset=utf-8), since it is stored as a tag, and a value over 64 KiB. All of this isinvalid_argumentbefore any request that writes. - Unlabelled versions age out. A version that is neither
AWSCURRENTnorAWSPREVIOUSis deprecated; Secrets Manager keeps up to 100 of them and removes the oldest beyond that, after which the version isversion_not_found. - Recovery is 30 days.
delete_secretschedules deletion with the longest window the service offers.scheduled_purge_onis AWS’s recovery deadline. Permanent deletion is asynchronous and can happen later; a name is reusable after deletion completes, including afterpurge_deleted_secret. See AWS’s deletion semantics (opens in a new tab). - The deletion start is unknown. AWS returns the recovery deadline,
not the original deletion-request time. The provider uses the Unix
epoch (
1970-01-01T00:00:00Z) as the unknown sentinel fordeleted_on; consult CloudTrail for the actual request time. It does not subtract 30 days, because another tool may have requested a shorter recovery window. - Timestamps come from the service except
updated_on, which the provider writes into a tag when it changes properties, and the unknowndeleted_onsentinel above. - Listing costs one request per secret, after the
ListSecretspages. The paged methods cut pages out of that listing. - Two audit trails. The vault’s own log records every call
SourceOrrery makes at the connection, with its outcome and, for a
change, the attempt recorded before it was sent
(providers):
sourceorrery auditshows it andaudit exportsends it on, like any local event. The provider’s own trail (below) records what it served, including to clients other than SourceOrrery. The provider client itself keeps no log (audit_eventsisunsupportedon it). - Two writers can race. Nothing locks a secret between the describe and the writes that follow it, and the service has no conditional write: the publication above holds for one writer at a time per secret.
Audit trail
Section titled “Audit trail”AWS CloudTrail holds the provider’s trail, beside the vault’s own log of
each call (above); sourceorrery audit prints this pointer for every
connection that stores a secret.
Secrets Manager’s API calls are management events, recorded by default
with the identity the process authenticated as: a read is
GetSecretValue (preceded by DescribeSecret and ListSecretVersionIds
for the policy decision), a write CreateSecret or PutSecretValue,
property changes TagResource and UntagResource, soft delete
DeleteSecret, recovery RestoreSecret. One query shows SourceOrrery’s
operations on the vault’s prefix:
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventSource,AttributeValue=secretsmanager.amazonaws.com \ --start-time 2026-10-01T00:00:00Z --query 'Events[].CloudTrailEvent' --output text \ | jq -c 'select(.requestParameters.secretId | startswith("myapp/")) | {eventTime, eventName, who: .userIdentity.arn, secret: .requestParameters.secretId, errorCode}'A CloudTrail Lake event data store, or a trail delivered to S3 and read by
a collector, gives the same records continuously; route them where
sourceorrery audit export from the local vaults goes
(enterprise.md).
Errors
Section titled “Errors”| Situation | Code |
|---|---|
| the default chain finds no credentials; the service rejects them | unauthenticated |
the policy denies the call (AccessDeniedException) |
permission_denied |
| the service cannot be reached, times out, throttles or fails internally | provider_unavailable (transient) |
| a tag, content type or value the service would refuse (checked before any write) | invalid_argument |
InvalidRequestException, InvalidParameterException, LimitExceededException and other rejections |
invalid_argument with the service’s message |
| a write’s answer was lost, or the value was published and its tags were not all written | outcome_unknown (providers) |
| a version that does not exist, or that was removed | version_not_found |
No error message repeats a value: the SDK’s messages carry the service’s error code and text, which name secrets and parameters only.
Testing
Section titled “Testing”cargo test -p sourceorrery-provider-aws runs the mapping and error
classification tests. With SOURCEORRERY_AWS_TEST_ENDPOINT naming an
emulator (and AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY set to
anything), the live test runs the lifecycle conformance suite without
PerVersionProperties, one fresh prefix per scenario, plus the
per-secret behaviour above:
npm ci --ignore-scripts --prefix scripts/ci/lstk # the pinned LocalStack CLI# From the repository root, with LOCALSTACK_AUTH_TOKEN set:scripts/ci/lstk/node_modules/.bin/lstk startSOURCEORRERY_AWS_TEST_ENDPOINT=http://127.0.0.1:4566 AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \ cargo test -p sourceorrery-provider-aws --test live -- --show-outputThe provider-aws CI job does the same against LocalStack, started by
lstk with the image pinned in .lstk/config.toml and the
LOCALSTACK_AUTH_TOKEN repository secret (a LocalStack CI auth token),
then writes secrets into a Secrets-Manager-backed vault with the CLI and
reads them back through the Rust and C conformance runners with the SDK
harness’s --remote scenarios (without --remote-per-version). Pull
requests from forks do not receive the secret: their emulator tests are
skipped with a warning and run after merge.
LocalStack refuses ListSecretVersionIds for a secret scheduled for
deletion, which AWS allows, and gives version creation times in whole
seconds. The provider then falls back to the secret’s own creation time,
so the two deletion tests compare everything except the times and report
the divergence as a workflow warning. Against an endpoint that lists those
versions, they compare the times too.