DEV Community

Cover image for How to Securely Store and Use API Keys in Laravel in 2026
Alexey Babenko
Alexey Babenko

Posted on

How to Securely Store and Use API Keys in Laravel in 2026

In 2026, almost every Laravel project integrates 3–10 external APIs: OpenAI, Stripe, Telegram, AWS S3, Resend, Brevo, and so on.

Yet most key leaks happen not because of sophisticated attacks, but due to silly mistakes: committing to git, logging them, sending them to the frontend, or calling env() in production after config:cache.

Today we’ll walk through a battle-tested path — from “it just works” to “I sleep peacefully”.

1. Basic (but already correct) level — .env + config

Never hard-code keys in your source:

// ❌ Bad
$openai = new OpenAI('sk-123...');
Enter fullscreen mode Exit fullscreen mode
// ✅ Good
# .env
OPENAI_API_KEY=sk-...
STRIPE_SECRET_KEY=sk_live_...

// config/services.php
return [
    'openai' => [
        'api_key' => env('OPENAI_API_KEY'),
        'organization' => env('OPENAI_ORG'),
    ],

    'stripe' => [
        'key'    => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET_KEY'),
    ],
];
Enter fullscreen mode Exit fullscreen mode

Usage:

$key = config('services.openai.api_key');

// or with type safety (Laravel 11+)
$key = Config::string('services.openai.api_key');
Enter fullscreen mode Exit fullscreen mode

Why is config better than env() everywhere?

After php artisan config:cache, any env() call outside config files returns null.
That’s why every env() should live only inside config/*.php files.

Golden rule:

env() — only in config files.
Everywhere else — config('path.to.key').

2. Middle level: type-safe configs + DTO

A plain string is fragile. Let’s make it strongly typed.
Create a value object:

// app/Config/OpenAiConfig.php
final readonly class OpenAiConfig
{
    public function __construct(
        public string $apiKey,
        public ?string $organization = null,
        public string $baseUrl = 'https://api.openai.com/v1',
    ) {
        if (empty($this->apiKey)) {
            throw new RuntimeException('OpenAI API key is missing');
        }
    }

    public static function make(): self
    {
        return new self(
            apiKey: Config::string('services.openai.api_key'),
            organization: Config::string('services.openai.organization', null),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it as a singleton:

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(OpenAiConfig::class, fn () => OpenAiConfig::make());
}
Enter fullscreen mode Exit fullscreen mode

Usage anywhere:

$openai = app(OpenAiConfig::class);

$client = OpenAI::client($openai->apiKey, $openai->organization);
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • IDE autocompletion for fields
  • Fail fast at boot — not in production three days later
  • Easy to mock / test

3. Zero-downtime key rotation

API providers (OpenAI, Anthropic, Stripe, AWS) recommend rotating keys every 90 days.
But simply replacing the key in .env → every request fails until the next deploy.

Simple fallback approach:

# .env
OPENAI_API_KEY_CURRENT=sk-abc123...
OPENAI_API_KEY_OLD=sk-oldkey...
Enter fullscreen mode Exit fullscreen mode
// app/Services/OpenAiService.php
class OpenAiService
{
    private array $keys;

    public function __construct()
    {
        $this->keys = [
            Config::string('services.openai.api_key_current'),
            Config::string('services.openai.api_key_old', null),
        ];
    }

    public function client(): \OpenAI\Client
    {
        foreach ($this->keys as $key) {
            if ($key === null) continue;

            try {
                $client = OpenAI::client($key);
                $client->models()->list(); // quick validation
                return $client;
            } catch (\Exception $e) {
                // log and try next
            }
        }

        throw new RuntimeException('All OpenAI keys failed');
    }
}
Enter fullscreen mode Exit fullscreen mode

Even better — grace period (like AWS / Laravel encryption):

  1. Add new key as CURRENT
  2. Keep old one as FALLBACK_1, FALLBACK_2…
  3. Remove old key 1–2 hours later (or after deploy)

For automatic rotation, consider packages like laravel-locksmith (zero-downtime rotation with AWS secrets / other providers).

4. Production-only enhancements

Health check for API keys
Add a /health (or /up) route:

// routes/health.php
Route::get('/health/api-keys', function () {
    $status = [
        'openai' => Config::string('services.openai.api_key') ? 'OK' : 'MISSING',
        'stripe' => Config::string('services.stripe.secret')  ? 'OK' : 'MISSING',
        // ...
    ];

    $missing = array_keys(array_filter($status, fn($v) => $v === 'MISSING'));

    if ($missing) {
        abort(500, 'Missing API keys: ' . implode(', ', $missing));
    }

    return response()->json([
        'status' => 'healthy',
        'checked_keys' => $status,
    ]);
})->name('health.api-keys');
Enter fullscreen mode Exit fullscreen mode

Kubernetes, Laravel Forge, Ploi can monitor this endpoint.

Laravel Pennant + feature flags

Temporarily disable a key:

Pennant::define('openai-enabled', fn () => env('OPENAI_ENABLED', true));

if (Pennant::active('openai-enabled')) {
    // use it
} else {
    // fallback / mock
}
Enter fullscreen mode Exit fullscreen mode

5. Anti-patterns of 2026 (don’t do this)

  • env('KEY') in controllers/services after config:cache
  • Storing keys in database without encryption (encrypt() / Crypt::)
  • Committing .env to git
  • Sending keys to frontend (even via VITE_ env)
  • Ignoring 429 / invalid key errors without fallback
  • Using the same key across all environments

Pre-deploy checklist

  1. All keys are in .env — .env is not in git
  2. env() calls exist only in config/*.php
  3. php artisan config:cache is running in production
  4. You’re using Config::string() / typed config methods
  5. DTO / singleton exists for each service
  6. Fallback or rotation is implemented
  7. /health route checks API keys
  8. No keys appear in logs (check config/logging.php)

Go check your projects right now — one missing key can cost you a fortune.
How do you store and rotate API keys?
Share your approaches in the comments — maybe you have an even cleaner solution!
If you found this useful — give it a ❤️ and save for later.
Secure deploys! 🔐

Top comments (0)