How to migrate from AWS Secrets Manager

Secrets Manager is built for services that read their own configuration at runtime with an IAM role. It is much less comfortable for the part of the job that happens on a laptop, in a pull request, or in a conversation with a new hire — which is the part Sink is for.

Updated

What moves and what does not

Sink reads the current value of each secret under a name prefix and stores it as a versioned environment variable. What stays behind in AWS is everything that depends on being inside AWS: rotation Lambdas, IAM-based access at runtime, resource policies, replica regions. Sink is not a drop-in replacement for those, and it does not pretend to be — it is where the values are versioned, diffed and shared with people.

Make a read-only credential first

The import needs two actions and nothing else. Create a dedicated IAM user or an STS session for the migration, attach exactly this, and delete it when you are done:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["secretsmanager:ListSecrets", "secretsmanager:GetSecretValue"],
    "Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:myapp/prod/*"
  }]
}

Scope the Resource to the prefix you are importing. An access key with broader rights than the secrets it unlocks is the thing worth avoiding here; if you are using temporary credentials, pass the session token too.

Run the import

sink login
sink init                         # writes .sink.json — ids only, safe to commit

export SINK_AWS_SECRETS_MANAGER_ACCESS_KEY_ID=AKIA...
export SINK_AWS_SECRETS_MANAGER_SECRET_ACCESS_KEY=...
export SINK_AWS_SECRETS_MANAGER_REGION=eu-west-1

sink import aws_secrets_manager production \
  --scope prefix=myapp/prod/ \
  --dry-run

Leave the environment variables out and the CLI prompts for each field instead. The prefix does two jobs: it filters the ListSecrets call, so nothing outside it is ever read, and it is stripped when naming the variable.

Names change, and you see how

myapp/prod/db-password is not a legal environment variable name, so the import rewrites it. The whole remaining path is used, not just the last segment — that is what stops prod/password and staging/password from quietly becoming the same key:

myapp/prod/db-password   →  DB_PASSWORD        (prefix myapp/prod/)
myapp/prod/db-password   →  MYAPP_PROD_DB_PASSWORD   (no prefix)

A secret whose value is a flat JSON object — the shape an RDS-managed credential has — is split into one variable per field, each qualified by the secret's own name so two secrets cannot both claim PASSWORD:

myapp/prod/db  {"username": "app", "password": "…", "port": 5432}

  →  DB_USERNAME, DB_PASSWORD, DB_PORT

Pass --no-expand-json to keep the JSON whole instead. Either way every rename is printed before anything is written, and two names that collide are reported and skipped rather than silently overwriting each other. Binary secrets are skipped: they are not environment variables.

Saving the credential

A one-off migration should just use the environment variables above and be done. If you are going to keep importing — a staging environment refreshed each sprint, say — a workspace admin can store the credential instead:

sink connections add aws_secrets_manager "AWS production" \
  --scope prefix=myapp/prod/

sink import --connection "AWS production" production

The credential is checked against AWS before it is saved, encrypted at rest with the same envelope scheme as your secrets, and never returned by any endpoint — it is decrypted server-side for one import and nowhere else. Revoke it from Settings › Connections, or with sink connections rm. Prefer a narrow, read-only IAM policy for anything you save.

Afterwards

  • Re-run with --dry-run to see whether AWS has drifted from Sink.
  • Rotate in AWS first, then re-import, then let everyone re-pull.
  • An import of more than 500 secrets is refused — narrow the prefix and do it in batches.
  • Delete the migration IAM user once you are finished with it.
Try it on your own project

Sink keeps every environment under AES-256-GCM envelope encryption, with roles, versions and an audit trail — and puts it back in your .env with one command. The free tier does not ask for a card.