> ## Documentation Index
> Fetch the complete documentation index at: https://www.ravion.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Managing secrets

> Store application secrets in one AWS Secrets Manager JSON secret per environment, then reference individual keys as runtime variables in your modules.

<Note>
  **Native secret management is coming soon.** Ravion will let you create, edit, and share secrets
  directly from the dashboard, the CLI, and `ravion.yaml`. Until then, you own the secret values in
  your AWS account and Ravion references them — which is what this guide covers.
</Note>

Ravion never stores your secret values. Terraform and your deployments run inside your own AWS account, so a secret's value only ever moves from AWS Secrets Manager (or SSM Parameter Store) into your running task. Your Ravion config holds **references** — ARNs — not values, which makes `ravion.yaml` safe to commit.

The pattern we recommend, and the one we use for Ravion itself:

**One Secrets Manager secret per environment, holding a JSON object of many keys.** Each module then references individual keys out of that one secret.

## Why one secret with many keys

The obvious alternative — one Secrets Manager secret per environment variable — works, but it scales badly:

* **Cost.** Secrets Manager bills per secret, per month. Forty secrets cost forty times as much as one secret with forty keys.
* **One place to edit.** Adding a variable is a JSON key edit, not a new AWS resource, a new ARN, and a new config entry pointing at it.
* **One ARN to remember.** Every reference in `ravion.yaml` shares the same ARN prefix and differs only by the key name, so the config stays readable and diffs stay obvious.
* **One audit trail.** Secrets Manager versions the whole JSON document, so every change to any variable is a single version you can inspect or roll back.

This applies to the secrets you create. Leave secrets that a module or AWS creates for you — database master passwords, cache connection strings — where they are, and reference them by their own ARN. Those rotate in place without touching your environment secret. Each module's catalog page documents the secret it creates, and you'll find its ARN in the module's stack outputs.

## Step 1: Create the environment secret

Create one secret per environment, named for the environment and the app. We use `prod/ravion`.

```bash theme={null}
aws secretsmanager create-secret \
  --name prod/myapp \
  --description "Runtime secrets for myapp production" \
  --secret-string '{
    "DATABASE_URL": "postgres://...",
    "OPENAI_API_KEY": "sk-...",
    "SESSION_SECRET": "..."
  }'
```

Create the secret in the **same AWS account and region** as the modules that read it. The generated execution role policy is scoped to the current account, so cross-account secret references don't work (see [Permissions](#permissions)).

<Tip>
  Keep the environment in the name even when each environment lives in its own AWS account. The ARN
  then says which environment it belongs to on its own, which matters when you're reading it in a
  config diff, a task definition, or an IAM policy.
</Tip>

Then grab the full ARN, which includes the six-character suffix AWS appends:

```bash theme={null}
aws secretsmanager describe-secret --secret-id prod/myapp --query ARN --output text
# arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/myapp-AbCdEf
```

## Step 2: Reference keys from your modules

ECS modules (`rvn-ecs-web`, `rvn-ecs-worker`) take a **Runtime secrets** (`secrets`) input: a list of \{name, valueFrom} objects, where `name` is the environment variable inside your container and `valueFrom` is an ARN. Append the JSON key to the ARN to pull a single key out of the document:

```yaml ravion.yaml highlight={9-14} theme={null}
- givenId: api
  name: API
  type: rvn-ecs-web
  version: 0.8.4
  input:
    environment_variables:
      - name: NODE_ENV
        value: production
    secrets:
      - name: DATABASE_URL
        valueFrom: "arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/myapp-AbCdEf:DATABASE_URL::"
      - name: OPENAI_API_KEY
        valueFrom: "arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/myapp-AbCdEf:OPENAI_API_KEY::"
```

Use `environment_variables` for anything non-sensitive and `secrets` for everything else. Secret values never appear in the Ravion dashboard, in your config file, or in build logs.

### Anatomy of the ARN

```text theme={null}
arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/myapp-AbCdEf:DATABASE_URL::
└──────────────── secret ARN ──────────────────────────────────────┘ └── key ──┘└┘└┘
                                                                                 │  │
                                                              version stage ─────┘  │
                                                                 version id ────────┘
```

Three things trip people up:

* **The trailing `::` is required** when you specify a JSON key. The empty version stage and version ID mean "whatever is current" (`AWSCURRENT`), which is what you almost always want. Omit them and ECS rejects the ARN.
* **The `-AbCdEf` suffix is part of the ARN.** It's generated by AWS, so read the ARN with `describe-secret` rather than assembling it by hand.
* **Quote the value in YAML.** An unquoted string ending in `::` is fine in most parsers, but quoting avoids surprises.

Leave the key off entirely — `...:secret:prod/myapp-AbCdEf` — and ECS injects the **entire secret string** into the variable, JSON braces and all. That's only what you want when the secret holds a single plain value, which is typical of the secrets a module creates for you.

## Step 3: Apply and deploy

```bash theme={null}
ravion project config apply <project-id> --file ravion.yaml --dry-run
ravion project config apply <project-id> --file ravion.yaml
```

Adding or removing a `secrets` entry changes the task definition, so it takes effect on the next deployment of that module.

## Rotating and adding values

Adding a key is a change to the JSON document, not to your Ravion config — except for the one line that maps it to an environment variable.

<Steps>
  <Step title="Update the JSON document">
    `put-secret-value` **replaces the whole document**, so merge rather than overwrite:

    ```bash theme={null}
    aws secretsmanager get-secret-value --secret-id prod/myapp --query SecretString --output text \
      | jq '.NEW_API_KEY = "value"' \
      | aws secretsmanager put-secret-value --secret-id prod/myapp --secret-string file:///dev/stdin
    ```

    Editing the secret in the AWS console does the same merge for you.
  </Step>

  <Step title="Map it to an environment variable">
    Add an entry to `secrets` in `ravion.yaml` and apply the config. Skip this step when you're
    rotating a value that's already mapped.
  </Step>

  <Step title="Redeploy to pick up new values">
    ECS resolves secrets **when a task starts**, so a rotated value doesn't reach running tasks.
    Deploy the module — or otherwise force new tasks — to roll the new value out.
  </Step>
</Steps>

<Warning>
  Rotating a secret in place is not zero-risk: during the rollout, old tasks hold the old value and
  new tasks hold the new one. For credentials that can't tolerate that overlap, accept both values
  during the rollout, or roll dependent services in a deliberate order.
</Warning>

## Secrets during builds

Runtime secrets are injected into the running container, not into the build. When a build needs a credential — a private package registry token, for example — use **Build environment variables** (`build_environment_variables`), which accept a reference instead of a literal value:

```yaml ravion.yaml theme={null}
build_environment_variables:
  NODE_ENV: production
  NPM_TOKEN:
    fromSecretManager: "arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/myapp-AbCdEf:NPM_TOKEN::"
  SENTRY_DSN:
    fromParameterStore: "/myapp/prod/sentry-dsn"
```

For Dockerfile builds, set `dockerfile_inject_env_variables: true` to pass these through as Docker build arguments, and declare a matching `ARG` in your Dockerfile. Build arguments are visible in image history, so prefer runtime secrets for anything the build doesn't strictly need.

## Permissions

When Ravion creates the ECS execution role for a service, it attaches a policy that allows reading Secrets Manager secrets and SSM parameters and decrypting them with KMS, conditioned on the resource living in the **same AWS account** as the service. So the common case needs no IAM work from you.

Three cases do need attention:

* **Cross-account secrets** don't work with the generated policy. Replicate the secret into the workload account instead.
* **Customer-managed KMS keys** need the key's own resource policy to allow `kms:Decrypt` for the execution role. The identity-side permission is already there; the key policy isn't.
* **A supplied execution role** — when you set `execution_role_arn` yourself — bypasses the generated policy entirely. Attach the equivalent permissions to your role.

## SSM Parameter Store as an alternative

Anywhere a Secrets Manager ARN works, an SSM `SecureString` parameter ARN works too:

```yaml theme={null}
secrets:
  - name: STRIPE_SECRET_KEY
    valueFrom: "arn:aws:ssm:us-east-2:111122223333:parameter/myapp/prod/stripe-secret-key"
```

Parameter Store has no per-parameter charge for standard parameters, which makes one-parameter-per-value affordable. The tradeoff is that each value is its own resource with its own ARN, and there's no JSON key selection — so you lose the single-document editing and versioning that makes the one-secret pattern pleasant. Use Parameter Store for a handful of values, and the one-secret pattern once you have more than a few.

## Practices worth keeping

* **Never let environments share a secret.** A staging secret should never be reachable from production, or vice versa — separate secrets, ideally in separate AWS accounts.
* **Split a secret only for access control.** One secret per environment is the default; add another when a team shouldn't be able to read another team's values.
* **Don't paste secret values into support reports.** `ravion report` and `ravion feedback` send what you type to us — review the message first.

## Troubleshooting

| Symptom                                                                 | Likely cause                                                               | Fix                                                                             |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Tasks fail to start with `ResourceInitializationError` fetching secrets | Wrong ARN, missing suffix, or the key doesn't exist in the JSON            | Recheck the ARN with `describe-secret`; confirm the key with `get-secret-value` |
| ECS rejects the ARN as invalid                                          | JSON key given without the trailing `::`                                   | Use `...:secret:name-AbCdEf:MY_KEY::`                                           |
| The variable contains the whole JSON document                           | No JSON key on the ARN                                                     | Append `:KEY::` to select one key                                               |
| Rotated value isn't visible in the app                                  | ECS resolves secrets at task start                                         | Redeploy the module so new tasks pick up the current version                    |
| `AccessDeniedException` on `GetSecretValue` or `kms:Decrypt`            | Cross-account secret, a supplied execution role, or a customer-managed key | See [Permissions](#permissions)                                                 |
| Tasks in private subnets time out fetching secrets                      | No route to the Secrets Manager endpoint                                   | Ensure NAT egress or a Secrets Manager VPC endpoint in the task subnets         |

## Related pages

<CardGroup cols={2}>
  <Card title="ECS Web Service" icon="server" href="/docs/module-definitions/catalog/rvn-ecs-web">
    Runtime secrets and build environment variables for `rvn-ecs-web`.
  </Card>

  <Card title="ECS Worker" icon="gear" href="/docs/module-definitions/catalog/rvn-ecs-worker">
    The same secrets input for background workers.
  </Card>

  <Card title="Project config file" icon="file-code" href="/docs/config-as-code/project-config-file">
    How `ravion.yaml` is structured and applied.
  </Card>

  <Card title="How Ravion works" icon="shield" href="/docs/how-ravion-works">
    Why secret values stay inside your AWS account.
  </Card>

  <Card title="Builds" icon="hammer" href="/docs/modules/build">
    Build environment variables and Docker build arguments.
  </Card>
</CardGroup>
