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
| Storage | storageType | Credentials | Best for |
|---|---|---|---|
| Amazon S3 | S3 | EKS Pod Identity or access keys | EKS, or any cluster with access to an AWS account |
| Azure Blob Storage | AZURE_BLOB | Workload Identity (managed identity) | AKS |
| Google Cloud Storage | GCS | Workload Identity | GKE |
| MinIO / S3-compatible | S3_COMPATIBLE | Access key and secret | On-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-2Block 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 keyThe 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:GenerateDataKeyto write objects (including multipart uploads) andkms:Decryptto read them back. The agent never copies or re-encrypts objects, sokms:Encryptandkms: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.
For clusters outside AWS, create an IAM user with the policy above and pass its credentials as environment variables. Store them in a Kubernetes secret rather than inline:
kubectl create secret generic mcd-agent-aws-credentials -n mcd-agent \
--from-literal=AWS_ACCESS_KEY_ID=<your-access-key-id> \
--from-literal=AWS_SECRET_ACCESS_KEY=<your-secret-access-key>container:
extraEnv:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: mcd-agent-aws-credentials
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: mcd-agent-aws-credentials
key: AWS_SECRET_ACCESS_KEY
- name: AWS_DEFAULT_REGION
value: "<your-bucket-region>"
container.storageAccessKeyandcontainer.storageSecretKeydo not apply here β those values only configureS3_COMPATIBLEstorage. WithstorageType: "S3"the agent reads the standardAWS_*environment variables.
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:
| Prefix | Expiration |
|---|---|
mcd/ | 90 days |
mcd/tmp | 2 days |
mcd/responses | 1 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 falseaz storage container create \
--name mcdstore \
--account-name <your-storage-account-name> \
--auth-mode loginBlob 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://AzureADTokenExchangeThe 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 changednamespacein yourvalues.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 tsvBoth the service account annotation and the
azure.workload.identity/usepod label are required. The label is what tells the workload identity webhook to inject the projected token and theAZURE_CLIENT_IDenvironment 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 variableLabel 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, andAZURE_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:
| Prefix | Expiration |
|---|---|
mcdstore/mcd | 90 days |
mcdstore/mcd/tmp | 2 days |
mcdstore/mcd/responses | 1 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-preventionCloud 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.legacyBucketReaderBoth roles are required.
roles/storage.objectAdmincovers reading, writing, and deleting objects, but grants nothing at the bucket level. The agent resolves the bucket before every operation, so withoutstorage.buckets.geteven 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 onlystorage.buckets.getworks 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 changednamespacein yourvalues.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:
| Prefix | Expiration |
|---|---|
mcd/ | 90 days |
mcd/tmp | 2 days |
mcd/responses | 1 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.yamlThis manifest is a reference for development and testing: it runs MinIO with the default
minioadmincredentials 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:9001Open 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-storage3. 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).
storageAccessKeyandstorageSecretKeyend 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
storageAccessKeyandstorageSecretKeyunset, and pass the equivalent environment variables throughcontainer.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-keySet 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=50Then 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.
Updated about 3 hours ago
