diff --git a/admin/slices/rancher/components/rancher/Provider.vue b/admin/slices/rancher/components/rancher/Provider.vue
index 5d54a5fe..7f8f1a8b 100644
--- a/admin/slices/rancher/components/rancher/Provider.vue
+++ b/admin/slices/rancher/components/rancher/Provider.vue
@@ -390,9 +390,10 @@ async function onDeploy() {
Configure S3 storage
Required to seed template files and persist agent state. Set
- s3_bucket, aws_access_key_id and
- aws_secret_access_key. Local dev: use the included
- MinIO defaults.
+ s3_bucket (and optionally aws_region).
+ aws_access_key_id / aws_secret_access_key
+ are optional — on EKS the pod's IRSA role is used automatically.
+ Local dev: use the included MinIO defaults.
Done
diff --git a/api/src/slices/agent/file/data/file.gateway.ts b/api/src/slices/agent/file/data/file.gateway.ts
index 33c88751..9cd26e37 100644
--- a/api/src/slices/agent/file/data/file.gateway.ts
+++ b/api/src/slices/agent/file/data/file.gateway.ts
@@ -666,15 +666,15 @@ export class S3FileGateway extends IFileGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
-
+ // Credentials are optional: when both are present we pass them explicitly
+ // (static keys / local MinIO). When blank we OMIT `credentials` so the AWS
+ // SDK default provider chain resolves them — this is what lets an EKS pod
+ // authenticate through its IRSA / Pod Identity role with no static keys.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});
diff --git a/api/src/slices/agent/secret/data/awsSecret.gateway.ts b/api/src/slices/agent/secret/data/awsSecret.gateway.ts
index 27edfc78..cbd3f0b1 100644
--- a/api/src/slices/agent/secret/data/awsSecret.gateway.ts
+++ b/api/src/slices/agent/secret/data/awsSecret.gateway.ts
@@ -157,20 +157,19 @@ export class AwsSecretGateway {
get('aws_secret_prefix'),
]);
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
if (!prefix) {
throw new BadRequestException(
'AWS Secrets Manager prefix is not configured (settings → integrations → aws_secret_prefix)',
);
}
+ // Credentials optional — omit `credentials` when blank so the SDK default
+ // provider chain (IRSA / Pod Identity on EKS) supplies them.
const client = new SecretsManagerClient({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
});
return { client, prefix: prefix.replace(/\/+$/, '') };
diff --git a/api/src/slices/agent/secret/data/fileSecret.gateway.ts b/api/src/slices/agent/secret/data/fileSecret.gateway.ts
index 58fba832..8cb4d8a2 100644
--- a/api/src/slices/agent/secret/data/fileSecret.gateway.ts
+++ b/api/src/slices/agent/secret/data/fileSecret.gateway.ts
@@ -150,15 +150,15 @@ export class FileSecretGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
-
+ // Credentials are optional: when both are present we pass them explicitly
+ // (static keys / local MinIO). When blank we OMIT `credentials` so the AWS
+ // SDK default provider chain resolves them — this is what lets an EKS pod
+ // authenticate through its IRSA / Pod Identity role with no static keys.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});
diff --git a/api/src/slices/agent/templateFile/data/templateFile.gateway.ts b/api/src/slices/agent/templateFile/data/templateFile.gateway.ts
index b307d069..6f67326f 100644
--- a/api/src/slices/agent/templateFile/data/templateFile.gateway.ts
+++ b/api/src/slices/agent/templateFile/data/templateFile.gateway.ts
@@ -249,15 +249,15 @@ export class S3TemplateFileGateway extends ITemplateFileGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
-
+ // Credentials are optional: when both are present we pass them explicitly
+ // (static keys / local MinIO). When blank we OMIT `credentials` so the AWS
+ // SDK default provider chain resolves them — this is what lets an EKS pod
+ // authenticate through its IRSA / Pod Identity role with no static keys.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});
diff --git a/api/src/slices/aws/s3/s3.repository.ts b/api/src/slices/aws/s3/s3.repository.ts
index d43f4be4..37ea6d78 100644
--- a/api/src/slices/aws/s3/s3.repository.ts
+++ b/api/src/slices/aws/s3/s3.repository.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import {
DeleteObjectCommand,
GetObjectCommand,
@@ -80,16 +80,15 @@ export class S3Repository {
get('s3_secret_key'),
]);
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'S3 is not configured (settings → integrations → s3_access_key, s3_secret_key)',
- );
- }
-
+ // Credentials optional — when both are present we use them explicitly
+ // (static keys / MinIO); when blank we omit `credentials` so the SDK
+ // default provider chain (IRSA / Pod Identity on EKS) supplies them.
this.client = new S3Client({
endpoint: endpoint || undefined,
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
forcePathStyle: Boolean(endpoint),
});
return this.client;
diff --git a/api/src/slices/paddock/evaluation/data/paddockReport.gateway.ts b/api/src/slices/paddock/evaluation/data/paddockReport.gateway.ts
index a24f4c6f..d3ed3485 100644
--- a/api/src/slices/paddock/evaluation/data/paddockReport.gateway.ts
+++ b/api/src/slices/paddock/evaluation/data/paddockReport.gateway.ts
@@ -190,14 +190,13 @@ export class PaddockReportGateway extends IPaddockReportGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
+ // Credentials optional — omit `credentials` when blank so the SDK default
+ // provider chain (IRSA / Pod Identity on EKS) supplies them.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});
return { client, bucket };
diff --git a/api/src/slices/rancher/domain/rancher.service.ts b/api/src/slices/rancher/domain/rancher.service.ts
index a59351a4..498a276e 100644
--- a/api/src/slices/rancher/domain/rancher.service.ts
+++ b/api/src/slices/rancher/domain/rancher.service.ts
@@ -72,9 +72,13 @@ export class RancherService implements OnApplicationBootstrap {
};
}
- // S3 is "configured" when bucket + AWS credentials are all present in
- // settings → integrations. Endpoint/region are optional (region falls
- // back to us-east-1; endpoint blank means real AWS).
+ // S3 is "configured" once a bucket is set. AWS credentials are OPTIONAL:
+ // on EKS the pod authenticates through its IRSA / Pod Identity role via the
+ // SDK's default provider chain, so no static keys are needed. When keys are
+ // supplied (static keys / local MinIO) they must come as a pair — a lone
+ // key or secret is a half-filled, unusable state, so we don't count it.
+ // Endpoint/region stay optional (region falls back to us-east-1; blank
+ // endpoint means real AWS).
private async checkS3Configured(): Promise {
const get = async (name: string): Promise => {
const s = await this.settingGateway.findByKey('integrations', name);
@@ -85,7 +89,9 @@ export class RancherService implements OnApplicationBootstrap {
get('aws_access_key_id'),
get('aws_secret_access_key'),
]);
- return !!(bucket && key && secret);
+ // both-or-neither: reject a partially-entered credential pair
+ const credsConsistent = !!key === !!secret;
+ return !!bucket && credsConsistent;
}
// Idempotent — returns the existing Rancher template if one exists,
diff --git a/api/src/slices/user/browserState/data/browserState.gateway.ts b/api/src/slices/user/browserState/data/browserState.gateway.ts
index 6744489e..e2953158 100644
--- a/api/src/slices/user/browserState/data/browserState.gateway.ts
+++ b/api/src/slices/user/browserState/data/browserState.gateway.ts
@@ -143,15 +143,15 @@ export class UserBrowserStateGateway extends IUserBrowserStateGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
-
+ // Credentials are optional: when both are present we pass them explicitly
+ // (static keys / local MinIO). When blank we OMIT `credentials` so the AWS
+ // SDK default provider chain resolves them — this is what lets an EKS pod
+ // authenticate through its IRSA / Pod Identity role with no static keys.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});
diff --git a/api/src/slices/user/secret/data/awsSecret.gateway.ts b/api/src/slices/user/secret/data/awsSecret.gateway.ts
index 0ffcf505..c0f66448 100644
--- a/api/src/slices/user/secret/data/awsSecret.gateway.ts
+++ b/api/src/slices/user/secret/data/awsSecret.gateway.ts
@@ -160,20 +160,20 @@ export class AwsUserSecretGateway {
get('aws_user_secret_prefix'),
]);
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
if (!prefix) {
throw new BadRequestException(
'AWS Secrets Manager user prefix is not configured (settings → integrations → aws_user_secret_prefix)',
);
}
+ // Credentials are optional: when both are present we pass them explicitly,
+ // otherwise we omit `credentials` so the SDK default provider chain (IRSA /
+ // Pod Identity on EKS) supplies them.
const client = new SecretsManagerClient({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
});
return { client, prefix: prefix.replace(/\/+$/, '') };
diff --git a/api/src/slices/user/secret/data/fileSecret.gateway.ts b/api/src/slices/user/secret/data/fileSecret.gateway.ts
index 2fdbe79c..cd61f609 100644
--- a/api/src/slices/user/secret/data/fileSecret.gateway.ts
+++ b/api/src/slices/user/secret/data/fileSecret.gateway.ts
@@ -136,15 +136,15 @@ export class FileUserSecretGateway {
'S3 bucket is not configured (settings → integrations → s3_bucket)',
);
}
- if (!accessKeyId || !secretAccessKey) {
- throw new BadRequestException(
- 'AWS credentials are not configured (settings → integrations)',
- );
- }
-
+ // Credentials are optional: when both are present we pass them explicitly
+ // (static keys / local MinIO). When blank we OMIT `credentials` so the AWS
+ // SDK default provider chain resolves them — this is what lets an EKS pod
+ // authenticate through its IRSA / Pod Identity role with no static keys.
const client = new S3Client({
region: region || 'us-east-1',
- credentials: { accessKeyId, secretAccessKey },
+ ...(accessKeyId && secretAccessKey
+ ? { credentials: { accessKeyId, secretAccessKey } }
+ : {}),
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
});