incompass/aws-secrets-bundle
A Symfony bundle that integrates AWS Secrets Manager, letting you load and manage secrets from AWS directly in your application configuration and environment without hardcoding sensitive values.
First, the framing problem. You were told this is a Laravel package. It is not — incompass/aws-secrets-bundle (a republish of casechek/aws-secrets-bundle) is a Symfony bundle. It extends Symfony\Component\HttpKernel\Bundle\Bundle, registers a DI extension, and hooks the Symfony kernel boot. There is no LaravelServiceProvider, no composer extra.laravel block, and no bootstrap/app.php integration. A Laravel developer cannot composer require this and expect it to work.
Where to look first: read src/ and composer.json before anything else. Confirm which Symfony versions are constrained, whether it actually touches $_ENV/$_SERVER vs. Symfony ParameterBag, how it authenticates (Aws\Credentials\CredentialProvider::defaultProvider() or hardcoded Credentials), and — critically — whether it caches. If it dumps values into ParameterBag only, it is useless to Laravel even as a reference.
First use case: use this repo as a reference implementation, not a dependency. The pattern is: at bootstrap, call SecretsManagerClient::getSecretValue(), json_decode() the SecretString, and merge the resulting key-value pairs into the env repository before config is read. That is ~100–200 lines in Laravel, and you own it.
Minimal steps (reference-only reading):
ContainerBuilder compiles, so env mutation at that point is visible to %env()% resolution).Application::bootstrapWith() → LoadEnvironmentVariables → LoadConfiguration → config:cache. Your hook goes before LoadConfiguration, i.e., a bootstrap provider or an override of LoadEnvironmentVariables.Minimal Laravel loader skeleton (what you actually want):
// App\Bootstrap\LoadAwsSecrets
public function bootstrap(Application $app): void
{
if (! filter_var(env('AWS_SECRETS_ENABLED', false), FILTER_VALIDATE_BOOL)) {
return; // local dev, CI, non-AWS hosts
}
$client = new SecretsManagerClient([
'region' => env('AWS_DEFAULT_REGION'),
'version' => 'latest',
]);
$json = json_decode(
$client->getSecretValue(['SecretId' => env('AWS_SECRET_ID')])->get('SecretString'),
true,
512,
JSON_THROW_ON_ERROR
);
foreach ($json as $key => $value) {
// Precedence rule — see gotchas
if (getenv($key) === false) {
putenv("$key=$value");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}
Register it in bootstrap/app.php after LoadEnvironmentVariables (so .env is populated for the enable flag and region) but before LoadConfiguration (so cached config sees the values).
Pattern 1 — Secrets as env vars, config stays untouched. The whole point is that your existing config/database.php continues to read env('DB_PASSWORD'). Application code changes zero. That is the selling point and should be preserved.
Pattern 2 — Single JSON secret, flat keys. Store one secret named for the app (myapp/production), structured exactly like your .env:
{ "DB_HOST": "...", "DB_PASSWORD": "...", "STRIPE_SECRET": "..." }
Flat key-for-key mapping makes diffing against .env trivial and avoids nested-key traversal logic. Nested JSON is a trap — you end up writing AWS_SECRETS_MAP="db.password=DB_PASSWORD" DSLs that nobody maintains.
Pattern 3 — Fail-fast, not fail-open. If the loader is enabled and Secrets Manager is unreachable, throw — do not silently continue with an empty env. Silent fallback means the app boots against stale or missing credentials and fails later with a confusing AccessDenied from the database driver. Fail-closed at boot gives you a clean 5xx with a stack trace naming Secrets Manager.
Pattern 4 — Shadow / non-authoritative mode for rollout. Add AWS_SECRETS_AUTHORITATIVE=false. In that mode, fetch and log the diff of what would have changed, without overriding existing env. Run in staging for a week; flip to authoritative once clean. This is the single most valuable pattern in the brief. It also doubles as the local-dev escape hatch: on developer laptops, enabled, non-authoritative, .env values win, zero AWS dependency.
Pattern 5 — IAM, not keys. Rely on CredentialProvider::defaultProvider() — task role on ECS/Fargate, instance profile on EC2, web-identity on EKS/Lambda. Never put AWS_ACCESS_KEY_ID in .env for this to work; the moment you do, you have reintroduced the exact problem you are solving. Scope the role to one secret ARN and kms:Decrypt on the specific CMK.
Pattern 6 — Cache across boots, not across requests. Two options:
apcu_fetch('awssec:'.$name) with a TTL) — cheapest, per-node, and survives across PHP-FPM requests. Good default.storage/framework/cache/aws-secrets/ — survives container restarts on persistent volumes but not on Fargate (ephemeral disk), so it only helps within a running process.Never make a Secrets Manager call per request. Boot-once is the model.
Pattern 7 — Version stages for rotation. Always call getSecretValue without a VersionId — that returns AWSCURRENT. Hardcoding a version pin kills your rotation story. If you cache, the TTL is your rotation propagation window. Document it: "TTL 300s → rotated credentials take effect within 5 minutes."
Pattern 8 — Config cache is the decisive constraint. php artisan config:cache bakes current env() values into bootstrap/cache/config.php. Your loader must run before that file is read, and the values it sets must be identical at config:cache time and at runtime, or you get drift. Simplest rule: the loading path in bootstrap/app.php runs both for config:cache and for normal requests. Verify by clearing cache, caching, and grepping the config cache file for a sentinel secret value.
env() returns null after config:cache. This is the number-one Laravel trap. env() calls inside config/*.php are fine (they get evaluated and baked); env() calls inside application code return null once cached. A secrets loader that relies solely on putenv() will work before config:cache and silently fail on cached deploys if any code reads env() at runtime. Never read secrets via env() outside config files — always config('services.stripe.secret')..env, or does .env win? Both are defensible. override = false is safer for the shadow rollout and allows local overrides; override = true is safer for production (env vars cannot accidentally shadow secret values). Pick per-environment via a config flag, not per-key.getenv() vs $_ENV vs $_SERVER. vlucas/phpdotenv populates $_ENV and $_SERVER unless Env::disablePutenv() is called (which Laravel 9+ does by default in some setups). Write to all three, and check getenv()/$_ENV before setting to respect precedence. Debugging this across PHP versions and variables_order INI settings is a rabbit hole — test it explicitly.$_ENV values appear in phpinfo(), Laravel Telescope/debugbar request dumps, error page exception context, and any var_dump($_ENV) someone leaves in. Add redaction: mask the loaded secret keys in Telescope's RequestWatcher and never log the raw env array. This is an audit finding waiting to happen.aws/aws-sdk-php is a large SDK — hundreds of files. On Fargate it is measurable in image size and autoload; on Lambda it is measurable in cold-start. Consider bundling only Aws\SecretsManager + Aws\Credentials via an isolated service provider, or just accept the SDK if you already use S3/SQS elsewhere.GetSecretValue is rate-limited per account/region. A rolling deploy of 200 tasks with cold caches = 200 calls in seconds. Pre-warm caches in a bootstrap job, or stagger deploys, or reduce fleet churn. APCu cache does not help a fresh task..env dump usually fits but is close once you include many keys. Do not store certificate bundles or large blobs here.AccessDeniedException with a misleading message. Verify the key is enabled and the IAM policy grants kms:Decrypt on the key ARN (not the secret ARN).AWSCURRENT vs AWSPENDING. Automated rotation writes AWSPENDING while finalizing. Calling without VersionStage returns AWSCURRENT, which is correct. If you ever pin a stage, you will break rotation — do not.ResourceNotFoundException on boot = total outage. Secret renamed or wrong region → boot fails hard. Add a health-check endpoint that verifies secret loadability, and alert on its failure before a rolling deploy finishes. This is your canary.AWS_SECRETS_ENABLED=false by default in .env.example, and the loader must be a no-op when unset.composer.json. If you fork its code, fork it into your own namespace, pin by SHA, add tests for the fail-fast and precedence paths, and record it in your SBOM. The realistic maintenance plan is: you own it, and the realistic exit is replacing it with the in-house loader you are going to write anyway.enabled and secret_id/region — because those are the two things operations will want to toggle without a code change: disable the loader during an AWS incident, and point at a different secret in a new region. Design for those two flags from day one.How can I help you explore Laravel packages today?