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.
  • Storage is provisioned automatically for every organization while managed CAS is enabled — no per-org action from you or your users. Each org gets:
    • one S3 Access Point on the shared bucket,
    • one IAM managed policy, used as a session policy to scope credentials to that access point,
    • one storage backend in Chainloop, named chainloop-cloud-storage and marked as managed.
  • Chainloop reaches the access point through short-lived, per-request sts:AssumeRole sessions scoped to a single organization.
  • No long-lived AWS credentials are stored. Chainloop persists a small record per org in its secrets backend holding only the access point ARN, the region, the tenant role ARN and the per-org session policy ARN — no access keys.
  • Every object is keyed under <org-uuid>/sha256:<digest> inside the bucket, and the key prefix is derived from the authenticated org on each request rather than read from stored configuration.
  • Encryption at rest is delegated to a customer-managed KMS key on the bucket (SSE-KMS).
Provisioning is currently one-way: Chainloop creates access points, session policies and objects, but never deletes them. Removing storage for a deleted or downgraded organization is a manual operation today.

Authentication Model

S3 Access Points have no credentials of their own. Authentication is ordinary AWS IAM — the access point is a resource-side scoping primitive, not an identity. Getting this model right is what most of the setup below is about. Two properties drive everything:
  1. A request must be allowed by every layer it passes through. Effective permissions are the intersection of the tenant role’s identity policy, the per-org session policy, and the resource policies — not their union.
  2. A request through an access point is authorized twice, once against the access point ARN and once against the bucket ARN. AWS grants the access only if both contexts allow it. This is why the bucket policy must delegate to access points: without a bucket-context Allow, requests fail even when every access point and session policy is perfect.

How Pods Obtain AWS Credentials

Chainloop supports the two standard EKS mechanisms for giving a pod an AWS identity, and you pick one for your installation:
  • EKS Pod Identity (recommended) — the workload role trusts the pods.eks.amazonaws.com service principal, and you bind service accounts to it with pod identity associations.
  • IRSA — the workload role trusts your cluster’s OIDC provider, and each pod exchanges a projected service-account token via sts:AssumeRoleWithWebIdentity.
Both produce the same thing: workload credentials the pod then uses to assume the tenant role. Everything from that point on — the tenant-role session, the access point policy, the bucket policy, KMS — is identical either way. Only this first layer differs, and it is the layer that fails first when misconfigured. Configure your choice in Configure Workload Identities. Each layer, what must allow it, and how it fails: Which identity performs which call matters when reading logs:
  • The backend worker provisions: it creates access points, writes their resource policies, and creates the per-org session policies.
  • The cas and controlplane pods read and write objects. They only ever assume the tenant role.
  • The tenant role creates nothing. It is the principal named in every access point policy, and the role whose temporary sessions are narrowed per org.
Per-org isolation rests on two independent mechanisms: the session policy limits the credentials to a single access point, and the access point policy requires aws:userid to equal <tenant-role-id>:cas-<org-uuid> — a session name Chainloop derives from the authenticated request, not from stored configuration. A tampered record pointing org A at org B’s access point still assumes a session named for org A, and is denied.
Access point resource policies and per-org session policies are machine-generated. Never hand-author or edit them:
  • A hand-written access point policy is silently overwritten on the next reconciliation.
  • A hand-edited session policy is never corrected — Chainloop creates each per-org policy once and does not update existing ones, so a modified document stays modified until you delete the policy and let it be recreated.
The only policies you own are the ones in this guide: the bucket policy, the KMS key policy, and the IAM roles.

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 role Chainloop workloads assume per-request to reach the access points
  • Workload identities (EKS Pod Identity or IRSA) attached to the Chainloop backend, controlplane, and cas pods so they can call AWS APIs
You also need a secrets backend (HashiCorp Vault or AWS Secrets Manager) configured in your installation. Managed CAS stores each org’s access point record there, and provisioning fails without it.

Resource Naming

Chainloop derives access point and session policy names — they are not configurable. Both include a slug taken from backend.environment, and the bucket and IAM policies you write below must match it: <org-uuid-hex> is the organization UUID with hyphens stripped (32 characters); <12-hex> is its last 12 characters. Throughout the policies below, substitute [env] with prod or canary to match the environment you will configure. Setting backend.environment is mandatory in production: if you leave it unset, Chainloop creates chainloop-dev-* access points that your bucket policy — scoped to chainloop-prod-* — will reject.

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 exactly as shown, substituting only [bucketName], [region], [accountId], and [env]. It is the AWS-documented delegation pattern for access points: the bucket defers every access decision to the access point policies, which Chainloop generates per organization with a per-tenant aws:userid guard. Any request that does not arrive through one of your installation’s access points is denied by default.
Do not narrow this policy into a hand-scoped grant — for example naming the tenant role as the Principal or listing individual actions such as s3:GetObject/s3:PutObject. As described in Authentication Model, every access point request is authorized in two contexts, and the bucket context requires the bucket policy itself to allow the access. The per-org session policies Chainloop generates speak only access-point ARNs, so a partial bucket grant fails the bucket-context check and surfaces as opaque 403 Forbidden errors on the data path (typically on HeadObject), even when every access point and session policy is correct.This delegation form is not a loosening: the real access control lives in the machine-generated access point policies with per-tenant aws:userid guards, and access to the bucket other than through your installation’s access points remains denied.
If your security review requires an explicit block on direct bucket access, add a Deny statement alongside the delegation Allow — never instead of it. Narrow that Deny to object actions rather than s3:*: a blanket s3:* deny conditioned on the absence of an access point also denies bucket administration, including PutBucketPolicy, which leaves account-root access as the only way back in.

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. The key policy must admit the tenant role, either directly or by delegating to IAM for the account (a statement granting the account root, so identity-based policies can grant key use). An identity-based grant alone is not enough — a KMS key policy cannot be overridden by an IAM policy, so a key that does not admit the tenant role fails every upload and download with a KMS error. The tenant role is the only identity that touches objects; the backend workload identity only manages access points and needs no key access. Chainloop’s per-org session policies grant kms:GenerateDataKey and kms:Decrypt, matching SSE-KMS on PutObject and GetObject.

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. Grant sts:TagSession alongside sts:AssumeRole: workload sessions minted by EKS can carry session tags, and the assume call fails if propagating them is not permitted.
Permission policy — let it speak through any access point that matches your installation’s prefix, and use the KMS key:
s3:AbortMultipartUpload and s3:ListBucket are both load-bearing, and both fail in ways that look unrelated to this policy:
  • Without s3:AbortMultipartUpload, small uploads succeed and large (multipart) uploads fail — the generated access point policy grants the action, but effective access is the intersection.
  • Without s3:ListBucket, S3 hides object existence: a lookup for an object that is simply absent returns 403 Forbidden instead of 404, which Chainloop cannot distinguish from a real denial.

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: a controlplane workload role (shared by the controlplane and cas pods) and a backend workload role. Each one needs a trust policy that lets the pod’s Kubernetes service account assume it, plus the permission policy shown below.

Trust policy

The trust policy is what bootstraps the pod’s credentials. If it doesn’t allow the service account, every AWS call fails with AccessDenied before any tenant sts:AssumeRole is attempted — so getting this right is a prerequisite for everything else. The shape depends on the mechanism:
The role trusts the EKS service principal. Which service accounts may use it is expressed as an association (see Enable in Chainloop EE), not in the trust policy:
The cas pod runs the data path but shares the controlplane workload role (it is annotated with that role in Enable in Chainloop EE). The controlplane role must therefore be assumable by both the controlplane and the cas service accounts:
  • IRSA: list both subjects in the sub condition, as shown above.
  • Pod Identity: create an association for each of the two service accounts.
Forgetting the cas service account is the most common misconfiguration: reads fail for every org with AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity, because the cas pod can never bootstrap its credentials.

Permission policies

Controlplane workload role — used by the controlplane and cas pods on the data path. Needs to call sts:AssumeRole against the tenant role:
Backend workload role — used by the platform backend’s reconciler. Needs the controlplane permissions above plus the access point lifecycle and per-org session policy permissions:
The backend assumes the tenant role once at startup to read its unique ID (needed for the aws:userid condition in every access point policy), then creates each org’s access point, writes its resource policy, and creates its session policy. s3:TagResource is required even though Chainloop never calls the TagResource API: it tags each access point as part of the CreateAccessPoint call, and AWS requires both actions to allow tagging on create. Without it, provisioning fails with AccessDenied on CreateAccessPoint. s3:DeleteAccessPoint and iam:DeletePolicy are not exercised today — provisioning is one-way — but granting them now means teardown support will not require a policy change.
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, set backend.environment and configure backend.managedCas:
chainloop-ee values.yaml
backend.environment selects the access point name prefix (see Resource Naming) and must match the prefix in your bucket and IAM policies. The chart accepts only production or canary. Leaving it unset produces chainloop-dev-* access points, and every CreateAccessPoint call is then denied by the bucket policy.
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
For EKS Pod Identity, create an aws_eks_pod_identity_association per (namespace, service account) pair instead — including one binding the cas service account to the controlplane workload role, matching the IRSA annotation above. Apply the values and roll out the chart.

Restricting Access Points to a VPC

By default the per-org access points are internet-facing (access is still governed entirely by the policies above). To make them reachable only from inside a VPC, set:
chainloop-ee values.yaml
Two caveats:
  • Existing access points are not migrated. The network origin is fixed at creation, and reconciliation treats an already-existing access point as done. Only orgs provisioned after you set vpcId get VPC-scoped access points.
  • An invalid VPC ID stalls provisioning. The value is not validated up front; a wrong ID turns into a per-org CreateAccessPoint failure that blocks the reconciler from progressing to the remaining orgs.

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 provided by Chainloop.
    Chainloop UI showing a default storage backend managed by Chainloop
  2. Confirm the access point exists in the AWS console under S3 → Access Points, named chainloop-[env]-<org-uuid-hex>. If it isn’t there yet, give it a moment and check the backend logs — provisioning runs in both the backend and its worker, so select by label to avoid reading just one pod:
  3. As a user of that org, run an attestation or upload an artifact:
  4. Inspect the bucket — the object should be under the org’s prefix, encrypted with your KMS key:
If no access point is ever created and the logs show no provisioning errors either, grep the backend logs for continuing with managed CAS disabled. When the AWS client cannot be constructed — a misconfigured base role, a missing sts:AssumeRole permission, the wrong region — Chainloop logs a warning and starts with managed CAS silently switched off rather than crash-looping.

Troubleshooting

Read any chained STS error inside-out — the innermost operation is the one that actually failed. A HeadObject error wrapping an AssumeRole error wrapping AssumeRoleWithWebIdentity is a credential-bootstrap failure, not an S3 policy problem. When you hit a bare 403 Forbidden with no error code or message, that is HeadObject: it is an HTTP HEAD request, so S3 cannot return an error body and cannot tell you which layer denied it. Re-run the same access as a request that can return a body. Both of these name the denying layer in one line — use the access point ARN as --bucket:
Then match what you get: