sondahub

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

These examples share one session: run them in order and each sees what the one before it did.

The page signs these with SigV4 and the published key, the way an SDK would.

List the secrets
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}'
Read one
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"}'
Create a secret
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"}]}'
Put a new value: the old one becomes 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.PutSecretValue" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -d '{"SecretId":"dev/myapp/api-key","SecretString":"{\"api_key\":\"def456\"}"}'
Read the previous version
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"}'
Rotate the database 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.RotateSecret" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -d '{"SecretId":"prod/db/orders-postgres"}'
A secret scheduled for deletion refuses reads
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"}'
Restore it
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"}'
A random 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:

SecretWhat it is
prod/payments/stripeStripe keys for the checkout service
prod/db/orders-postgresOrders database, app user (rotated every 30 days) — rotation on
staging/api/sendgridSendGrid key for staging email
dev/app/feature-flagsFeature flags read at boot
prod/tls/api-private-keyPrivate key for api.example.com (binary, DER) — customer-managed KMS key
shared/github/deploy-tokenDeploy token for the release pipeline, replicated to Ireland — replica in eu-west-1
legacy/ftp-passwordThe old FTP drop — scheduled for deletion — scheduled for deletion
prod/oauth/google-clientOAuth 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.

POST/sandbox/aws-secrets-managerX-Amz-Target: secretsmanager.<Action>, a JSON 1.1 body, SigV4. Errors are {"__type", "Message"} with x-amzn-ErrorType, and the validation errors Coral gives.

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).