Generic Agent: Object Storage

Configure object storage for the Generic Agent on any platform

The Generic Agent requires an object storage bucket for data samples and temporary data written during operations. This storage is private and accessed only by the agent.

The AWS, Azure, and GCP Terraform modules create the bucket and grant the agent access automatically. This guide is for deployments that configure storage manually β€” the Kubernetes and Docker Compose guides.

Options

StoragestorageTypeCredentialsBest for
Amazon S3S3EKS Pod Identity or access keysEKS, or any cluster with access to an AWS account
Azure Blob StorageAZURE_BLOBWorkload Identity (managed identity)AKS
Google Cloud StorageGCSWorkload IdentityGKE
MinIO / S3-compatibleS3_COMPATIBLEAccess key and secretOn-premises clusters, development, and testing

The agent writes all objects under the mcd/ prefix. Set MCD_STORAGE_PREFIX via container.extraEnv to change it, or to an empty string to write at the bucket root.

Amazon S3

1. Create the bucket

aws s3 mb s3://mcd-agent-store --region us-west-2

Block public access and enable default encryption:

aws s3api put-public-access-block \
  --bucket mcd-agent-store \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

aws s3api put-bucket-encryption \
  --bucket mcd-agent-store \
  --server-side-encryption-configuration \
    '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
πŸ“˜

Using a KMS key

The agent never specifies an encryption method when it writes an object, so whatever you configure as the bucket's default encryption is what gets applied. To encrypt with a customer-managed KMS key, set it as the bucket default:

aws s3api put-bucket-encryption \
  --bucket mcd-agent-store \
  --server-side-encryption-configuration \
    '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"<your-kms-key-arn>"},"BucketKeyEnabled":true}]}'

The agent's identity then also needs access to the key β€” add this statement to the role you create in the next step, and make sure the key policy allows that role (directly, or by delegating to IAM policies in the account):

{
  "Effect": "Allow",
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "<your-kms-key-arn>"
}

Both actions are required: kms:GenerateDataKey to write objects (including multipart uploads) and kms:Decrypt to read them back. The agent never copies or re-encrypts objects, so kms:Encrypt and kms:ReEncrypt* β€” listed in the equivalent AWS data store policy β€” are not needed here.

2. Grant the agent access

The agent needs the following permissions on the bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:GetBucketPublicAccessBlock",
        "s3:GetBucketPolicyStatus",
        "s3:GetBucketAcl"
      ],
      "Resource": [
        "arn:aws:s3:::mcd-agent-store",
        "arn:aws:s3:::mcd-agent-store/*"
      ]
    }
  ]
}

Attach the policy above to a role, then choose one of the following:

Requires the eks-pod-identity-agent add-on on the cluster. Create a role that the EKS service can assume:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "pods.eks.amazonaws.com" },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    }
  ]
}

Associate it with the agent's service account:

aws eks create-pod-identity-association \
  --cluster-name <your-cluster-name> \
  --namespace mcd-agent \
  --service-account mcd-agent-service-account \
  --role-arn <your-role-arn>

No values.yaml change is needed β€” the association is matched by namespace and service account name.

The agent uses the standard AWS credential chain, so other mechanisms β€” such as IAM Roles for Service Accounts (IRSA) β€” also work if your platform already standardizes on them. Annotate the agent's service account through serviceAccount.annotations in your values.yaml.

3. Configure the agent

container:
  storageType: "S3"
  storageBucketName: "mcd-agent-store"

4. Expire temporary objects (optional)

The agent does not delete every object it writes. These lifecycle rules match what the Terraform module configures:

PrefixExpiration
mcd/90 days
mcd/tmp2 days
mcd/responses1 day

Then verify storage access.

Azure Blob Storage (AKS)

πŸ“

These steps assume the agent runs on AKS, with the cluster's OIDC issuer and workload identity features enabled. The agent authenticates to Blob Storage with a user-assigned managed identity through Microsoft Entra Workload ID β€” it never uses storage account keys or connection strings, so the account can keep shared key access disabled.

1. Create the storage account and container

az storage account create \
  --name <your-storage-account-name> \
  --resource-group <your-resource-group> \
  --sku Standard_LRS \
  --https-only true \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false \
  --allow-shared-key-access false
az storage container create \
  --name mcdstore \
  --account-name <your-storage-account-name> \
  --auth-mode login
πŸ“˜

Blob Storage encrypts data at rest by default. If you configure a customer-managed key on the storage account, the agent needs no additional permissions β€” the storage account uses its own identity to reach the key.

2. Grant the agent access

Create a user-assigned managed identity:

az identity create \
  --name <your-identity-name> \
  --resource-group <your-resource-group>

Assign it the Storage Blob Data Contributor role on the storage account:

az role assignment create \
  --assignee-object-id "$(az identity show --name <your-identity-name> --resource-group <your-resource-group> --query principalId -o tsv)" \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope "$(az storage account show --name <your-storage-account-name> --resource-group <your-resource-group> --query id -o tsv)"

Federate the identity with the agent's Kubernetes service account, so the pod can exchange its projected token for an Entra token:

az identity federated-credential create \
  --name kubernetes-federated-credential \
  --identity-name <your-identity-name> \
  --resource-group <your-resource-group> \
  --issuer "$(az aks show --name <your-cluster-name> --resource-group <your-resource-group> --query oidcIssuerProfile.issuerUrl -o tsv)" \
  --subject "system:serviceaccount:mcd-agent:mcd-agent-service-account" \
  --audience api://AzureADTokenExchange
πŸ“˜

The subject must match the namespace and service account the agent runs under. The chart always names the service account mcd-agent-service-account; adjust the namespace if you changed namespace in your values.yaml.

3. Configure the agent

container:
  storageType: "AZURE_BLOB"
  storageAccountName: "<your-storage-account-name>"
  storageBucketName: "mcdstore"

serviceAccount:
  annotations:
    azure.workload.identity/client-id: "<your-identity-client-id>"

deploymentTemplateLabels:
  azure.workload.identity/use: "true"

Get the client ID with:

az identity show \
  --name <your-identity-name> \
  --resource-group <your-resource-group> \
  --query clientId -o tsv
⚠️

Both the service account annotation and the azure.workload.identity/use pod label are required. The label is what tells the workload identity webhook to inject the projected token and the AZURE_CLIENT_ID environment variable into the pod.

Each one fails differently, and neither failure names the missing piece:

Annotation missing β€” the webhook injects everything except AZURE_CLIENT_ID, so the agent fails on startup, before contacting the storage account:

ValueError: 'client_id' is required. Please pass it in or set the AZURE_CLIENT_ID environment variable

Label missing β€” nothing is injected, and the agent authenticates as the cluster's node identity instead. That identity has no role on the storage account, so the failure looks like a missing role assignment:

ErrorCode:AuthorizationPermissionMismatch
This request is not authorized to perform this operation using this permission.

In either case, check the annotation and the label before revisiting role assignments. To confirm what the pod actually received:

kubectl exec -n mcd-agent deploy/mcd-agent-deployment -- env | grep AZURE_

A correctly configured pod lists AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE, and AZURE_AUTHORITY_HOST.

4. Expire temporary blobs (optional)

The agent does not delete every blob it writes. Add a lifecycle management policy on the storage account matching what the Terraform module configures:

PrefixExpiration
mcdstore/mcd90 days
mcdstore/mcd/tmp2 days
mcdstore/mcd/responses1 day

Prefixes in a lifecycle policy include the container name, and are matched against blockBlob and appendBlob types.

Then verify storage access.

Google Cloud Storage (GKE)

πŸ“

These steps assume the agent runs on GKE, with Workload Identity Federation enabled on the cluster. The agent authenticates to Cloud Storage as a Google service account bound to its Kubernetes service account β€” no service account keys are needed.

1. Create the bucket

gcloud storage buckets create gs://mcd-agent-store \
  --project <your-project-id> \
  --location <your-location> \
  --uniform-bucket-level-access \
  --public-access-prevention
πŸ“˜

Cloud Storage encrypts data at rest by default. If you configure a customer-managed encryption key (CMEK) on the bucket, the agent needs no additional permissions β€” Cloud Storage uses its own service agent to reach the key.

2. Grant the agent access

Create a Google service account for the agent:

gcloud iam service-accounts create mcd-agent \
  --project <your-project-id>

Grant it access to the bucket:

gcloud storage buckets add-iam-policy-binding gs://mcd-agent-store \
  --member "serviceAccount:mcd-agent@<your-project-id>.iam.gserviceaccount.com" \
  --role roles/storage.objectAdmin

gcloud storage buckets add-iam-policy-binding gs://mcd-agent-store \
  --member "serviceAccount:mcd-agent@<your-project-id>.iam.gserviceaccount.com" \
  --role roles/storage.legacyBucketReader
πŸ“˜

Both roles are required. roles/storage.objectAdmin covers reading, writing, and deleting objects, but grants nothing at the bucket level. The agent resolves the bucket before every operation, so without storage.buckets.get even a write fails:

403 GET https://storage.googleapis.com/storage/v1/b/mcd-agent-store:
does not have storage.buckets.get access to the Google Cloud Storage bucket

roles/storage.legacyBucketReader (Storage Legacy Bucket Reader) adds it. Despite the name, these roles are not deprecated β€” they are the granular way to grant bucket-level reads. A custom role containing only storage.buckets.get works equally well if you prefer to avoid the predefined role.

Bind the Google service account to the agent's Kubernetes service account:

gcloud iam service-accounts add-iam-policy-binding \
  mcd-agent@<your-project-id>.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:<your-project-id>.svc.id.goog[mcd-agent/mcd-agent-service-account]"
πŸ“˜

The member must match the namespace and service account the agent runs under. The chart always names the service account mcd-agent-service-account; adjust the namespace if you changed namespace in your values.yaml.

3. Configure the agent

container:
  storageType: "GCS"
  storageBucketName: "mcd-agent-store"

serviceAccount:
  annotations:
    iam.gke.io/gcp-service-account: "mcd-agent@<your-project-id>.iam.gserviceaccount.com"

4. Expire temporary objects (optional)

The agent does not delete every object it writes. These lifecycle rules match what the Terraform module configures:

PrefixExpiration
mcd/90 days
mcd/tmp2 days
mcd/responses1 day

Apply them with a lifecycle configuration using matchesPrefix conditions and the Delete action.

Then verify storage access.

MinIO and other S3-compatible storage

The agent works with any S3-compatible storage service. This is the option to use when no cloud-native service is available β€” on-premises clusters, or local development and testing.

1. Deploy MinIO

If you already run an S3-compatible service, skip to step 3.

A complete working example β€” MinIO, secrets, and a ready values.yaml β€” is available in the mcd-public-resources repository.

The example below deploys MinIO into the same cluster as the agent, creating a minio namespace with a PersistentVolumeClaim, Deployment, and Services:

kubectl apply -f https://raw.githubusercontent.com/monte-carlo-data/hermes-agent/main/environments/local/minio/k8s.yaml
⚠️

This manifest is a reference for development and testing: it runs MinIO with the default minioadmin credentials over plain HTTP. For production, use a cloud-native storage service, or run MinIO with your own credentials, TLS, and a storage class suited to the workload.

2. Create the bucket

Port-forward the MinIO API and console:

kubectl port-forward -n minio deploy/minio 9000:9000 9001:9001

Open http://localhost:9001/, log in, and create a bucket named mcd-agent-storage.

You can also create it without the console, by running the MinIO client inside the cluster:

kubectl run mc --rm -it --restart=Never -n minio --image=quay.io/minio/mc \
  --env="MC_HOST_local=http://<your-access-key>:<your-secret-key>@minio-api.minio.svc.cluster.local:9000" \
  -- --config-dir /tmp mb --ignore-existing local/mcd-agent-storage

3. Configure the agent

container:
  storageType: "S3_COMPATIBLE"
  storageBucketName: "mcd-agent-storage"
  storageEndpointUrl: "http://minio-api.minio.svc.cluster.local:9000"
  storageAccessKey: "<your-access-key>"
  storageSecretKey: "<your-secret-key>"

storageEndpointUrl is the service endpoint of your storage service β€” the in-cluster MinIO service above, or the URL of an external S3-compatible service (use https:// where the service supports it).

πŸ“˜

storageAccessKey and storageSecretKey end up in plain text in your values file. To keep them in a Kubernetes secret instead, create the secret:

kubectl create secret generic mcd-agent-storage-credentials -n mcd-agent \
  --from-literal=access-key=<your-access-key> \
  --from-literal=secret-key=<your-secret-key>

Then leave storageAccessKey and storageSecretKey unset, and pass the equivalent environment variables through container.extraEnv:

container:
  extraEnv:
    - name: MCD_STORAGE_ACCESS_KEY
      valueFrom:
        secretKeyRef:
          name: mcd-agent-storage-credentials
          key: access-key
    - name: MCD_STORAGE_SECRET_KEY
      valueFrom:
        secretKeyRef:
          name: mcd-agent-storage-credentials
          key: secret-key

Set them one way or the other β€” not both, or the pod ends up with duplicate environment variables.

Then verify storage access.

Verify storage access

Restart the agent and check for storage errors in the logs:

kubectl rollout restart deployment/mcd-agent-deployment -n mcd-agent
kubectl logs -n mcd-agent -l app=mcd-agent --tail=50

Then run the validations from Settings > Deployments: select your agent and click Validate. Storage access is one of the checks β€” a failure here means the agent reached Monte Carlo but could not read or write the bucket.


Did this page help you?