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

# Deploy on AWS ECS

> Run Keyring on Fargate with an ECS task role, private tasks, and Composio-only ingress.

export const useAwsEcsValues = () => {
  const [values, setValues] = useState(awsEcsValuesStore.values);
  useEffect(() => {
    awsEcsValuesStore.listeners.push(setValues);
    return () => {
      awsEcsValuesStore.listeners = awsEcsValuesStore.listeners.filter(listener => listener !== setValues);
    };
  }, []);
  return values;
};

export const updateAwsEcsValue = (name, value, setValues) => {
  setValues(current => {
    const next = {
      ...current,
      [name]: value
    };
    awsEcsValuesStore.values = next;
    for (const listener of awsEcsValuesStore.listeners) {
      listener(next);
    }
    return next;
  });
};

export const renderAwsKeyCommand = ({filename, description, keySpec, purpose, outputLabel, sampleArn}) => {
  const values = useAwsEcsValues();
  const command = `aws kms create-key \\
  --region "${values.region}" \\
  --description "${description}" \\
  --key-spec ${keySpec} \\
  --key-usage ENCRYPT_DECRYPT \\
  --tags \\
    TagKey=ComposioKeyring,TagValue=true \\
    TagKey=Environment,TagValue=production \\
    TagKey=KeyringPurpose,TagValue=${purpose} \\
  --query '{${outputLabel}: KeyMetadata.Arn}' \\
  --output json`;
  const sampleOutput = `{
  "${outputLabel}": "${sampleArn}"
}`;
  return <div className="space-y-3">
      <CodeBlock language="bash" filename={filename} wrap>
        {command}
      </CodeBlock>
      <CodeBlock language="json" filename="Example output" wrap>
        {sampleOutput}
      </CodeBlock>
    </div>;
};

export const renderAwsInputs = ({fields, title, description}) => {
  const [values, setValues] = useState(awsEcsValuesStore.values);
  const inputClassName = "mt-1 w-full rounded-lg border border-zinc-950/15 px-3 py-2 font-mono text-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-white/15";
  useEffect(() => {
    awsEcsValuesStore.listeners.push(setValues);
    return () => {
      awsEcsValuesStore.listeners = awsEcsValuesStore.listeners.filter(listener => listener !== setValues);
    };
  }, []);
  return <div className="aws-ecs-inputs not-prose my-6 rounded-xl border border-zinc-950/10 bg-zinc-950/[0.025] p-4 dark:border-white/10 dark:bg-white/[0.035]">
      <div className="mb-4">
        <p className="m-0 text-sm font-semibold text-zinc-950 dark:text-white">{title}</p>
        <p className="mb-0 mt-1 text-sm text-zinc-600 dark:text-zinc-400">{description}</p>
      </div>

      <div className="grid gap-4 sm:grid-cols-2">
        {fields.map(field => <label className="block text-sm font-medium text-zinc-800 dark:text-zinc-200" key={field.name}>
            {field.label}
            <input type="text" value={values[field.name]} onChange={event => updateAwsEcsValue(field.name, event.target.value, setValues)} className={inputClassName} placeholder={field.placeholder} autoComplete="off" spellCheck={false} />
          </label>)}
      </div>
    </div>;
};

export const displayValue = (value, placeholder) => value || placeholder;

export const awsEcsValuesStore = {
  values: {
    region: "us-east-1",
    organizationId: "",
    credentialKeyArn: "",
    gateKeyArn: "",
    transferKeyArn: ""
  },
  listeners: []
};

export const AwsTransferKeyCommand = () => renderAwsKeyCommand({
  filename: "Create the secret-transfer key",
  description: "Composio Keyring secret-transfer key",
  keySpec: "RSA_3072",
  purpose: "composio-keyring-secret-transfer",
  outputLabel: "SecretTransferKeyArn",
  sampleArn: "arn:aws:kms:us-east-1:111122223333:key/3456cdef-34cd-56ef-78ab-3456789012cd"
});

export const AwsTaskDefinition = () => {
  const values = useAwsEcsValues();
  const organizationId = displayValue(values.organizationId, "YOUR_COMPOSIO_ORG_ID");
  const definition = `{
  "family": "composio-keyring",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "taskRoleArn": "composio-keyring-task",
  "cpu": "1024",
  "memory": "2048",
  "containerDefinitions": [
    {
      "name": "keyring",
      "image": "composiohq/keyring:alpha",
      "essential": true,
      "portMappings": [{ "containerPort": 7464, "protocol": "tcp" }],
      "environment": [
        { "name": "APP_ENV", "value": "production" },
        { "name": "RUNTIME", "value": "node" },
        { "name": "HOST", "value": "0.0.0.0" },
        { "name": "PORT", "value": "7464" },
        {
          "name": "AUTH_JWKS_URL",
          "value": "https://backend.composio.dev/.well-known/jwks.json"
        },
        { "name": "AUTH_ISSUER", "value": "https://backend.composio.dev" },
        { "name": "AUTH_AUDIENCE", "value": "${organizationId}" },
        { "name": "AUTH_JWT_ALGORITHMS", "value": "RS256" },
        { "name": "AUDIT_DURABILITY", "value": "required" },
        { "name": "OTEL_COLLECTOR_URL", "value": "http://localhost:4318" },
        { "name": "LOG_LEVEL", "value": "info" }
      ],
      "secrets": [
        {
          "name": "ENCRYPTION_CONFIG",
          "valueFrom": "keyring/production/encryption-config"
        }
      ],
      "healthCheck": {
        "command": ["CMD", "/usr/local/bin/node", "/app/healthcheck.mjs"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 15
      }
    }
  ]
}`;
  return <CodeBlock language="json" filename="keyring-task-definition.json" wrap>
      {definition}
    </CodeBlock>;
};

export const AwsTaskIngressCommand = () => {
  const values = useAwsEcsValues();
  const command = `aws ec2 authorize-security-group-ingress \\
  --region "${values.region}" \\
  --group-id "<TASK_SECURITY_GROUP_ID>" \\
  --protocol tcp --port 7464 \\
  --source-group "<ALB_SECURITY_GROUP_ID>"`;
  return <CodeBlock language="bash" filename="Allow only the ALB to reach Keyring" wrap>
      {command}
    </CodeBlock>;
};

export const AwsSecretManagerCommand = () => {
  const values = useAwsEcsValues();
  const command = `aws secretsmanager create-secret \\
  --region "${values.region}" \\
  --name "keyring/production/encryption-config" \\
  --secret-string file://config.json \\
  --tags \\
    Key=ComposioKeyring,Value=true \\
    Key=Environment,Value=production`;
  return <CodeBlock language="bash" filename="Store the encryption configuration" wrap>
      {command}
    </CodeBlock>;
};

export const AwsKmsPolicy = () => {
  const values = useAwsEcsValues();
  const credentialKeyArn = displayValue(values.credentialKeyArn, "CREDENTIAL_KEY_ARN");
  const gateKeyArn = displayValue(values.gateKeyArn, "GATE_KEY_ARN");
  const transferKeyArn = displayValue(values.transferKeyArn, "TRANSFER_KEY_ARN");
  const policy = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["kms:Encrypt", "kms:Decrypt"],
      "Resource": [
        "${credentialKeyArn}",
        "${gateKeyArn}"
      ],
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/ComposioKeyring": "true",
          "aws:ResourceTag/Environment": "production",
          "aws:ResourceTag/KeyringPurpose": [
            "composio-keyring-credential",
            "composio-keyring-authorization-gate"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": ["kms:GetPublicKey", "kms:Decrypt"],
      "Resource": "${transferKeyArn}",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/ComposioKeyring": "true",
          "aws:ResourceTag/Environment": "production",
          "aws:ResourceTag/KeyringPurpose": "composio-keyring-secret-transfer"
        }
      }
    }
  ]
}`;
  return <CodeBlock language="json" filename="keyring-kms-policy.json" wrap>
      {policy}
    </CodeBlock>;
};

export const AwsKeyArnInputs = () => renderAwsInputs({
  title: "Paste the three generated key ARNs",
  description: "The IAM policy and encryption configuration below update automatically.",
  fields: [{
    label: "Credential root key ARN",
    name: "credentialKeyArn",
    placeholder: "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
  }, {
    label: "Authorization-gate key ARN",
    name: "gateKeyArn",
    placeholder: "arn:aws:kms:us-east-1:111122223333:key/2345bcde-23bc-45de-67fa-2345678901bc"
  }, {
    label: "Secret-transfer key ARN",
    name: "transferKeyArn",
    placeholder: "arn:aws:kms:us-east-1:111122223333:key/3456cdef-34cd-56ef-78ab-3456789012cd"
  }]
});

export const AwsGateKeyCommand = () => renderAwsKeyCommand({
  filename: "Create the authorization-gate key",
  description: "Composio Keyring authorization-gate root",
  keySpec: "SYMMETRIC_DEFAULT",
  purpose: "composio-keyring-authorization-gate",
  outputLabel: "AuthorizationGateKeyArn",
  sampleArn: "arn:aws:kms:us-east-1:111122223333:key/2345bcde-23bc-45de-67fa-2345678901bc"
});

export const AwsEncryptionConfig = () => {
  const values = useAwsEcsValues();
  const credentialKeyArn = displayValue(values.credentialKeyArn, "CREDENTIAL_KEY_ARN");
  const gateKeyArn = displayValue(values.gateKeyArn, "GATE_KEY_ARN");
  const transferKeyArn = displayValue(values.transferKeyArn, "TRANSFER_KEY_ARN");
  const configuration = JSON.stringify({
    credential: {
      active_adapter_id: "aws-credential",
      active_key_id: credentialKeyArn,
      adapters: {
        "aws-credential": {
          type: "aws",
          region: values.region,
          credential_source: "runtime_environment",
          allowed_key_ids: [credentialKeyArn]
        }
      }
    },
    authorization_gate: {
      active_adapter_id: "aws-gate",
      active_key_id: gateKeyArn,
      adapters: {
        "aws-gate": {
          type: "aws",
          region: values.region,
          credential_source: "runtime_environment",
          allowed_key_ids: [gateKeyArn]
        }
      }
    },
    secret_transfer: {
      active_kid: "transfer-2026-01",
      adapters: {
        "aws-transfer": {
          type: "aws",
          region: values.region,
          credential_source: "runtime_environment",
          keys: {
            "transfer-2026-01": transferKeyArn
          }
        }
      }
    },
    dek_cache: {
      capacity: 1024,
      ttl_seconds: 300
    }
  }, null, 2);
  return <CodeBlock language="json" filename="config.json" wrap>
      {configuration}
    </CodeBlock>;
};

export const AwsEcsDeploymentInputs = () => renderAwsInputs({
  title: "Fill in your deployment values",
  description: "Every later command and configuration block updates as you type. No shell variables are required.",
  fields: [{
    label: "AWS region",
    name: "region",
    placeholder: "us-east-1"
  }, {
    label: "Composio organization ID",
    name: "organizationId",
    placeholder: "ok_example123456"
  }]
});

export const AwsCredentialKeyCommand = () => renderAwsKeyCommand({
  filename: "Create the credential root key",
  description: "Composio Keyring credential root",
  keySpec: "SYMMETRIC_DEFAULT",
  purpose: "composio-keyring-credential",
  outputLabel: "CredentialRootKeyArn",
  sampleArn: "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
});

export const AwsAlbIngressCommands = () => {
  const values = useAwsEcsValues();
  const commands = `aws ec2 authorize-security-group-ingress \\
  --region "${values.region}" \\
  --group-id "<ALB_SECURITY_GROUP_ID>" \\
  --protocol tcp --port 443 --cidr "34.233.50.61/32"

aws ec2 authorize-security-group-ingress \\
  --region "${values.region}" \\
  --group-id "<ALB_SECURITY_GROUP_ID>" \\
  --protocol tcp --port 443 --cidr "54.224.131.195/32"

aws ec2 authorize-security-group-ingress \\
  --region "${values.region}" \\
  --group-id "<ALB_SECURITY_GROUP_ID>" \\
  --protocol tcp --port 443 --cidr "54.243.138.89/32"

aws ec2 authorize-security-group-ingress \\
  --region "${values.region}" \\
  --group-id "<ALB_SECURITY_GROUP_ID>" \\
  --protocol tcp --port 443 --cidr "52.72.72.59/32"`;
  return <CodeBlock language="bash" filename="Allow Composio to reach the ALB" wrap>
      {commands}
    </CodeBlock>;
};

AWS ECS on Fargate is the preferred AWS deployment. Keyring receives short-lived AWS credentials from
the ECS task role and uses them only with the KMS keys listed in its configuration.

`us-east-1` is prefilled because most Composio infrastructure runs there. Keeping Keyring nearby can
reduce request latency, but you can replace it with the AWS region that fits your requirements.

Open [Project Settings → General](https://dashboard.composio.dev/~/project/settings/general#:~:text=%40org_id)
to find your Composio organization ID.

<AwsEcsDeploymentInputs />

## 1. Create the KMS keys

Keyring uses three separate keys. Each key has one job, and Keyring rejects configurations that reuse a
key for another purpose.

| Key                | Type      | ECS task-role access              |
| ------------------ | --------- | --------------------------------- |
| Credential root    | Symmetric | `kms:Encrypt`, `kms:Decrypt`      |
| Authorization gate | Symmetric | `kms:Encrypt`, `kms:Decrypt`      |
| Secret transfer    | RSA 3072  | `kms:GetPublicKey`, `kms:Decrypt` |

Each command labels its output and adds a `KeyringPurpose` tag. The output is easier to tell apart
while you deploy, and the tag identifies each key when you return to AWS later.

### Create the credential root key

This key wraps the project data keys that protect credentials for custom auth configs and sensitive
fields returned during token exchange.

<AwsCredentialKeyCommand />

Your key ARN will be different from the example. Copy the ARN value without the quotation marks and
paste it into the **Credential root key ARN** field below.

### Create the authorization-gate key

This independent key ensures protected credentials must pass through your Keyring before use, making
the stored credential unusable without your authorization path.

<AwsGateKeyCommand />

Copy the ARN value without the quotation marks and paste it into the **Authorization-gate key ARN**
field below.

### Create the secret-transfer key

This RSA key protects new credentials before they reach Composio. The Dashboard seals a credential
with the public key, while the private key stays in your KMS.

<AwsTransferKeyCommand />

Copy the ARN value without the quotation marks and paste it into the **Secret-transfer key ARN** field
below.

<AwsKeyArnInputs />

<Tip>
  The generated IAM policy and encryption configuration now use the key ARNs you pasted. You do not
  need to replace them manually in later code blocks.
</Tip>

## 2. Create the ECS task role

The ECS task role gives Keyring access to only the three keys above. It is separate from the task
execution role that ECS uses to pull the image, read task-definition secrets, and publish logs.

Save this generated policy as `keyring-kms-policy.json`. It requires both exact key ARNs and the
expected Keyring tags.

<AwsKmsPolicy />

Do not grant this role key administration or tag-management permissions. When IAM access depends on
tags, changing a tag can also change access.

Save this ECS trust policy as `ecs-task-trust.json`:

```json ecs-task-trust.json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

Create the task role using the trust policy:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
aws iam create-role \
  --role-name composio-keyring-task \
  --assume-role-policy-document file://ecs-task-trust.json
```

Attach the generated KMS policy to that role:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
aws iam put-role-policy \
  --role-name composio-keyring-task \
  --policy-name composio-keyring-kms \
  --policy-document file://keyring-kms-policy.json
```

The KMS key policies must also enable IAM policies for your account or explicitly allow this task role.
Keyring does not need `kms:GenerateDataKey`; it generates data keys locally and wraps them with
`kms:Encrypt`.

## 3. Store the encryption configuration

Save this generated configuration as `config.json`. It references the three exact key ARNs and tells
Keyring to obtain short-lived credentials from the ECS task role.

<AwsEncryptionConfig />

Create a Secrets Manager secret containing that file:

<AwsSecretManagerCommand />

The standard image already contains the reviewed Composio origin policy. Do not add `origin_policy`
unless Composio gives you a custom reviewed policy URL.

## 4. Register the Fargate task

The generated task definition includes your organization ID directly as `AUTH_AUDIENCE`. The task
role uses its short name, so the file does not need your AWS account ID.

<AwsTaskDefinition />

Add your OpenTelemetry Collector sidecar to the complete task definition, then register it. The
command asks AWS IAM for the execution-role ARN inline:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
aws ecs register-task-definition \
  --execution-role-arn "$(aws iam get-role --role-name composio-keyring-execution --query Role.Arn --output text)" \
  --cli-input-json file://keyring-task-definition.json
```

Follow the [telemetry delivery guide](/deployment/observability#recommended-open-telemetry-collector)
for the collector configuration. It should listen on `127.0.0.1:4318`. Containers in one Fargate task
share a network namespace, so Keyring can receive a local audit acknowledgment while the collector
handles delivery to your observability backend.

Mark the collector essential and start it before Keyring. Store any collector credential in Secrets
Manager rather than the task definition.

## 5. Put the tasks behind an ALB

Run the Fargate tasks in private subnets without public IP addresses. Use an Application Load Balancer
target group with target type `ip` and health path `/healthz`.

After creating the ALB, target group, listener, and security groups, create the ECS service:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
aws ecs create-service \
  --cluster <ECS_CLUSTER> \
  --service-name composio-keyring \
  --task-definition composio-keyring \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[<PRIVATE_SUBNET_1>,<PRIVATE_SUBNET_2>],securityGroups=[<TASK_SECURITY_GROUP>],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=<TARGET_GROUP_ARN>,containerName=keyring,containerPort=7464"
```

Configure the two security groups:

| Security group | Inbound rules                                                                         | Outbound rules                                   |
| -------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ |
| ALB            | TCP 443 from the four [Composio `/32` addresses](/deployment/overview#network-access) | TCP 7464 to the task security group              |
| Task           | TCP 7464 from the ALB security group only                                             | HTTPS to Composio JWKS, KMS, providers, and OTLP |

Add one explicit ALB rule for each Composio production egress address. Replace only the ALB security
group placeholder:

<AwsAlbIngressCommands />

<Warning>
  These addresses are for Keyring requests from the Composio backend. They are not Composio trigger
  or webhook egress addresses. Confirm the current Keyring list with Composio before changing a
  production firewall.
</Warning>

Allow the ALB security group to reach port 7464 on the Keyring tasks. Do not expose the task security
group directly to an IP range:

<AwsTaskIngressCommand />

Terminate TLS at the ALB with an ACM certificate.

## 6. Verify and connect

Check the public path after DNS and TLS are ready:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail https://keyring.example.com/healthz
curl --fail https://keyring.example.com/transfer-keys
```

Then [connect the deployment to Composio](/deployment/overview#connect-keyring-to-composio). Confirm
that Keyring initializes with the ECS task identity and can deliver audit events.

Start with 1 vCPU and 2 GiB per task, then load test with your expected provider latency and payload
mix. Keyring is stateless, so you can scale the ECS service horizontally.

Keep ECS Exec disabled unless an approved incident workflow requires it. Never print task environment
variables during diagnosis.
