<p align="justify">

# Set up a bucket to receive your data

How to create credentials on AWS, Google Cloud, or Azure, and the exact permissions to grant so we can deliver data to your own storage.

> **Where to set this up**
>
> If you are using static credentials, meaning a key and a secret or a key file, you can set this up yourself in the Aurora UI at [aurora.pixxel.space](https://aurora.pixxel.space/). That works today for all three providers.
>
> The AWS IAM role option is different. It needs a workspace ID from us, so check with your Pixxel contact for that one.
>
> Either way, grant the permissions in this guide first. Aurora checks your credentials when you save.
>
> **One naming note.** What this guide calls a delivery bucket is called a cloud store in Aurora. They are the same thing.

---

## What we do with your bucket

We write files into your bucket. We also read back and list what we wrote, so we can confirm a delivery arrived complete. We do not delete your files. The permissions stay small, and every section below shows the smallest set that works.

1. **You create the bucket** or container in your own cloud account. It stays yours.
2. **You create credentials** that can write to it and read back what was written.
3. **You send them to us once.** We encrypt them before storing them.
4. **We write files in,** then check they all arrived, under the path prefix you chose.

If you would rather we could not see anything else in the bucket, point the permissions at one folder and tell us to use that folder as the path prefix. Each provider section shows how.

---

## What you send us

Four things, whichever provider you use.

| Field | Required | What it means |
|---|:---:|---|
| Type | Yes | S3, GCS, or AZBLOB |
| Bucket | Yes | The bucket name, or the container name on Azure |
| Region | Yes | The region your bucket lives in |
| Path prefix | No | A folder we put every file under, for example `pixxel/deliveries/` |
| Credentials | Yes | The secret values listed in your provider's section below |

> **About region**
>
> On AWS the region must match the real region of your bucket, or writes will fail. On Google Cloud and Azure we do not use it to find your bucket, but we still ask for it so your records stay accurate.

---

## Login methods we support

Pick one method per bucket. Send us only the fields for that method.

| Provider | Method | Notes |
|---|---|---|
| AWS S3 | IAM role we assume | **Best choice.** No secret leaves your account, nothing to rotate |
| AWS S3 | IAM user access key | Works. You have to rotate it yourself |
| AWS S3 | Temporary session credentials | Expires in hours. Good for a test only |
| Google Cloud Storage | Service account key file | The only method we support on Google Cloud |
| Azure Blob Storage | Entra service principal | **Best choice.** The only Azure option you can limit to one container |
| Azure Blob Storage | Storage account key | Works. Grants full access to the whole storage account |
| Azure Blob Storage | Connection string | Same full access as an account key |

---

## AWS S3 — three methods

### Option 1: IAM role

This is the option we recommend. You create a role in your AWS account and let our account assume it. No secret ever leaves your account, and there are no keys for you to rotate.

> **Ask us first**
>
> You need one thing from us before you start: **your workspace ID**. It is unique to you, and it stops anyone else from using your role by mistake. Our AWS account ID is `375894565286`, and it is already filled in below.

#### 1. Create the permission policy

This is the smallest policy that works. Replace `YOUR-BUCKET` with your bucket name.

| Permission | Why we need it |
|---|---|
| `s3:PutObject` | Writes the files. Big files upload in parts, and this covers every part |
| `s3:AbortMultipartUpload` | Cleans up those parts when an upload fails partway. Without it, failed uploads leave hidden parts behind that you still pay for |
| `s3:GetObject` | Reads back a delivered file to confirm it arrived whole |
| `s3:ListBucket` | Lists what we delivered, so we can check nothing is missing |

Note the two separate statements. `s3:ListBucket` acts on the bucket itself, so its resource ends at the bucket name with no `/*`. Putting it on the `/*` resource is a common mistake, and it fails quietly.

To limit us to one folder, point the first statement at that folder and add a matching condition to the second.

**IAM permission policy:**
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PixxelWriteAndReadBack",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:AbortMultipartUpload",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET/*"
    },
    {
      "Sid": "PixxelListForDeliveryChecks",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::YOUR-BUCKET"
    }
  ]
}
```

**Scoped to one folder:**
```json
{
  "Sid": "PixxelWriteAndReadBack",
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:AbortMultipartUpload", "s3:GetObject"],
  "Resource": "arn:aws:s3:::YOUR-BUCKET/pixxel/*"
},
{
  "Sid": "PixxelListForDeliveryChecks",
  "Effect": "Allow",
  "Action": "s3:ListBucket",
  "Resource": "arn:aws:s3:::YOUR-BUCKET",
  "Condition": {
    "StringLike": {
      "s3:prefix": "pixxel/*"
    }
  }
}
```

Then tell us to use `pixxel/` as the path prefix.

#### 2. Create the trust policy

This says who is allowed to assume the role.

**IAM trust policy:**
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::375894565286:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "YOUR-WORKSPACE-ID"
        }
      }
    }
  ]
}
```

Please keep the workspace ID check. It is the thing that makes sure only your own deliveries can use this role. `sts:ExternalId` is what AWS calls the field. The value that goes in it is your workspace ID.

#### 3. Send us the details

| Field | Value | API name |
|---|---|---|
| Role ARN | The ARN of the role you just made | `role_arn` |
| Workspace ID | The same one you put in the trust policy | `external_id` |

Both are required. A role sent without a workspace ID will not be accepted.

**If you use our API:**
```json
{
  "name": "my-delivery-bucket",
  "type": "S3",
  "configs": {
    "bucket": "YOUR-BUCKET",
    "region": "us-west-2",
    "path_prefix": "pixxel/"
  },
  "secrets": {
    "role_arn": "arn:aws:iam::YOUR-ACCOUNT-ID:role/YourRoleName",
    "external_id": "YOUR-WORKSPACE-ID"
  }
}
```

### Option 2: IAM user access key

Create an IAM user, attach the same permission policy from step 1 (both statements), and create an access key for it.

| Field | Value | API name |
|---|---|---|
| Access key ID | Starts with `AKIA` | `access_key_id` |
| Secret access key | The secret shown once at creation | `secret_access_key` |

These keys never expire on their own, so rotation is on you. Read the [note on rotation](#credentials-cannot-be-edited-later) further down before you pick this option.

### Option 3: Temporary session credentials

If your security team issues short-lived credentials from AWS STS, we accept those too.

| Field | Value | API name |
|---|---|---|
| Access key ID | Starts with `ASIA` | `access_key_id` |
| Secret access key | The matching secret | `secret_access_key` |
| Session token | The session token | `session_token` |

These stop working when the session expires, usually within a few hours. Deliveries start failing at that point. Use this for a short test only.

### If your bucket uses KMS encryption

A bucket encrypted with a customer managed KMS key needs extra permissions, or writes are denied. Add these to the key policy, or to the role or user policy.

A bucket using the default S3 managed encryption needs nothing extra.

**KMS statement:**
```json
{
  "Effect": "Allow",
  "Action": [
    "kms:GenerateDataKey",
    "kms:Encrypt"
  ],
  "Resource": "arn:aws:kms:REGION:YOUR-ACCOUNT-ID:key/YOUR-KEY-ID"
}
```

---

## Google Cloud Storage — one method

We support one login method: a service account key file.

> **Not supported**
>
> We do not accept workload identity federation or any other credential type. If the JSON file does not say `"type": "service_account"`, we reject it.

#### 1. Create a service account

```bash
gcloud iam service-accounts create pixxel-delivery \
  --display-name="Pixxel delivery"
```

#### 2. Grant it access to your bucket

Grant on the bucket, not the whole project. That keeps the access narrow.

```bash
gcloud storage buckets add-iam-policy-binding gs://YOUR-BUCKET \
  --member="serviceAccount:pixxel-delivery@YOUR-PROJECT.iam.gserviceaccount.com" \
  --role="roles/storage.objectUser"
```

| Role | What it allows |
|---|---|
| `roles/storage.objectUser` | Create, read, list, and replace. One role that covers everything, and what we suggest |
| `roles/storage.objectCreator` plus `roles/storage.objectViewer` | The same, minus replace. Use this pair if you never want an existing file overwritten |

The second option cannot overwrite. If a delivery ever rewrites a file at the same path, it fails with a permission error. If you expect repeat deliveries to the same paths, use `objectUser`.

`roles/storage.objectCreator` on its own is not enough any more. It cannot read or list, so we would have no way to confirm a delivery finished.

For a custom role, these are the permissions we need:

| Permission | Why we need it |
|---|---|
| `storage.objects.create` | Writes the files |
| `storage.objects.get` | Reads back a delivered file to confirm it arrived whole |
| `storage.objects.list` | Lists what we delivered, so we can check nothing is missing |
| `storage.objects.delete` | Only needed so a delivery can replace a file at a path that already exists. Google counts an overwrite as a delete |

Grant these on the bucket, or on a single folder if you would rather keep us to one.

#### 3. Create a key

```bash
gcloud iam service-accounts keys create key.json \
  --iam-account=pixxel-delivery@YOUR-PROJECT.iam.gserviceaccount.com
```

#### 4. Send us the details

| Field | Value | API name |
|---|---|---|
| Project ID | Your Google Cloud project ID | `project_id` |
| Service account key | The full contents of `key.json` | `service_account_credentials` |

Send the whole file contents. Not a file path, and not just the private key.

We ask Google only for read and write access to storage. We cannot use the key for anything else in your project.

---

## Azure Blob Storage — three methods

We try these in the order below, so send us only the fields for the one you picked.

### Option 1: Entra service principal

This is the option we recommend. It is the only Azure method you can limit to a single container. The other two give access to the whole storage account.

#### 1. Create an app registration and service principal

```bash
az ad app create --display-name pixxel-delivery
az ad sp create --id <app-id>
```

#### 2. Create a client secret

```bash
az ad app credential reset --id <app-id>
```

Note the secret value now. Azure shows it only once.

#### 3. Grant access to the container

Set the scope to the container, not the storage account, so the access stays narrow.

```bash
az role assignment create \
  --assignee <app-id> \
  --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/<account-name>/blobServices/default/containers/<container-name>"
```

**Storage Blob Data Contributor** covers what we do, reading back and listing included. For a custom role instead, these are the data actions we need:

```
Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read
Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write
Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action
```

The `read` action covers both reading a blob back and listing the container.

#### 4. Send us the details

| Field | Value | API name |
|---|---|---|
| Account name | Your storage account name | `account_name` |
| Tenant ID | Your Entra tenant ID | `tenant_id` |
| Client ID | The app registration ID | `client_id` |
| Client secret | The secret from step 2 | `client_secret` |

Client secrets expire, and Azure sets a lifetime when you create one. Put a reminder in your calendar, and read the [note on rotation](#credentials-cannot-be-edited-later) below.

### Option 2: Storage account key

| Field | Value | API name |
|---|---|---|
| Account name | Your storage account name | `account_name` |
| Account key | Either of the two account keys | `account_key` |

> **Know what this grants**
>
> A storage account key gives full control of every container in that account, including read and delete. It cannot be narrowed. We only write, but the key itself is not limited. Use option 1 if you can.

### Option 3: Connection string

| Field | Value | API name |
|---|---|---|
| Connection string | Azure Portal, then Storage account, then Access keys | `connection_string` |

A connection string contains an account key, so it carries the same full access as option 2. The same warning applies.

---

## Things worth knowing

### Saving credentials does not test your bucket

When you save a delivery bucket with us, we check that your credentials are real and well formed. We do not write a test file at that moment.

So a bucket can save cleanly and still fail on the first delivery, usually because a permission is missing or the bucket name is wrong. If that happens, check the permission policy first. We suggest running one small delivery right after setup to confirm the whole path works.

### Credentials cannot be edited later

A delivery bucket is fixed once created. You can rename it and change its labels, but you cannot change the bucket, the region, or the credentials.

To rotate a key or a client secret, create a new delivery bucket with the new credentials, switch your deliveries over to it, then delete the old one. This is one more reason to prefer the AWS IAM role and the Azure service principal. With an IAM role there is nothing to rotate at all.

### Paths and overwrites

Every file we write goes under the path prefix you gave us. Set the prefix to `pixxel/` and a file named `scene.tif` lands at `pixxel/scene.tif`.

If a delivery writes to a path that already holds a file, the old file is replaced. If you do not want that, use a different path prefix for each delivery, or turn on object versioning on your bucket.

### Your credentials are encrypted

We encrypt your credentials before storing them. They are never returned by our API, never shown in our interface, and are only decrypted at the moment a delivery runs.

---

## Before you tell us you are ready

- [ ] The bucket or container exists, and I know its exact name and region.
- [ ] The credentials can write to that bucket, and read and list what was written.
- [ ] AWS role only: the trust policy names the Pixxel account ID and my workspace ID.
- [ ] AWS with a customer managed KMS key: the key permissions are added.
- [ ] Google Cloud: the key file says `"type": "service_account"`.
- [ ] Azure: I picked one method and am sending only those fields.
- [ ] I noted any expiry date, so I can plan a swap before it lapses.

If a delivery fails, send us the error message. It tells us which side the problem is on, and it usually points straight at the missing permission.

> Please check with your Pixxel contact that you have the current version of this guide before you start.

</p>
