From bdd44cb4eeee904a23727c6ee90af669b86fbea7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zhuk Date: Fri, 24 Jul 2026 18:34:18 +0300 Subject: [PATCH] fix(s3): make AWS credentials optional so EKS pods can auth via IRSA The S3 and Secrets Manager clients always passed `credentials` explicitly and threw when static keys were absent. Passing `credentials` disables the AWS SDK default provider chain, so a pod holding an IRSA / Pod Identity role could never authenticate without static keys. Omit `credentials` when the keys are blank, letting the SDK resolve the pod role; keep passing them when both are present (static keys / local MinIO). Also fix the Rancher onboarding check, which required bucket + both keys and so never marked S3 configured on a keyless IRSA setup. It now needs only s3_bucket, treating a half-filled key pair as unconfigured. Co-Authored-By: Claude Opus 4.8 --- .../rancher/components/rancher/Provider.vue | 7 ++++--- api/src/slices/agent/file/data/file.gateway.ts | 14 +++++++------- .../slices/agent/secret/data/awsSecret.gateway.ts | 11 +++++------ .../agent/secret/data/fileSecret.gateway.ts | 14 +++++++------- .../templateFile/data/templateFile.gateway.ts | 14 +++++++------- api/src/slices/aws/s3/s3.repository.ts | 15 +++++++-------- .../evaluation/data/paddockReport.gateway.ts | 11 +++++------ api/src/slices/rancher/domain/rancher.service.ts | 14 ++++++++++---- .../browserState/data/browserState.gateway.ts | 14 +++++++------- .../slices/user/secret/data/awsSecret.gateway.ts | 12 ++++++------ .../slices/user/secret/data/fileSecret.gateway.ts | 14 +++++++------- 11 files changed, 72 insertions(+), 68 deletions(-) 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 } : {}), });