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

# Managed CAS with AWS S3 Access Points

> Auto-provision a per-organization S3 Access Point for every org in your Chainloop installation, backed by a single shared bucket.

Managed CAS is a Chainloop Enterprise feature where the platform auto-provisions a dedicated [AWS S3 Access Point](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html) for every organization in your installation, all backed by a single shared S3 bucket. End users do not need to bring their own [CAS backend](/concepts/cas-backend) — uploads and downloads work out of the box from the moment an org is created.

<Info>
  This guide is for **platform operators** running Chainloop on-prem in AWS. If your users want to bring their own storage (OCI registry, their own S3 bucket, Azure Blob, etc.) instead, see the [CAS backend concepts page](/concepts/cas-backend).
</Info>

## How It Works

* **Single shared bucket** per Chainloop installation. There is no per-org bucket to provision or rotate.
* A periodic reconciler in the Chainloop backend creates one [S3 Access Point](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html) per organization, named with a stable prefix (for example `chainloop-<org-uuid>`).
* The controlplane talks to the access point through ephemeral, per-request `sts:AssumeRole` sessions. **No long-lived credentials are ever stored** in Chainloop's secrets backend — only the tenant's AP ARN and the base role ARN.
* Org-level isolation is layered:
  * The **access point resource policy** restricts who can address the AP, scoped via `aws:userid` to the session minted for that org.
  * A **per-tenant IAM managed session policy** further narrows the assumed session to the org's AP and its key prefix.
  * The **bucket policy** rejects any request whose `s3:DataAccessPointArn` doesn't match your installation's AP-name prefix, so a misconfigured role still cannot cross the boundary.
  * Every object is keyed under `<org-uuid>/sha256:<digest>` inside the bucket.
* Encryption at rest is delegated to a customer-managed **KMS key** on the bucket (`SSE-KMS`).

## Prerequisites

You will provision the following resources in your AWS account before enabling the feature in Chainloop. We recommend managing them with Terraform or OpenTofu.

* A single **S3 bucket** for the installation
* A customer-managed **KMS key** for `SSE-KMS` on the bucket
* A **tenant IAM role** — the "base role" the Chainloop workloads assume per-request
* **Workload identities** (EKS Pod Identity or IRSA) attached to the Chainloop `backend`, `controlplane`, and `cas` pods so they can call AWS APIs

Pick an **AP name prefix** that uniquely identifies this installation, for example `chainloop-` or `chainloop-prod-`. Every access point Chainloop creates will start with this prefix; the bucket policy and IAM permission policies below all reference it.

### 1. Create the S3 Bucket

Create one bucket in the region you want to operate from. Block all public access. Enable default encryption with the KMS key you create in the next step.

Attach this **bucket policy** to constrain every request to go through an access point that matches your prefix:

```json theme={"dark"}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireAccessPointWithInstallPrefix",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::[bucketName]",
        "arn:aws:s3:::[bucketName]/*"
      ],
      "Condition": {
        "StringNotLike": {
          "s3:DataAccessPointArn": "arn:aws:s3:[region]:[accountId]:accesspoint/[apPrefix]-*"
        }
      }
    }
  ]
}
```

Replace `[bucketName]`, `[region]`, `[accountId]`, and `[apPrefix]` with your values.

### 2. Create the KMS Key

Create a customer-managed KMS key in the same region as the bucket and set it as the bucket's default encryption key. Make sure the key policy allows both the **tenant role** (for `Encrypt`/`Decrypt` on the data path) and the **backend workload identity** (for AP lifecycle calls) to use it.

### 3. Create the Tenant IAM Role

This is the role the Chainloop workloads assume per-request via STS. Note its name — you will pass it to the chart as `baseRoleName`.

**Trust policy** — allow the backend and controlplane workload identities to assume it, including `sts:TagSession` (the Chainloop backend tags every assume call with the tenant identifier):

```json theme={"dark"}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::[accountId]:role/[backendWorkloadRoleName]",
          "arn:aws:iam::[accountId]:role/[controlplaneWorkloadRoleName]"
        ]
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}
```

**Permission policy** — let it speak through any access point that matches your prefix and read/write objects on the bucket:

```json theme={"dark"}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:[region]:[accountId]:accesspoint/[apPrefix]-*",
        "arn:aws:s3:[region]:[accountId]:accesspoint/[apPrefix]-*/object/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey"
      ],
      "Resource": "[kmsKeyArn]"
    }
  ]
}
```

### 4. Configure Workload Identities

The Chainloop pods need AWS credentials to call AWS APIs. We recommend [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html); [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) works equivalently.

You need **two** workload identity roles:

**Controlplane workload role** — used by the controlplane and cas pods on the data path. Needs to call `sts:AssumeRole` against the tenant role:

```json theme={"dark"}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ],
      "Resource": "arn:aws:iam::[accountId]:role/[tenantRoleName]"
    }
  ]
}
```

**Backend workload role** — used by the platform backend's periodic reconciler. Needs the controlplane permissions above **plus** the AP-lifecycle and per-tenant session-policy management permissions:

```json theme={"dark"}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AssumeTenantRole",
      "Effect": "Allow",
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ],
      "Resource": "arn:aws:iam::[accountId]:role/[tenantRoleName]"
    },
    {
      "Sid": "ManageAccessPoints",
      "Effect": "Allow",
      "Action": [
        "s3:CreateAccessPoint",
        "s3:DeleteAccessPoint",
        "s3:GetAccessPoint",
        "s3:GetAccessPointPolicy",
        "s3:PutAccessPointPolicy",
        "s3:DeleteAccessPointPolicy",
        "s3:TagResource",
        "s3:UntagResource",
        "s3:ListTagsForResource"
      ],
      "Resource": "arn:aws:s3:[region]:[accountId]:accesspoint/[apPrefix]-*"
    },
    {
      "Sid": "ManageTenantSessionPolicies",
      "Effect": "Allow",
      "Action": [
        "iam:CreatePolicy",
        "iam:DeletePolicy",
        "iam:CreatePolicyVersion",
        "iam:DeletePolicyVersion",
        "iam:GetPolicy",
        "iam:ListPolicyVersions"
      ],
      "Resource": "arn:aws:iam::[accountId]:policy/[sessionPolicyPrefix]-*"
    }
  ]
}
```

`[sessionPolicyPrefix]` is the prefix Chainloop uses when naming the per-tenant managed policies it creates. Pick a short, installation-specific prefix such as `cas-`. The only requirement is that this IAM resource scope matches what the backend will actually create.

<Info>
  The IAM "1500 customer-managed policies per account" service quota applies because Chainloop creates one managed policy per organization. Request a quota increase if you expect to scale past it.
</Info>

## Enable in Chainloop EE

In your `chainloop-ee` Helm chart `values.yaml`, configure `backend.managedCas`:

```yaml chainloop-ee values.yaml theme={"dark"}
backend:
  managedCas:
    enabled: true
    bucket: [bucketName]
    region: [region]
    accountId: "[accountId]"
    baseRoleName: [tenantRoleName]
```

Bind the workload identities to the Chainloop service accounts. The exact mechanism depends on whether you are using EKS Pod Identity or IRSA — both come down to associating an IAM role with the Kubernetes service account. For IRSA, annotate the service accounts:

```yaml chainloop-ee values.yaml theme={"dark"}
backend:
  serviceAccount:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::[accountId]:role/[backendWorkloadRoleName]

controlplane:
  serviceAccount:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::[accountId]:role/[controlplaneWorkloadRoleName]

cas:
  serviceAccount:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::[accountId]:role/[controlplaneWorkloadRoleName]
```

For EKS Pod Identity, create an `aws_eks_pod_identity_association` per `(namespace, service account)` pair instead.

Apply the values and roll out the chart.

<Warning>
  Do not enable `backend.managedCas.devMode`. It bypasses the per-tenant isolation guarantees and is only meant for single-user development against ambient AWS credentials.
</Warning>

## Verify

1. Create a new organization in the Chainloop UI. Open **Storage Backends** for that org — you should see a default backend named `chainloop-cloud-storage` labelled "managed by Chainloop".
   <Frame>
     <img src="https://mintcdn.com/chainloop/2DV8Wt8Zh2ib38eQ/guides/deployment/guides/img/managed-cas-storage-backend.png?fit=max&auto=format&n=2DV8Wt8Zh2ib38eQ&q=85&s=ddcf31fbac8fa2853af770dbe70be0d0" alt="Chainloop UI showing a default storage backend managed by Chainloop" width="1526" height="314" data-path="guides/deployment/guides/img/managed-cas-storage-backend.png" />
   </Frame>
2. Watch the platform backend logs — within one reconciliation cycle you should see it create a new access point under your prefix, e.g. `chainloop-<org-uuid>`. Verify in the AWS console under **S3 → Access Points**.
3. As a user of that org, [run an attestation](/get-started/first-attestation) or upload an artifact:
   ```bash theme={"dark"}
   chainloop artifact upload -f myfile
   ```
4. Inspect the bucket — the object should be under the org's prefix, encrypted with your KMS key:
   ```
   s3://[bucketName]/<org-uuid>/sha256:<digest>
   ```

If the upload fails, check the backend logs for STS or S3 access denials — the most common causes are a missing `sts:TagSession` action on the tenant role's trust policy, or a bucket policy whose AP-prefix glob does not match the access point names Chainloop is creating.
