AWS IAM authentication for Laravel: RDS database connections and SDK credential caching. Designed for EKS workloads using Pod Identity or IRSA (no sidecars, no static credentials).
- RDS IAM Auth — overrides Laravel's MySQL, MariaDB, and PostgreSQL connectors to generate short-lived IAM auth tokens via the AWS SDK when
use_iam_authis enabled on a connection. - AWS Credential Caching — caches resolved AWS SDK credentials across PHP-FPM requests (APCu-first), benefiting all AWS SDK calls (S3, SQS, SES, etc.).
This package overrides Laravel's database connectors for MySQL, MariaDB, and PostgreSQL. When use_iam_auth is enabled on a connection, the connector generates a short-lived IAM auth token (via the AWS SDK) and uses it as the database password. The token is generated fresh on each connection; the AWS SDK credential resolution that backs the token signature is cached across PHP-FPM requests (APCu-first).
The package does not introduce a new database driver. Laravel's MySqlConnection, MariaDbConnection, and PostgresConnection are used as-is.
The package wires config('aws.credentials') to a custom CachedCredentialProvider so that the AWS SDK singleton (app('aws')) and any client created from it share cached credentials across PHP-FPM requests. Other-package customizations on the SDK singleton are preserved (the singleton is not rebuilt).
Supports MySQL, MariaDB, and PostgreSQL. All three drivers share the same connector trait (InjectsIamToken), so the auth-token signing, cache-defense, and auth-rejection-detection behavior is identical across them.
- PHP >= 8.2
- Laravel 12 or 13
- aws/aws-sdk-php-laravel >= 3.7 and aws/aws-sdk-php >= 3.249 (both installed automatically)
- APCu extension (recommended for production — caches resolved AWS credentials across FPM requests)
- RDS instance with IAM authentication enabled
- SSL CA bundle (bundled — override via
IAM_AUTH_SSL_CA_PATHenv if needed)
composer require hackthebox/laravel-iam-authThe service provider is auto-discovered. To publish the config:
php artisan vendor:publish --tag=iam-auth-configAdd use_iam_auth and region to your connection in config/database.php:
MySQL / MariaDB:
'mysql' => [
'driver' => 'mysql', // or 'mariadb'
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'use_iam_auth' => env('DB_USE_IAM_AUTH', false),
'region' => env('AWS_DEFAULT_REGION', 'eu-central-1'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],PostgreSQL:
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'use_iam_auth' => env('DB_USE_IAM_AUTH', false),
'region' => env('AWS_DEFAULT_REGION', 'eu-central-1'),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
],When use_iam_auth is false, the connector behaves identically to Laravel's default — password is used as-is.
Production (EKS with Pod Identity):
DB_USE_IAM_AUTH=true
DB_PASSWORD=
AWS_DEFAULT_REGION=eu-central-1Local development:
DB_USE_IAM_AUTH=false
DB_PASSWORD=secretThe package config (config/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 for all SDK operations (S3, SQS, RDS, etc.). Override with IAM_AUTH_CREDENTIAL_PROVIDER env. Supported: default, environment, ecs, web_identity, instance_profile, sso, ini. |
cache_store |
null |
Laravel cache store for caching resolved AWS credentials when APCu is unavailable. Use file, redis, memcached, etc. Never database or dynamodb. Override with IAM_AUTH_CACHE_STORE env. |
debug |
false |
When true, emits a verbose iam-auth.token-access debug log on every getToken call, including host, port, username, region, force_fresh, and access_key_prefix (first 8 characters of the AccessKeyId). High log volume; intended for short investigation soaks only. Override with IAM_AUTH_DEBUG env. |
pgsql_sslmode |
verify-full |
SSL mode for PostgreSQL IAM connections. Override with IAM_AUTH_PGSQL_SSLMODE env. |
ssl_ca_path |
Bundled global-bundle.pem |
Path to the RDS CA bundle. Override with IAM_AUTH_SSL_CA_PATH env. |
CREATE USER 'app_user' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
GRANT ALL ON mydb.* TO 'app_user'@'%';CREATE USER app_user WITH LOGIN;
GRANT rds_iam TO app_user;- Install the Pod Identity Agent addon
- Create an IAM role with a trust policy for
pods.eks.amazonaws.com - Attach an IAM policy allowing
rds-db:connect - Create a pod identity association for your namespace/service account
- Restart your pods
The AWS SDK default credential chain picks up Pod Identity credentials automatically. No code changes needed beyond enabling use_iam_auth.
The option to force a specific credential provider exists via the credential_provider config option.
IAM auth tokens are valid for 15 minutes and are generated fresh on each database connection. Because RDS token signing is local HMAC work using already-cached credentials, the cost per connection is negligible.
Bundled CA certificate: The package includes the AWS RDS global CA bundle. This certificate bundle may become stale over time. If you encounter SSL verification errors, download the latest bundle from AWS and set IAM_AUTH_SSL_CA_PATH to point to it.
Expired-on-arrival credentials throw. When the AWS SDK credential provider hands back a Credentials object that is already past its expiration, CachedCredentialProvider emits a Log::warning under the channel iam-auth.credentials-expired-on-arrival and throws a RuntimeException before the credentials reach the SigV4 signer. The same guard also lives in AwsCredentialCacheStore::set() as defense-in-depth against any external code writing expired credentials directly into the cache.
Cache-backend resilience. All three cache operations (get, set, remove) treat Laravel cache backend failures as best-effort: transient errors (e.g. Redis temporarily unavailable) are swallowed and logged under iam-auth.cache-store-write-failed, so a single backend blip cannot fail user requests even when credentials were resolved successfully. Misconfiguration (an unsafe cache_store like database or dynamodb) still throws loudly via assertSafeCacheStore before any operation.
Auth rejection triggers a single retry with fresh credentials. Any auth rejection from RDS that reaches the connector (SQLSTATE class 28 for PostgreSQL, native code 1045 for MySQL/MariaDB) triggers the following recovery sequence:
- Log
iam-auth.rds-auth-rejected(warning) with the current credential cache snapshot. - Invalidate
CachedCredentialProvider's in-process memo and the cross-request store. - Re-sign the RDS token against freshly resolved credentials, which also repopulates the store so sibling workers benefit from the rotation.
- Retry the connection once.
If the retry also fails with an auth rejection, iam-auth.rds-auth-rejected-retry-failed is logged and the exception is re-thrown. This covers the common case of a credential rotation window landing between the credential cache write and the DB connect.
The credential snapshot in the warning reflects the cache state at the moment the warning fires. On a busy multi-worker pod a concurrent refresh between signing and rejection can produce a snapshot of post-rotation credentials; this is cosmetic and does not affect the retry logic.
Performance note. If you disable credential caching (no APCu, no cache_store), every DB connection will re-invoke the SDK credential chain. Keep credential caching enabled for IAM-auth workloads.
Set IAM_AUTH_DEBUG=true in your environment to enable verbose per-getToken logging. The package emits an iam-auth.token-access debug line on every call:
host,port,username,region: connection target.force_fresh: whether the call bypassed the credential cache (i.e., this is a retry after auth rejection).access_key_prefix: first 8 characters of the AccessKeyId used to sign the token (never the full credential).
The iam-auth.rds-auth-rejected and iam-auth.rds-auth-rejected-retry-failed warnings are unconditional and fire regardless of IAM_AUTH_DEBUG.
This log volume is too high for steady-state production. Enable for short investigation soaks only. No credential secrets are ever logged.
When using IAM roles (IRSA, Pod Identity, instance profiles), the AWS SDK resolves credentials via network calls to STS or IMDS on every PHP-FPM request. Under high traffic this adds latency and can hit rate limits.
This package caches resolved AWS SDK credentials across requests, benefiting all AWS SDK calls made by your application (S3, SQS, SES, etc.), not just RDS token generation.
The cache_store setting controls AWS credential caching. APCu is always preferred when available.
Proactive refresh window. CachedCredentialProvider refreshes credentials 60 seconds before their reported expiration to avoid handing the SigV4 signer credentials that may expire mid-request. The constant (CachedCredentialProvider::REFRESH_WINDOW = 60) matches the AWS SDK's own CredentialProvider::memoize() default. This window is not configurable in v3; the load impact is bounded (one extra fetch per credentials cycle, where the cycle is hours long for STS sessions).
Important: Credential caching only applies to AWS clients created through the SDK singleton (e.g. app('aws')->createS3()). Clients instantiated directly (new S3Client([...])) bypass the singleton and do not benefit from cached credentials. Always resolve clients via the container:
// Correct: uses cached credentials
$s3 = app('aws')->createS3();
// Bypasses credential caching
$s3 = new \Aws\S3\S3Client([...]);Cache security note: Cached AWS credentials are stored in plaintext in the configured backend. Ensure your cache backend is appropriately secured. APCu stores credentials in shared memory within the PHP process, which is not accessible externally.
Laravel 13 serializable_classes: Laravel 13 added a cache.serializable_classes config option, defaulted to false in fresh installs, which restricts the classes the cache may unserialize (defense against deserialization gadget chains). The cache_store fallback path stores an Aws\Credentials\Credentials object, so on Laravel 13 apps that keep the false default the credential entry is read back as __PHP_Incomplete_Class and treated as a cache miss. The effect is silent degradation of the fallback path (every request re-resolves credentials), not an error. If you rely on the cache_store fallback rather than APCu, allow-list the class:
// config/cache.php
'serializable_classes' => [
Aws\Credentials\Credentials::class,
],The APCu path is unaffected (APCu uses its own serialization, not the Laravel cache repository).
Static credentials are not cached. Credentials without a reported expiration (e.g. static env keys, ~/.aws/credentials profiles without an expiration) cannot be safely cached because the package has no signal for when to evict them. Each request will re-invoke the SDK credential chain. This is intentional: the expired-on-arrival guard in AwsCredentialCacheStore::set() prevents storing credentials the SDK has already marked expired, but long-lived credentials have no expiration to check. Production workloads on IRSA / Pod Identity / instance profiles get full caching benefits because the SDK reports an expiration.
This package consciously does not work around the following AWS-side gaps. Each limitation is documented so consumers understand the behavior and where to file issues upstream if material.
The agent's internal cache can lag behind server-side session revocation. No client-side mechanism can detect this. The connector retry helps only if the agent has rotated between attempts. If your observability shows sustained iam-auth.rds-auth-rejected-retry-failed events, file with aws/containers-roadmap; do not add client-side mitigation here.
RDS speaks the database wire protocol, so auth rejections arrive as PDOException, never as AWS SDK exceptions. The SDK's credential-refresh retry cannot apply. The connector-layer retry in InjectsIamToken is the correct place for the equivalent recovery logic.
If you are on v1, follow the v1 → v2 migration notes in the v2.x branch README first, then return here.
v3 is a breaking change from v2 with the following migration steps:
- Update
composer.jsonto require^3.0. - Remove
cache_ttlandcredentials_expiry_bufferfrom anyconfig/iam-auth.phpoverrides. The RDS token cache is gone (tokens are signed per call); credential cache expiry is governed by the AWS SDK's standardAws\CacheInterfacecontract. - Remove the
IAM_AUTH_CACHE_TTLandIAM_AUTH_CREDENTIALS_EXPIRY_BUFFERenv vars from deployment manifests. - Existing APCu/Laravel cache entries from v2 will be discarded automatically (different cache key, different value shape). No migration step required.
- Custom handlers or middleware on the
awsSDK singleton now actually survive (v3 no longer rebuilds the singleton).
After deployment, expect occasional iam-auth.rds-auth-rejected warnings (followed by silent successful retries) during credential rotation windows. Sustained iam-auth.rds-auth-rejected-retry-failed events indicate the residual case and should be escalated upstream.
MIT