Securing Stripe API Keys in Production Applications
A security breach involving exposed API keys can cost businesses thousands in fraudulent charges and months in recovery time. In 2023, a mid-sized SaaS company...
Osmoto Team
Senior Software Engineer

A security breach involving exposed API keys can cost businesses thousands in fraudulent charges and months in recovery time. In 2023, a mid-sized SaaS company discovered their Stripe secret key had been committed to a public GitHub repository for six months, resulting in $12,000 in unauthorized charges and a complete payment system overhaul.
Stripe API keys are the gateway to your payment infrastructure, controlling everything from creating charges to managing customer data. When these keys are compromised, attackers can initiate transactions, access sensitive customer information, and manipulate your payment flows. The financial and reputational damage extends far beyond the immediate fraudulent activity.
This guide covers the essential security practices for protecting Stripe API keys in production environments, from proper environment variable management to implementing automated key rotation. We'll explore real-world scenarios, common vulnerabilities, and the specific steps needed to maintain a secure payment infrastructure.
Understanding Stripe API Key Types and Security Levels
Stripe provides different types of API keys, each with distinct security implications and use cases. Understanding these differences is crucial for implementing appropriate security measures.
Publishable vs Secret Keys
Publishable keys (pk_live_ or pk_test_) are designed for client-side use and can be safely exposed in frontend code. They allow limited operations like creating payment methods and confirming payment intents, but cannot access sensitive data or perform administrative actions.
Secret keys (sk_live_ or sk_test_) provide full API access and must never be exposed in client-side code or public repositories. These keys can create charges, access customer data, and modify account settings.
// Safe - publishable key in frontend const stripe = Stripe('pk_live_abcdef123456789'); // NEVER do this - secret key exposed in frontend const stripe = Stripe('sk_live_abcdef123456789'); // ❌ Security vulnerability
Restricted API Keys
Stripe's restricted API keys allow you to create keys with limited permissions, following the principle of least privilege. This is particularly valuable for third-party integrations or specific microservices.
// Example: Key restricted to only read customer data // Created in Stripe Dashboard with specific resource permissions const restrictedKey = 'rk_live_abcdef123456789';
When creating restricted keys, limit permissions to exactly what each service needs. A webhook handler might only need write access to subscription objects, while a reporting service might only need read access to charges.
Environment Variable Management
Proper environment variable handling is the foundation of API key security. The goal is to keep secrets out of your codebase while ensuring they're accessible to your application at runtime.
Server-Side Environment Configuration
Never hardcode API keys in your source code. Instead, use environment variables that are loaded at runtime:
// ❌ Hardcoded key - security vulnerability const stripe = new Stripe('sk_live_abcdef123456789'); // ✅ Environment variable - secure approach const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16', });
Create a .env.local file for development (ensure it's in your .gitignore):
# .env.local (never commit this file) STRIPE_SECRET_KEY=sk_test_your_test_key_here STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
Environment-Specific Key Management
Maintain separate keys for different environments to prevent accidental production charges during development:
// config/stripe.ts const getStripeConfig = () => { const env = process.env.NODE_ENV; if (env === 'production') { return { secretKey: process.env.STRIPE_LIVE_SECRET_KEY!, publishableKey: process.env.STRIPE_LIVE_PUBLISHABLE_KEY!, webhookSecret: process.env.STRIPE_LIVE_WEBHOOK_SECRET!, }; } return { secretKey: process.env.STRIPE_TEST_SECRET_KEY!, publishableKey: process.env.STRIPE_TEST_PUBLISHABLE_KEY!, webhookSecret: process.env.STRIPE_TEST_WEBHOOK_SECRET!, }; }; export const stripeConfig = getStripeConfig();
Validation and Error Handling
Implement validation to catch configuration issues early:
// lib/stripe-config.ts class StripeConfigError extends Error { constructor(message: string) { super(`Stripe Configuration Error: ${message}`); this.name = 'StripeConfigError'; } } const validateStripeKeys = () => { const { secretKey, publishableKey } = stripeConfig; if (!secretKey) { throw new StripeConfigError('Secret key is required'); } if (!publishableKey) { throw new StripeConfigError('Publishable key is required'); } // Validate key format if (!secretKey.startsWith('sk_')) { throw new StripeConfigError('Invalid secret key format'); } // Ensure environment consistency const isLiveSecret = secretKey.startsWith('sk_live_'); const isLivePublishable = publishableKey.startsWith('pk_live_'); if (isLiveSecret !== isLivePublishable) { throw new StripeConfigError('Key environment mismatch'); } }; validateStripeKeys();
Secure Storage Solutions
While environment variables work for simple deployments, production applications often require more sophisticated secret management.
Cloud Provider Secret Managers
Modern cloud platforms provide dedicated secret management services that offer encryption, access control, and audit logging.
AWS Secrets Manager integration:
// lib/secrets.ts import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; class SecretManager { private client: SecretsManagerClient; private cache: Map<string, { value: string; expires: number }> = new Map(); constructor() { this.client = new SecretsManagerClient({ region: process.env.AWS_REGION || 'us-east-1', }); } async getSecret(secretId: string, ttl: number = 300000): Promise<string> { // Check cache first const cached = this.cache.get(secretId); if (cached && Date.now() < cached.expires) { return cached.value; } try { const command = new GetSecretValueCommand({ SecretId: secretId }); const response = await this.client.send(command); const value = response.SecretString!; // Cache with TTL this.cache.set(secretId, { value, expires: Date.now() + ttl, }); return value; } catch (error) { throw new Error(`Failed to retrieve secret ${secretId}: ${error.message}`); } } } export const secretManager = new SecretManager(); // Usage in your Stripe configuration export const getStripeKey = async (): Promise<string> => { const secretId = process.env.NODE_ENV === 'production' ? 'prod/stripe/secret-key' : 'dev/stripe/secret-key'; return await secretManager.getSecret(secretId); };
Container Orchestration Secrets
For Kubernetes deployments, use native secret management:
# k8s-secrets.yaml apiVersion: v1 kind: Secret metadata: name: stripe-secrets type: Opaque data: stripe-secret-key: <base64-encoded-key> stripe-webhook-secret: <base64-encoded-webhook-secret>
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: payment-service spec: template: spec: containers: - name: app image: your-app:latest env: - name: STRIPE_SECRET_KEY valueFrom: secretKeyRef: name: stripe-secrets key: stripe-secret-key
Implementing API Key Rotation
Regular key rotation is a critical security practice that limits the impact of potential compromises. Stripe supports seamless key rotation without service interruption.
Automated Rotation Strategy
Implement a rotation system that creates new keys before deactivating old ones:
// lib/key-rotation.ts interface KeyRotationConfig { rotationIntervalDays: number; gracePeriodHours: number; notificationWebhook?: string; } class StripeKeyRotation { private config: KeyRotationConfig; constructor(config: KeyRotationConfig) { this.config = config; } async rotateSecretKey(): Promise<void> { const currentKey = await this.getCurrentKey(); const keyAge = this.getKeyAge(currentKey); if (keyAge < this.config.rotationIntervalDays) { return; // Key is still fresh } try { // Step 1: Generate new key via Stripe API const newKey = await this.generateNewKey(); // Step 2: Update secret storage await this.updateSecretStorage(newKey); // Step 3: Deploy new key to all services await this.deployNewKey(newKey); // Step 4: Verify new key is working await this.verifyNewKey(newKey); // Step 5: Schedule old key deactivation await this.scheduleOldKeyDeactivation(currentKey); // Step 6: Send notification await this.notifyRotationComplete(currentKey, newKey); } catch (error) { await this.handleRotationFailure(error); throw error; } } private async generateNewKey(): Promise<string> { // Use Stripe's API to create a new restricted key // This requires a master key with key management permissions const stripe = new Stripe(process.env.STRIPE_MASTER_KEY!); const key = await stripe.apiKeys.create({ type: 'restricted', name: `Auto-generated-${new Date().toISOString()}`, scope: { type: 'account', }, }); return key.secret; } private async verifyNewKey(key: string): Promise<void> { const stripe = new Stripe(key); try { // Perform a simple API call to verify the key works await stripe.balance.retrieve(); } catch (error) { throw new Error(`New key verification failed: ${error.message}`); } } }
Graceful Key Transition
Implement a transition period where both old and new keys are valid:
// lib/stripe-client.ts class StripeClientManager { private primaryClient: Stripe; private fallbackClient?: Stripe; constructor() { this.primaryClient = new Stripe(process.env.STRIPE_SECRET_KEY!); // During rotation, maintain a fallback client if (process.env.STRIPE_SECRET_KEY_PREVIOUS) { this.fallbackClient = new Stripe(process.env.STRIPE_SECRET_KEY_PREVIOUS); } } async executeWithFallback<T>( operation: (client: Stripe) => Promise<T> ): Promise<T> { try { return await operation(this.primaryClient); } catch (error) { if (this.fallbackClient && this.isAuthError(error)) { console.warn('Primary key failed, attempting with fallback key'); return await operation(this.fallbackClient); } throw error; } } private isAuthError(error: any): boolean { return error.type === 'StripeAuthenticationError'; } } export const stripeManager = new StripeClientManager(); // Usage const customer = await stripeManager.executeWithFallback( (stripe) => stripe.customers.create({ email: 'user@example.com' }) );
Monitoring and Auditing
Continuous monitoring helps detect unauthorized key usage and potential security breaches.
API Usage Monitoring
Implement logging and alerting for unusual API patterns:
// lib/stripe-monitor.ts interface ApiCallMetrics { keyId: string; endpoint: string; timestamp: Date; responseCode: number; ipAddress?: string; userAgent?: string; } class StripeApiMonitor { private metrics: ApiCallMetrics[] = []; logApiCall(metrics: ApiCallMetrics): void { this.metrics.push(metrics); // Check for suspicious patterns this.detectAnomalies(metrics); } private detectAnomalies(metrics: ApiCallMetrics): void { const recentCalls = this.getRecentCalls(5 * 60 * 1000); // Last 5 minutes // Detect rate limiting threshold breach if (recentCalls.length > 100) { this.alertSuspiciousActivity('High API usage detected', { callCount: recentCalls.length, timeWindow: '5 minutes', }); } // Detect unusual IP addresses const uniqueIPs = new Set(recentCalls.map(call => call.ipAddress)); if (uniqueIPs.size > 10) { this.alertSuspiciousActivity('Multiple IP addresses detected', { ipCount: uniqueIPs.size, ips: Array.from(uniqueIPs), }); } // Detect failed authentication attempts const authFailures = recentCalls.filter(call => call.responseCode === 401); if (authFailures.length > 5) { this.alertSuspiciousActivity('Multiple authentication failures', { failureCount: authFailures.length, }); } } private async alertSuspiciousActivity(message: string, details: any): Promise<void> { // Send alert to monitoring system console.error('SECURITY ALERT:', message, details); // You might integrate with services like: // - Slack/Discord webhooks // - PagerDuty // - CloudWatch alarms // - Custom monitoring dashboard } }
Webhook Security Monitoring
Monitor webhook endpoints for unauthorized access attempts:
// lib/webhook-security.ts export const secureWebhookHandler = ( handler: (event: Stripe.Event) => Promise<void> ) => { return async (req: NextApiRequest, res: NextApiResponse) => { const signature = req.headers['stripe-signature'] as string; const payload = req.body; if (!signature) { console.warn('Webhook received without signature', { ip: req.socket.remoteAddress, userAgent: req.headers['user-agent'], }); return res.status(400).json({ error: 'Missing signature' }); } try { const event = stripe.webhooks.constructEvent( payload, signature, process.env.STRIPE_WEBHOOK_SECRET! ); // Log successful webhook processing console.info('Webhook processed successfully', { eventType: event.type, eventId: event.id, }); await handler(event); res.status(200).json({ received: true }); } catch (error) { console.error('Webhook signature verification failed', { error: error.message, ip: req.socket.remoteAddress, userAgent: req.headers['user-agent'], }); res.status(400).json({ error: 'Invalid signature' }); } }; };
Common Security Pitfalls and How to Avoid Them
Through years of Stripe security audits, we've identified recurring vulnerabilities that developers often overlook.
Logging Sensitive Data
One of the most common mistakes is accidentally logging API keys or sensitive payment data:
// ❌ Never log API keys or sensitive data console.log('Stripe config:', { secretKey: process.env.STRIPE_SECRET_KEY, // Exposed in logs! webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, }); // ❌ Don't log full Stripe objects console.log('Payment intent created:', paymentIntent); // May contain sensitive data // ✅ Safe logging approach console.log('Stripe initialized successfully'); console.log('Payment intent created:', { id: paymentIntent.id, status: paymentIntent.status, amount: paymentIntent.amount, });
Create a utility function for safe logging:
// lib/safe-logger.ts const SENSITIVE_FIELDS = ['secret_key', 'client_secret', 'webhook_secret']; export const safeLog = (message: string, data?: any) => { if (!data) { console.log(message); return; } const sanitized = sanitizeObject(data); console.log(message, sanitized); }; const sanitizeObject = (obj: any): any => { if (typeof obj !== 'object' || obj === null) { return obj; } const sanitized: any = {}; for (const [key, value] of Object.entries(obj)) { if (SENSITIVE_FIELDS.some(field => key.toLowerCase().includes(field))) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'object') { sanitized[key] = sanitizeObject(value); } else { sanitized[key] = value; } } return sanitized; };
Client-Side Key Exposure
Never send secret keys to the client, even in server-side rendered applications:
// ❌ Dangerous - secret key sent to client export const getServerSideProps = async () => { return { props: { stripeConfig: { secretKey: process.env.STRIPE_SECRET_KEY, // Exposed to client! publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, }, }, }; }; // ✅ Safe - only publishable key sent to client export const getServerSideProps = async () => { return { props: { stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY, }, }; };
Insufficient Error Handling
Poor error handling can leak sensitive information:
// ❌ Exposes internal details try { const charge = await stripe.charges.create({ amount: 2000, currency: 'usd', source: 'tok_visa', }); } catch (error) { // This might expose API keys in error messages res.status(500).json({ error: error.message }); } // ✅ Safe error handling try { const charge = await stripe.charges.create({ amount: 2000, currency: 'usd', source: 'tok_visa', }); } catch (error) { console.error('Stripe charge failed:', { errorType: error.type, errorCode: error.code, // Don't log the full error object }); res.status(500).json({ error: 'Payment processing failed', code: error.code, // Safe to expose }); }
Best Practices Summary
Implementing comprehensive API key security requires attention to multiple layers:
Environment Management:
- Never hardcode API keys in source code
- Use separate keys for each environment (development, staging, production)
- Validate key formats and environment consistency at startup
- Implement proper error handling for missing or invalid keys
Storage and Access Control:
- Use dedicated secret management services for production
- Implement the principle of least privilege with restricted keys
- Cache secrets with appropriate TTL to reduce external calls
- Maintain audit logs of secret access
Rotation and Monitoring:
- Implement automated key rotation on a regular schedule
- Monitor API usage patterns for anomalies
- Set up alerts for suspicious activity or authentication failures
- Maintain graceful fallback during key transitions
Development Practices:
- Sanitize logs to prevent accidental key exposure
- Never send secret keys to client-side code
- Implement proper error handling that doesn't leak sensitive information
- Use environment-specific validation in CI/CD pipelines
Incident Response:
- Have a documented procedure for key compromise
- Implement immediate key deactivation capabilities
- Maintain backup authentication methods during emergencies
- Test your incident response procedures regularly
Conclusion
Securing Stripe API keys is not a one-time setup task—it's an ongoing security practice that requires careful planning, implementation, and monitoring. The financial and reputational risks of compromised payment credentials make this investment essential for any production application.
The security measures outlined here form the foundation of a robust payment infrastructure. However, security requirements vary significantly based on your application's architecture, compliance needs, and risk tolerance. Regular security audits can help identify vulnerabilities specific to your implementation and ensure your security measures keep pace with evolving threats.
If you're concerned about your current Stripe security posture or need help implementing these practices, consider a professional security audit to identify vulnerabilities and get specific recommendations for your payment infrastructure.
Related Articles


