# Sync to AWS

The AWS destinations are usually chosen for a different reason than the others.
You often *can* decrypt on your own side in AWS — [`seekrit run`](/docs/guides/run)
in an ECS container, [an SDK](/docs/guides/sdks) in a Lambda — and where you can,
you should. Sync to Secrets Manager or Parameter Store when something else
already reads from them: an ECS task definition's `secrets:` block, a Terraform
data source, a CloudFormation `{{resolve:ssm-secure:…}}`, or a team convention
you are not going to change.

| | Secrets Manager | Parameter Store |
| --- | --- | --- |
| **What seekrit writes** | One secret per name, or all of them as one JSON secret | SSM parameters directly under one path, `SecureString` by default |
| **Addressed by** | An optional name prefix, or the one bundle secret's name | A `/`-delimited path |
| **IAM actions** | `PutSecretValue`, `CreateSecret`, `DeleteSecret`, `RestoreSecret`, `ListSecrets` | `PutParameter`, `DeleteParameters`, `GetParametersByPath` |
| **Takes effect** | Next read by your app | Next read by your app |
| **Deletion** | AWS's scheduled deletion, 30-day recovery window | Immediate |

New to sync? Read [Third-party sync](/docs/guides/third-party-sync) first — the
decryption grant, name mapping, deletions, and failure handling are the same on
every destination.

## The connection: a region and one IAM key

Both AWS destinations share one connection shape: a **region**, an IAM **access
key ID**, and the **secret access key** that pairs with it.

Only the secret access key is treated as a credential. An access key ID is an
identifier — it appears in CloudTrail, in the IAM console, and in the
`Authorization` header of every signed request — so seekrit stores it in the
clear, where the dashboard can show you which key a connection is using. That is
the first thing you want to know when a connection starts failing after a key
rotation. The secret half is encrypted in your browser, like every other
destination credential.

> **Note:** Use a long-lived key belonging to an IAM user created for this. Temporary `ASIA…` credentials from STS expire within hours, and a sync connection has to keep working unattended.

## 1. Create an IAM user with one policy

Give it only what its destination needs. For Secrets Manager, scoped to the
prefix the binding writes under:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:PutSecretValue",
        "secretsmanager:CreateSecret",
        "secretsmanager:DeleteSecret",
        "secretsmanager:RestoreSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/storefront/*"
    },
    { "Effect": "Allow", "Action": "secretsmanager:ListSecrets", "Resource": "*" }
  ]
}
```

`ListSecrets` cannot be resource-scoped and is only used by **Test connection**;
drop it if you would rather find out at the first push. A customer-managed
`--kms-key-id` additionally needs `kms:GenerateDataKey` and `kms:Decrypt` on that
key.

For Parameter Store, scoped to the path:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ssm:PutParameter", "ssm:DeleteParameters", "ssm:GetParametersByPath"],
      "Resource": "arn:aws:ssm:us-east-1:111122223333:parameter/prod/storefront/*"
    },
    { "Effect": "Allow", "Action": "kms:Encrypt", "Resource": "arn:aws:kms:…:key/…" }
  ]
}
```

`SecureString` parameters are encrypted with KMS, so the key needs `kms:Encrypt`
— the account's `aws/ssm` default key, or whichever one the binding names.

## 2. Add the connection

```bash
printf '%s' "$AWS_SECRET_ACCESS_KEY" | seekrit sync connect \
  --name acme-aws --provider aws-secrets-manager \
  --region us-east-1 --access-key-id AKIAIOSFODNN7EXAMPLE
```

One connection covers one region. Syncing the same environment into two regions
means two connections — which also keeps their key grants separate.

There is no account id to supply: every endpoint is reached through the regional
host and authorizes off the signature, so the account is whichever one the key
belongs to.

## 3. Bind an environment

### Secrets Manager

Choose how the secrets are laid out. The default writes one AWS secret per
seekrit secret, under an optional prefix:

```bash
seekrit sync verify acme-aws --provider aws-secrets-manager --path prod/storefront/

seekrit sync enable --connection acme-aws \
  --provider aws-secrets-manager --path prod/storefront/ \
  --app storefront --env production --acknowledge-decryption
```

The alternative packs every value into **one** secret as a JSON object — the
shape an ECS task definition or Lambda reads with `secret-arn:json-key::`:

```bash
seekrit sync enable --connection acme-aws \
  --provider aws-secrets-manager --layout json-bundle \
  --secret-name prod/storefront/env \
  --app storefront --env production --acknowledge-decryption
```

Prefer the bundle when your runtime reads secrets as a group — Secrets Manager
bills per secret per month, so fifty names cost fifty times as much stored
separately. Prefer one-per-name when consumers read them individually or you
want per-secret IAM.

> **Note:** seekrit rewrites the bundle **whole** on every run, so it mirrors exactly what the binding resolves: a removed secret disappears from it, and so would a key you added by hand. `onDelete: retain` cannot apply to a single JSON value — if you need removals left in place, use the one-secret-per-name layout.

### Parameter Store

Name the path every parameter lands directly under, so an application can read
the whole environment with one `GetParametersByPath`:

```bash
seekrit sync enable --connection acme-ssm \
  --provider aws-parameter-store --path /prod/storefront/ \
  --app storefront --env production --acknowledge-decryption
```

The path needs a leading **and** trailing slash, so the binding's names
concatenate unambiguously — `/prod/storefront/` + `DB_URL`. AWS reserves `aws`
and `ssm` as a first segment.

Values are written as `SecureString` unless you pass `--param-type String`, and
on the free `Standard` tier unless you pass `--tier` — `Standard` caps a value
at 4KB, `Advanced` at 8KB and bills per parameter per month, and
`Intelligent-Tiering` lets AWS upgrade only the parameters that need it.

## How a push behaves

- **Neither connector lists before writing.** Secrets Manager has no bulk write,
  so a listing would add requests without removing any; `PutParameter` with
  `Overwrite` is already an upsert. Each secret is written optimistically and
  only falls back to `CreateSecret` on `ResourceNotFoundException` — one request
  per secret in the steady state, two the first time.
- **Parameter Store batches deletions** ten at a time (`DeleteParameters`), the
  one place this API is kinder than Secrets Manager's.
- **Secrets seekrit creates carry a description** marking them as managed, so
  the console says who owns them. A secret that already existed keeps its own.
- **A run is capped at 400 requests**, and reports more work rather than
  silently syncing a subset.
- **A credential or permission failure stops the run once.** `AccessDenied`,
  `UnrecognizedClientException`, `InvalidSignatureException`, `ExpiredToken` and
  their kin are facts about the connection, not about a name — repeating one
  beside fifty secrets would bury it.

> **Note:** **Deletion on Secrets Manager is scheduled, not immediate.** seekrit takes AWS's 30-day default recovery window, so a mistaken removal is recoverable in the console. The consequence is that the name stays **reserved** for those 30 days and `CreateSecret` on it fails — so a secret that is deleted and then comes back (renamed away and back, an `exclude` glob edited twice) is *restored* rather than recreated.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `AccessDeniedException`, whole run stopped | The IAM policy is missing an action, or its `Resource` doesn't cover the prefix or path the binding writes under | Compare the policy against the actions above. AWS reports this per request, so the run stops and says so once |
| `UnrecognizedClientException` or `InvalidSignatureException` | The key pair is wrong or was deactivated | Rotating an AWS key changes the **id** as well as the secret, so a rotation means a new connection, not just a new credential |
| `ExpiredTokenException` | Temporary STS credentials were used | Use a long-lived IAM user key — sync runs unattended |
| `ThrottlingException`, run reported partial | AWS rate-limited the account | Nothing to do — seekrit backs off and retries the rest |
| A new secret fails with `InvalidRequestException` about a scheduled deletion | The name is inside its 30-day recovery window | seekrit restores it automatically on the next run; force one with `seekrit sync run` |
| `ParameterLimitExceeded` or a value rejected as too long | `Standard` tier caps a value at 4KB | Pass `--tier Advanced` (billed) or `Intelligent-Tiering` |
| Bundle secret missing keys you added by hand | The bundle is rewritten whole every run | Use `--layout secret-per-name`, or keep hand-managed values in a different secret |

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [AWS KMS drop-in](/docs/guides/kms-aws) — the other direction: KMS-compatible envelope crypto against seekrit
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
