sondahub / AWS Secrets Manager sandbox
An AWS Secrets Manager mock that boto3 and the AWS SDKs run against
AWS Secrets Manager’s API, answered by sondahub: every action, signed with SigV4, with versions and their staging labels, idempotent tokens, deletion with a recovery window, rotation played out step by step, resource policies, replicas in other regions and random passwords — the same errors, word for word where it matters. Point the SDK’s endpoint URL here and test the code that reads your secrets.
An independent imitation for testing. Not affiliated with, or endorsed by, Amazon Web Services.
Connect
- Instead of
- https://secretsmanager.<region>.amazonaws.com
- Endpoint URL
- https://api.sondahub.com/sandbox/aws-secrets-manager
- Protocol
- POST, JSON 1.1, X-Amz-Target: secretsmanager.<Action>
- Credentials
- AKIASONDAHUB000001 / probe-secret (checked) — or any key
- Region
- any of the 32 commercial regions; us-east-1 in the examples
- OpenAPI 3
- https://api.sondahub.com/sandbox/aws-secrets-manager/openapi.json
Set the SDK’s endpoint URL and it signs and sends every call here, as it would to AWS. For reads of the seed account that is all it takes; for your own writes to stick from one call to the next, add the few lines that carry X-Sondahub-Session.
Python (boto3)
import boto3
sm = boto3.client(
'secretsmanager',
region_name='us-east-1',
endpoint_url='https://api.sondahub.com/sandbox/aws-secrets-manager',
aws_access_key_id='AKIASONDAHUB000001', # its signature is checked
aws_secret_access_key='probe-secret',
)
# carry the sandbox session from each answer to the next request
session = {}
def send(request, **kwargs):
if 'token' in session:
request.headers['X-Sondahub-Session'] = session['token']
def keep(http_response, **kwargs):
token = http_response.headers.get('X-Sondahub-Session')
if token:
session['token'] = token
sm.meta.events.register('before-sign.secrets-manager.*', send)
sm.meta.events.register('after-call.secrets-manager.*', keep)
sm.create_secret(Name='dev/myapp/api-key', SecretString='{"api_key": "abc123"}')
print(sm.get_secret_value(SecretId='dev/myapp/api-key')['SecretString'])
Node (@aws-sdk/client-secrets-manager)
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
const sm = new SecretsManagerClient({
region: 'us-east-1',
endpoint: 'https://api.sondahub.com/sandbox/aws-secrets-manager',
credentials: { accessKeyId: 'AKIASONDAHUB000001', secretAccessKey: 'probe-secret' },
})
// carry the sandbox session from each answer to the next request
let session = null
sm.middlewareStack.add((next) => async (args) => {
if (session) args.request.headers['x-sondahub-session'] = session
const out = await next(args)
session = out.response.headers['x-sondahub-session'] ?? session
return out
}, { step: 'build', name: 'sondahubSession' })
const { SecretString } = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/payments/stripe' }))
AWS CLI
export AWS_ACCESS_KEY_ID=AKIASONDAHUB000001 AWS_SECRET_ACCESS_KEY=probe-secret AWS_DEFAULT_REGION=us-east-1 aws secretsmanager get-secret-value --secret-id prod/payments/stripe \ --endpoint-url https://api.sondahub.com/sandbox/aws-secrets-manager
The CLI can’t carry the session token from one command to the next, so it reads the seed account; writes answer exactly as AWS would but don’t stick.
Import it. In Sonda: Import → From a URL, paste the OpenAPI address. You get a folder per resource and a request per operation, with example bodies and names from the seed, so most requests work as they are. Then set the auth: AWS Signature Version 4, service secretsmanager, with the key above — each action has a path of its own (/sandbox/aws-secrets-manager/GetSecretValue), which the sandbox reads when X-Amz-Target is missing. Any client that imports OpenAPI 3 takes the same address.
Try it here
The page signs these with SigV4 and the published key, the way an SDK would.
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.ListSecrets" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"MaxResults":10}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.GetSecretValue" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"prod/payments/stripe"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.CreateSecret" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"Name":"dev/myapp/api-key","Description":"An API key for myapp","SecretString":"{\"api_key\":\"abc123\"}","Tags":[{"Key":"env","Value":"dev"}]}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.PutSecretValue" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"dev/myapp/api-key","SecretString":"{\"api_key\":\"def456\"}"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.GetSecretValue" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"dev/myapp/api-key","VersionStage":"AWSPREVIOUS"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.RotateSecret" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"prod/db/orders-postgres"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.GetSecretValue" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"legacy/ftp-password"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.RestoreSecret" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"legacy/ftp-password"}'
curl https://api.sondahub.com/sandbox/aws-secrets-manager \
--aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "AKIASONDAHUB000001:probe-secret" \
-H "X-Amz-Target: secretsmanager.GetRandomPassword" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"PasswordLength":24,"ExcludeCharacters":"/@\"'\''\\"}'
From the shell, keep the token yourself: curl -i shows X-Sondahub-Session; send it back with -H "X-Sondahub-Session: …". How sessions work.
The seed account
Account 123456789012, with these secrets in every region:
| Secret | What it is |
|---|---|
prod/payments/stripe | Stripe keys for the checkout service |
prod/db/orders-postgres | Orders database, app user (rotated every 30 days) — rotation on |
staging/api/sendgrid | SendGrid key for staging email |
dev/app/feature-flags | Feature flags read at boot |
prod/tls/api-private-key | Private key for api.example.com (binary, DER) — customer-managed KMS key |
shared/github/deploy-token | Deploy token for the release pipeline, replicated to Ireland — replica in eu-west-1 |
legacy/ftp-password | The old FTP drop — scheduled for deletion — scheduled for deletion |
prod/oauth/google-client | OAuth client for Sign in with Google (resource policy: the web role only) — resource policy |
Every value in the seed is fake and made up when the site is built. What you write is kept in your own session token and nowhere else — but this is a public playground: never send it a real secret. Not affiliated with, or endorsed by, Amazon Web Services.
Versions and labels
Each value is a version, and its id is the ClientRequestToken that made it (the SDKs make one for you). AWSCURRENT is what a read gets; a new value takes it, and the old one becomes AWSPREVIOUS. Send the same token with the same value again and you get the same version back — with another value, ResourceExistsException. UpdateSecretVersionStage moves a label: rolling back is moving AWSCURRENT to the AWSPREVIOUS version, naming where it is now in RemoveFromVersionId.
DeleteSecret schedules deletion 7 to 30 days out: until then reads answer “marked for deletion”, ListSecrets hides it unless IncludePlannedDeletion, its name can’t be reused, and RestoreSecret brings it back. ForceDeleteWithoutRecovery deletes it now.
What it answers
All 23 Secrets Manager actions: CreateSecret, GetSecretValue, BatchGetSecretValue, DescribeSecret, ListSecrets, ListSecretVersionIds, PutSecretValue, UpdateSecret, UpdateSecretVersionStage, DeleteSecret, RestoreSecret, RotateSecret, CancelRotateSecret, TagResource, UntagResource, GetResourcePolicy, PutResourcePolicy, DeleteResourcePolicy, ValidateResourcePolicy, GetRandomPassword, ReplicateSecretToRegions, RemoveRegionsFromReplication, StopReplicationToReplica.
Questions
Is this AWS?
No — an independent imitation of AWS Secrets Manager for testing, not affiliated with or endorsed by Amazon Web Services. No AWS account is involved, nothing is encrypted with a real KMS key, and no Lambda runs.
Will boto3 and the AWS SDKs work against it?
Yes, with the endpoint URL set. The Python set-up on this page was run against the sandbox with boto3: create, read by name, ARN and label, versions and their labels, rotation, deletion and restore, batch reads, paging, random passwords, replicas read in another region, and a bad signature refused. The Node set-up follows the AWS SDK for JavaScript v3’s endpoint and middleware options; the CLI line uses --endpoint-url.
Is the signature checked?
For the published key, AKIASONDAHUB000001 / probe-secret: the sandbox recomputes SigV4 and a mismatch answers InvalidSignatureException, as AWS does. Any other access key is taken on trust (the sandbox can’t know its secret), but the rest is checked for every key: the credential scope must name secretsmanager and a real region, X-Amz-Date must be within 15 minutes, and a request without Authorization is MissingAuthenticationTokenException.
How does rotation work without Lambda?
The sandbox plays the rotation function: RotateSecret runs the four steps at once — a new value at AWSPENDING (a fresh password where the old one was, in JSON or plain text), then AWSCURRENT moves to it and the old version becomes AWSPREVIOUS. Any Lambda ARN works. A function whose name contains “fail” stops after createSecret, leaving the new version at AWSPENDING — the next RotateSecret answers “A previous rotation isn’t complete”, so your recovery path can be tested. Schedules are kept and reported but don’t run on their own.
Do regions matter?
Yes, as at AWS: the region in your signature is the region you work in. The seed account’s secrets answer in every region; a secret you create lives in the region you signed for, and in the regions you replicate it to, where it is read-only. shared/github/deploy-token lives in us-east-1 with a replica in eu-west-1.
What is not modelled?
Managed rotation for RDS, Redshift and DocumentDB (owning services), real KMS encryption and key policies, and CloudTrail. LastAccessedDate comes from the seed: reads are not written down. A secret keeps every labeled version and the newest unlabeled ones, 20 in all (AWS keeps up to 100).