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

# AWS S3

export const PublisherBadge = ({size = "md"}) => {
  return <Tooltip tip="Members with the publisher role can execute this action." cta="Workspace roles" href="/admin/users/access-control">
            <Badge icon="git-merge" color="green" size={size}>publisher</Badge>
        </Tooltip>;
};

export const MutableBadge = ({size = "sm"}) => {
  return <Tooltip tip="Property is automatically updated by the system; cannot be modified directly">
            <Badge icon="feather" color="orange" size={size}>mutable</Badge>
        </Tooltip>;
};

export const ImmutableBadge = ({size = "sm"}) => {
  return <Tooltip tip="Property cannot be modified">
            <Badge icon="lock" color="gray" size={size}>immutable</Badge>
        </Tooltip>;
};

An S3 bucket is connected by **assuming a cross-account IAM role** —
there are **no access keys**. You create a role in your own AWS account that grants
write access to your bucket and trusts Miru's integration role to assume it.

## How it works

You create a role in your AWS account that grants write access to the bucket and trusts Miru's AWS role to assume it.

<img src="https://assets.mirurobotics.com/docs/v04/images/data-uploads/aws-sts-assume-role.svg" alt="AWS STS assume-role exchange" />

When a device needs to upload data to you bucket:

1. Miru assumes the role in your AWS account
2. Miru creates a short-lived token scoped to the exact object key the device is uploading to
3. Miru distributes the token to the device
4. The device uploads the data to the bucket using the token

Connecting your bucket to Miru is a one-time setup that uses only identifiers and
configuration values. No access keys, passwords, or long-lived tokens change hands in
either direction.

### Miru provides you

<ParamField path="miru_role_arn">
  The AWS role Miru assumes into your account. You name this role as the trusted
  principal in your bucket role's trust policy. This is a fixed value that never changes.

  `arn:aws:iam::008971655189:role/MiruS3IntegrationRole`
</ParamField>

<ParamField path="external_id">
  The external ID that must be present in your role's trust policy, guarding against
  the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). **Generated by Miru** and shown in the dashboard — you
  fetch it from the UI and copy it into your trust policy.

  Example: `wsp_123`
</ParamField>

### You provide Miru

<ParamField path="bucket_name">
  <ImmutableBadge />

  The name of your S3 bucket to connect to Miru.

  Example: `my-upload-bucket`
</ParamField>

<ParamField path="region">
  <ImmutableBadge />

  The region of the bucket.

  Example: `us-east-1`
</ParamField>

<ParamField path="role_arn">
  <MutableBadge />

  ARN of the IAM role in your account that Miru assumes to broker bucket access. Its
  trust policy must trust Miru's integration role with the external ID.

  Example: `arn:aws:iam::123456789012:role/miru-uploader`
</ParamField>

## Connect a bucket

Follow the steps below to connect a bucket to Miru using the AWS CLI. To connect a
bucket to Miru, you must hold the <PublisherBadge size="sm" /> role.

<Steps>
  <Step title="Get your external ID from Miru">
    In the Miru dashboard, navigate to the [Buckets page](https://app.mirurobotics.com/settings/buckets) and click the **+ Bucket** button in the top right corner.

    Switch to the **S3** tab, copy your workspace's **external ID**, and set it as a
    variable — the trust policy in a later step pins to this value so that only Miru,
    acting on behalf of your workspace, can assume the role.

    <Frame>
      ![Add Bucket Dialog with AWS External ID](https://assets.mirurobotics.com/docs/v04/images/buckets/add-bucket-aws-ext-id.png)
    </Frame>

    ```bash theme={null}
    EXTERNAL_ID="<external-id>"
    ```
  </Step>

  <Step title="Define the bucket">
    Set your bucket's name and region.

    ```bash theme={null}
    BUCKET_NAME="<bucket-name>"
    ```

    ```bash theme={null}
    REGION="<region>"
    ```

    Create the bucket if it doesn't yet exist.

    ```bash theme={null}
    aws s3api create-bucket \
      --bucket "${BUCKET_NAME}" \
      --region "${REGION}" \
      --create-bucket-configuration LocationConstraint="${REGION}"
    ```

    In `us-east-1`, omit `--create-bucket-configuration` — the API rejects a location
    constraint there.
  </Step>

  <Step title="Create a cross-account IAM role">
    Create an IAM role whose **trust policy** allows **Miru's integration role** as the
    principal and requires your external ID via an `sts:ExternalId` condition.

    The external ID is the guard against the [confused deputy](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) problem: another tenant of the same Miru service cannot trick Miru into assuming your role without presenting your specific external ID.

    ```bash theme={null}
    INTERNAL_ROLE_NAME="MiruUploaderRole"
    MIRU_EXTERNAL_ROLE_ARN="arn:aws:iam::008971655189:role/MiruS3IntegrationRole"

    cat > trust-policy.json <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": { "AWS": "${MIRU_EXTERNAL_ROLE_ARN}" },
          "Action": "sts:AssumeRole",
          "Condition": {
            "StringEquals": { "sts:ExternalId": "${EXTERNAL_ID}" }
          }
        }
      ]
    }
    EOF

    INTERNAL_ROLE_ARN="$(aws iam create-role \
      --role-name "${INTERNAL_ROLE_NAME}" \
      --assume-role-policy-document file://trust-policy.json \
      --query 'Role.Arn' --output text)"
    ```

    The captured `${INTERNAL_ROLE_ARN}` is your `role_arn` for registration.
  </Step>

  <Step title="Grant the role write access to the bucket">
    Attach a permissions policy granting **`s3:PutObject`** and
    **`s3:AbortMultipartUpload`** on the bucket. Devices upload via native S3
    multipart uploads, and the abort permission lets a failed multipart upload
    clean up after itself.

    ```bash theme={null}
    cat > put-policy.json <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject",
            "s3:AbortMultipartUpload"
          ],
          "Resource": "arn:aws:s3:::${BUCKET_NAME}/*"
        }
      ]
    }
    EOF

    aws iam put-role-policy \
      --role-name "${INTERNAL_ROLE_NAME}" \
      --policy-name miru-put-object \
      --policy-document file://put-policy.json
    ```

    <Note>
      [Verification](/data-uploads/primitives/buckets) writes a
      zero-byte probe object under `.miru/probe/`.
    </Note>
  </Step>

  <Step title="Register the bucket in Miru">
    Print the values needed to register the bucket:

    ```bash theme={null}
    printf 'name: %s\nregion: %s\nrole_arn: %s\n' \
      "${BUCKET_NAME}" \
      "${REGION}" \
      "${INTERNAL_ROLE_ARN}"
    ```

    From the **Buckets** page in the Miru dashboard, select **New bucket**, choose
    **AWS**, and supply:

    * `name` — the name of the S3 bucket
    * `region` — the bucket's region
    * `role_arn` — the role's ARN

    Select **Create** to register the bucket.
  </Step>
</Steps>
