From a436755d8d2c87e3ac72ff66e85df104e4f2d911 Mon Sep 17 00:00:00 2001 From: Vasilis Vlasopoulos Date: Thu, 19 Mar 2026 09:31:36 +0200 Subject: [PATCH 1/2] feat: Make credentials provider configurable --- CHANGELOG.md | 4 ++ README.md | 23 ++++++++ config/rds-iam-auth.php | 29 +++++++++ src/RdsAuthTokenProvider.php | 21 ++++++- tests/RdsAuthTokenProviderTest.php | 94 ++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e43fb..8d6a408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Configurable AWS credential provider via `credential_provider` config / `RDS_IAM_CREDENTIAL_PROVIDER` env var — supports `default`, `environment`, `ecs` (Pod Identity / ECS task role), `web_identity` (IRSA), `instance_profile`, `sso`, and `ini` + ## [1.0.2] - 2026-03-06 ### Fixed diff --git a/README.md b/README.md index 13ae26e..474feb5 100644 --- a/README.md +++ b/README.md @@ -99,11 +99,32 @@ The package config (`config/rds-iam-auth.php`): | Key | Default | Description | |---|---|---| | `region` | `AWS_DEFAULT_REGION` / `AWS_REGION` env | Fallback region when not set on connection | +| `credential_provider` | `default` | AWS credential provider to use for token signing. Override with `RDS_IAM_CREDENTIAL_PROVIDER` env. See [Credential Provider](#credential-provider) below. | | `cache_store` | `null` | Laravel cache store for token caching when APCu is unavailable. Use `file`, `redis`, `memcached`, etc. **Never** `database` or `dynamodb`. | | `cache_ttl` | `600` (10 min) | Cache TTL in seconds (APCu and Laravel cache). Tokens are valid for 15 min. | | `pgsql_sslmode` | `verify-full` | SSL mode for PostgreSQL IAM connections. Overrides connection-level `sslmode` to prevent accidental downgrade. Override with `RDS_IAM_PGSQL_SSLMODE` env. | | `ssl_ca_path` | Bundled `global-bundle.pem` | Path to the RDS CA bundle. Override with `RDS_IAM_SSL_CA_PATH` env. | +### Credential Provider + +By default, the package uses the AWS SDK's default credential chain, which tries multiple sources in order (env vars, IRSA, Pod Identity, instance profile, etc.). If your pod has multiple credential sources and you need to force a specific one, set `credential_provider`: + +```env +RDS_IAM_CREDENTIAL_PROVIDER=ecs +``` + +| Value | Source | Use case | +|---|---|---| +| `default` | Full AWS SDK credential chain | Most environments (default) | +| `environment` | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env vars only | When you only want static env var credentials | +| `ecs` | ECS container credentials endpoint | EKS Pod Identity or ECS task roles | +| `web_identity` | `AWS_WEB_IDENTITY_TOKEN_FILE` (STS AssumeRoleWithWebIdentity) | IRSA (IAM Roles for Service Accounts) | +| `instance_profile` | EC2 instance metadata (IMDSv2) | EC2 instances with an attached IAM role | +| `sso` | AWS SSO (`~/.aws/config` + SSO cache) | Local development with `aws sso login` | +| `ini` | Shared credentials file (`~/.aws/credentials`) | Local development or CI with credential files | + +This is useful when a pod has both AWS key env vars (e.g. for S3 access) and Pod Identity (for RDS IAM auth) — set `credential_provider` to `ecs` to skip the env vars and use Pod Identity. + ## RDS IAM Database User Setup ### MySQL / MariaDB @@ -130,6 +151,8 @@ GRANT rds_iam TO app_user; The AWS SDK default credential chain picks up Pod Identity credentials automatically. No code changes needed beyond enabling `use_iam_auth`. +If your pod also has `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env vars (e.g. for other AWS services), set `RDS_IAM_CREDENTIAL_PROVIDER=ecs` to force Pod Identity for RDS auth. + ## Token Caching IAM auth tokens are valid for 15 minutes. The package caches them to avoid per-request STS calls: diff --git a/config/rds-iam-auth.php b/config/rds-iam-auth.php index 5a95831..d97b4c4 100644 --- a/config/rds-iam-auth.php +++ b/config/rds-iam-auth.php @@ -15,6 +15,35 @@ 'region' => env('AWS_DEFAULT_REGION', env('AWS_REGION', 'us-east-1')), + /* + |-------------------------------------------------------------------------- + | AWS Credential Provider + |-------------------------------------------------------------------------- + | + | The credential provider to use when generating IAM auth tokens. This + | controls how the package obtains AWS credentials for signing tokens. + | + | By default, the AWS SDK credential chain is used, which tries env vars, + | IRSA, Pod Identity, instance profile, etc. in order. Set this to force + | a specific provider — useful when multiple credential sources exist in + | the pod and you need to pick one (e.g. Pod Identity over env vars). + | + | Supported: 'default', 'environment', 'ecs', 'web_identity', + | 'instance_profile', 'sso', 'ini' + | + | default - Full AWS SDK credential chain (env, ini, IRSA, + | Pod Identity, instance profile, etc.) + | environment - Only AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY + | ecs - ECS container credentials / EKS Pod Identity + | web_identity - IRSA (IAM Roles for Service Accounts) + | instance_profile - EC2 instance metadata (IMDSv2) + | sso - AWS SSO (useful for local development) + | ini - Shared credentials file (~/.aws/credentials) + | + */ + + 'credential_provider' => env('RDS_IAM_CREDENTIAL_PROVIDER', 'default'), + /* |-------------------------------------------------------------------------- | Token Cache Store diff --git a/src/RdsAuthTokenProvider.php b/src/RdsAuthTokenProvider.php index f29beba..d8c74b6 100644 --- a/src/RdsAuthTokenProvider.php +++ b/src/RdsAuthTokenProvider.php @@ -63,6 +63,25 @@ private function generateToken(string $host, int $port, string $username, string protected function createTokenGenerator(): AuthTokenGenerator { - return new AuthTokenGenerator(CredentialProvider::defaultProvider()); + return new AuthTokenGenerator($this->resolveCredentialProvider()); + } + + private function resolveCredentialProvider(): callable + { + $provider = config('rds-iam-auth.credential_provider', 'default'); + + return match ($provider) { + 'default' => CredentialProvider::defaultProvider(), + 'environment' => CredentialProvider::env(), + 'ecs' => CredentialProvider::ecsCredentials(), + 'web_identity' => CredentialProvider::assumeRoleWithWebIdentityCredentialProvider(), + 'instance_profile' => CredentialProvider::instanceProfile(), + 'sso' => CredentialProvider::sso(), + 'ini' => CredentialProvider::ini(), + default => throw new RuntimeException( + "Unsupported RDS IAM credential provider '{$provider}'. " + ."Supported values: default, environment, ecs, web_identity, instance_profile, sso, ini." + ), + }; } } diff --git a/tests/RdsAuthTokenProviderTest.php b/tests/RdsAuthTokenProviderTest.php index cd580d0..f1bfc97 100644 --- a/tests/RdsAuthTokenProviderTest.php +++ b/tests/RdsAuthTokenProviderTest.php @@ -2,6 +2,7 @@ namespace Hackthebox\RdsIamAuth\Tests; +use Aws\Credentials\CredentialProvider; use Aws\Rds\AuthTokenGenerator; use Hackthebox\RdsIamAuth\RdsAuthTokenProvider; use Hackthebox\RdsIamAuth\RdsIamAuthServiceProvider; @@ -111,6 +112,99 @@ public function test_throws_on_dynamodb_cache_store(): void $provider->getToken('my-rds.cluster.us-east-1.rds.amazonaws.com', 3306, 'app_user', 'us-east-1'); } + public function test_uses_default_credential_provider_by_default(): void + { + config(['rds-iam-auth.credential_provider' => 'default']); + + $provider = new class extends RdsAuthTokenProvider + { + public function exposeCredentialProvider(): callable + { + $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); + + return $reflection->invoke($this); + } + }; + + $result = $provider->exposeCredentialProvider(); + $this->assertIsCallable($result); + } + + public function test_resolves_environment_credential_provider(): void + { + config(['rds-iam-auth.credential_provider' => 'environment']); + + $provider = new class extends RdsAuthTokenProvider + { + public function exposeCredentialProvider(): callable + { + $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); + + return $reflection->invoke($this); + } + }; + + $result = $provider->exposeCredentialProvider(); + $this->assertIsCallable($result); + } + + /** + * @dataProvider validCredentialProviderNames + */ + public function test_resolves_all_supported_credential_providers(string $name): void + { + config(['rds-iam-auth.credential_provider' => $name]); + + $provider = new class extends RdsAuthTokenProvider + { + public function exposeCredentialProvider(): callable + { + $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); + + return $reflection->invoke($this); + } + }; + + $result = $provider->exposeCredentialProvider(); + $this->assertIsCallable($result); + } + + public static function validCredentialProviderNames(): array + { + return [ + 'default' => ['default'], + 'environment' => ['environment'], + 'ecs' => ['ecs'], + 'web_identity' => ['web_identity'], + 'instance_profile' => ['instance_profile'], + 'sso' => ['sso'], + 'ini' => ['ini'], + ]; + } + + public function test_throws_on_unsupported_credential_provider(): void + { + config(['rds-iam-auth.credential_provider' => 'banana']); + + $provider = $this->mockProvider(); + + // Override the mock to call the real resolveCredentialProvider + $realProvider = new class extends RdsAuthTokenProvider + { + public function callResolve(): callable + { + $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); + + return $reflection->invoke($this); + } + }; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Unsupported RDS IAM credential provider 'banana'"); + + $realProvider->callResolve(); + } + public function test_wraps_token_generation_failure_with_context(): void { $generator = Mockery::mock(AuthTokenGenerator::class); From 8a3651e0c9ba296e95b9e84d978c06445bc11d91 Mon Sep 17 00:00:00 2001 From: Vasilis Vlasopoulos Date: Thu, 19 Mar 2026 19:12:16 +0200 Subject: [PATCH 2/2] refactor: clean up credential provider tests and visibility --- README.md | 24 +----------- config/rds-iam-auth.php | 20 ++-------- src/RdsAuthTokenProvider.php | 8 ++-- tests/RdsAuthTokenProviderTest.php | 61 +++--------------------------- 4 files changed, 16 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 474feb5..9953ba5 100644 --- a/README.md +++ b/README.md @@ -99,32 +99,12 @@ The package config (`config/rds-iam-auth.php`): | Key | Default | Description | |---|---|---| | `region` | `AWS_DEFAULT_REGION` / `AWS_REGION` env | Fallback region when not set on connection | -| `credential_provider` | `default` | AWS credential provider to use for token signing. Override with `RDS_IAM_CREDENTIAL_PROVIDER` env. See [Credential Provider](#credential-provider) below. | +| `credential_provider` | `default` | AWS credential provider for token signing. Override with `RDS_IAM_CREDENTIAL_PROVIDER` env. Supported: `default`, `environment`, `ecs`, `web_identity`, `instance_profile`, `sso`, `ini`. | | `cache_store` | `null` | Laravel cache store for token caching when APCu is unavailable. Use `file`, `redis`, `memcached`, etc. **Never** `database` or `dynamodb`. | | `cache_ttl` | `600` (10 min) | Cache TTL in seconds (APCu and Laravel cache). Tokens are valid for 15 min. | | `pgsql_sslmode` | `verify-full` | SSL mode for PostgreSQL IAM connections. Overrides connection-level `sslmode` to prevent accidental downgrade. Override with `RDS_IAM_PGSQL_SSLMODE` env. | | `ssl_ca_path` | Bundled `global-bundle.pem` | Path to the RDS CA bundle. Override with `RDS_IAM_SSL_CA_PATH` env. | -### Credential Provider - -By default, the package uses the AWS SDK's default credential chain, which tries multiple sources in order (env vars, IRSA, Pod Identity, instance profile, etc.). If your pod has multiple credential sources and you need to force a specific one, set `credential_provider`: - -```env -RDS_IAM_CREDENTIAL_PROVIDER=ecs -``` - -| Value | Source | Use case | -|---|---|---| -| `default` | Full AWS SDK credential chain | Most environments (default) | -| `environment` | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env vars only | When you only want static env var credentials | -| `ecs` | ECS container credentials endpoint | EKS Pod Identity or ECS task roles | -| `web_identity` | `AWS_WEB_IDENTITY_TOKEN_FILE` (STS AssumeRoleWithWebIdentity) | IRSA (IAM Roles for Service Accounts) | -| `instance_profile` | EC2 instance metadata (IMDSv2) | EC2 instances with an attached IAM role | -| `sso` | AWS SSO (`~/.aws/config` + SSO cache) | Local development with `aws sso login` | -| `ini` | Shared credentials file (`~/.aws/credentials`) | Local development or CI with credential files | - -This is useful when a pod has both AWS key env vars (e.g. for S3 access) and Pod Identity (for RDS IAM auth) — set `credential_provider` to `ecs` to skip the env vars and use Pod Identity. - ## RDS IAM Database User Setup ### MySQL / MariaDB @@ -151,7 +131,7 @@ GRANT rds_iam TO app_user; The AWS SDK default credential chain picks up Pod Identity credentials automatically. No code changes needed beyond enabling `use_iam_auth`. -If your pod also has `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env vars (e.g. for other AWS services), set `RDS_IAM_CREDENTIAL_PROVIDER=ecs` to force Pod Identity for RDS auth. +*The option to force a specific credential provider exists via the `credential_provider` config option.* ## Token Caching diff --git a/config/rds-iam-auth.php b/config/rds-iam-auth.php index d97b4c4..acf8abf 100644 --- a/config/rds-iam-auth.php +++ b/config/rds-iam-auth.php @@ -20,26 +20,14 @@ | AWS Credential Provider |-------------------------------------------------------------------------- | - | The credential provider to use when generating IAM auth tokens. This - | controls how the package obtains AWS credentials for signing tokens. - | - | By default, the AWS SDK credential chain is used, which tries env vars, - | IRSA, Pod Identity, instance profile, etc. in order. Set this to force - | a specific provider — useful when multiple credential sources exist in - | the pod and you need to pick one (e.g. Pod Identity over env vars). + | The AWS credential provider used to sign IAM auth tokens. The default + | uses the full SDK credential chain. Override to force a specific + | provider when multiple credential sources exist (e.g. Pod Identity + | over env vars). | | Supported: 'default', 'environment', 'ecs', 'web_identity', | 'instance_profile', 'sso', 'ini' | - | default - Full AWS SDK credential chain (env, ini, IRSA, - | Pod Identity, instance profile, etc.) - | environment - Only AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY - | ecs - ECS container credentials / EKS Pod Identity - | web_identity - IRSA (IAM Roles for Service Accounts) - | instance_profile - EC2 instance metadata (IMDSv2) - | sso - AWS SSO (useful for local development) - | ini - Shared credentials file (~/.aws/credentials) - | */ 'credential_provider' => env('RDS_IAM_CREDENTIAL_PROVIDER', 'default'), diff --git a/src/RdsAuthTokenProvider.php b/src/RdsAuthTokenProvider.php index d8c74b6..785c13c 100644 --- a/src/RdsAuthTokenProvider.php +++ b/src/RdsAuthTokenProvider.php @@ -66,11 +66,11 @@ protected function createTokenGenerator(): AuthTokenGenerator return new AuthTokenGenerator($this->resolveCredentialProvider()); } - private function resolveCredentialProvider(): callable + protected function resolveCredentialProvider(): callable { - $provider = config('rds-iam-auth.credential_provider', 'default'); + $name = config('rds-iam-auth.credential_provider', 'default'); - return match ($provider) { + return match ($name) { 'default' => CredentialProvider::defaultProvider(), 'environment' => CredentialProvider::env(), 'ecs' => CredentialProvider::ecsCredentials(), @@ -79,7 +79,7 @@ private function resolveCredentialProvider(): callable 'sso' => CredentialProvider::sso(), 'ini' => CredentialProvider::ini(), default => throw new RuntimeException( - "Unsupported RDS IAM credential provider '{$provider}'. " + "Unsupported RDS IAM credential provider '{$name}'. " ."Supported values: default, environment, ecs, web_identity, instance_profile, sso, ini." ), }; diff --git a/tests/RdsAuthTokenProviderTest.php b/tests/RdsAuthTokenProviderTest.php index f1bfc97..800324e 100644 --- a/tests/RdsAuthTokenProviderTest.php +++ b/tests/RdsAuthTokenProviderTest.php @@ -2,7 +2,6 @@ namespace Hackthebox\RdsIamAuth\Tests; -use Aws\Credentials\CredentialProvider; use Aws\Rds\AuthTokenGenerator; use Hackthebox\RdsIamAuth\RdsAuthTokenProvider; use Hackthebox\RdsIamAuth\RdsIamAuthServiceProvider; @@ -112,40 +111,19 @@ public function test_throws_on_dynamodb_cache_store(): void $provider->getToken('my-rds.cluster.us-east-1.rds.amazonaws.com', 3306, 'app_user', 'us-east-1'); } - public function test_uses_default_credential_provider_by_default(): void + private function resolveCredentialProviderFor(string $name): callable { - config(['rds-iam-auth.credential_provider' => 'default']); - - $provider = new class extends RdsAuthTokenProvider - { - public function exposeCredentialProvider(): callable - { - $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); - - return $reflection->invoke($this); - } - }; - - $result = $provider->exposeCredentialProvider(); - $this->assertIsCallable($result); - } - - public function test_resolves_environment_credential_provider(): void - { - config(['rds-iam-auth.credential_provider' => 'environment']); + config(['rds-iam-auth.credential_provider' => $name]); $provider = new class extends RdsAuthTokenProvider { public function exposeCredentialProvider(): callable { - $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); - - return $reflection->invoke($this); + return $this->resolveCredentialProvider(); } }; - $result = $provider->exposeCredentialProvider(); - $this->assertIsCallable($result); + return $provider->exposeCredentialProvider(); } /** @@ -153,19 +131,7 @@ public function exposeCredentialProvider(): callable */ public function test_resolves_all_supported_credential_providers(string $name): void { - config(['rds-iam-auth.credential_provider' => $name]); - - $provider = new class extends RdsAuthTokenProvider - { - public function exposeCredentialProvider(): callable - { - $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); - - return $reflection->invoke($this); - } - }; - - $result = $provider->exposeCredentialProvider(); + $result = $this->resolveCredentialProviderFor($name); $this->assertIsCallable($result); } @@ -184,25 +150,10 @@ public static function validCredentialProviderNames(): array public function test_throws_on_unsupported_credential_provider(): void { - config(['rds-iam-auth.credential_provider' => 'banana']); - - $provider = $this->mockProvider(); - - // Override the mock to call the real resolveCredentialProvider - $realProvider = new class extends RdsAuthTokenProvider - { - public function callResolve(): callable - { - $reflection = new \ReflectionMethod($this, 'resolveCredentialProvider'); - - return $reflection->invoke($this); - } - }; - $this->expectException(RuntimeException::class); $this->expectExceptionMessage("Unsupported RDS IAM credential provider 'banana'"); - $realProvider->callResolve(); + $this->resolveCredentialProviderFor('banana'); } public function test_wraps_token_generation_failure_with_context(): void