Skip to main content
Managed CAS is a Chainloop Enterprise feature where the platform auto-provisions a dedicated AWS S3 Access Point 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 — uploads and downloads work out of the box from the moment an org is created.
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.

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 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:
{
  "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):
{
  "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:
{
  "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; IRSA 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:
{
  "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:
{
  "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.
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.

Enable in Chainloop EE

In your chainloop-ee Helm chart values.yaml, configure backend.managedCas:
chainloop-ee values.yaml
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:
chainloop-ee values.yaml
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.
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.

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”.
    Chainloop UI showing a default storage backend managed by Chainloop
  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 or upload an artifact:
    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.