Secret key management for payments: a step-by-step guide
We've all heard the horror stories: a Stripe secret key committed to a public GitHub repository, an AWS access key left in Docker logs, API credentials hardcoded in a frontend bundle. By day 92 of payment integration work, you've likely handled dozens of sensitive keys—and the stakes couldn't be higher. Secret key management for payments isn't optional security theater; it's foundational to protecting your platform, your customers' data, and your business reputation.
This guide walks through practical patterns we've implemented across e-commerce, SaaS, and fintech systems. You'll learn how to issue restricted keys with minimal permissions, rotate them on a schedule, store them safely in a secrets manager, and monitor for suspicious activity that suggests a breach.
Restricted keys with minimal permissions
The principle is simple: every integration point gets only the permissions it needs. Yet in practice, teams often create a single "admin" secret key and pass it everywhere—to the backend, the payment webhook handler, the batch reconciliation job, and sometimes even to third-party vendors. One compromise exposes everything.
Stripe's restricted keys feature lets you scope capabilities by resource type and operation. For example, your backend charge endpoint doesn't need access to customer deletion. Your webhook handler doesn't need to create refunds. Your reconciliation service doesn't need to modify product pricing.
Here's how we structure restricted keys in a typical SaaS payment stack:
- Webhook processor key: Read-only access to events, charges, and customers. Cannot modify or delete.
- Charge creation key: Limited to creating payment intents, confirming charges, and reading customer data. No refunds, no dispute management.
- Refund handler key: Can read charges and create refunds only. Cannot access customer bank details or modify billing.
- Admin key: Full permissions, used only in secure admin dashboards behind authentication and audit logging.
In your backend application, you'd load these as separate environment variables:
// config/stripe-keys.ts
export const stripeKeys = {
webhookKey: process.env.STRIPE_RESTRICTED_WEBHOOK_KEY!,
chargeKey: process.env.STRIPE_RESTRICTED_CHARGE_KEY!,
refundKey: process.env.STRIPE_RESTRICTED_REFUND_KEY!,
adminKey: process.env.STRIPE_ADMIN_KEY!, // Use sparingly
};
// services/payment-service.ts
import Stripe from 'stripe';
import { stripeKeys } from '../config/stripe-keys';
export class ChargeService {
private stripe = new Stripe(stripeKeys.chargeKey, {
apiVersion: '2024-04-10',
});
async createPaymentIntent(amount: number, customerId: string) {
return this.stripe.paymentIntents.create({
amount,
currency: 'usd',
customer: customerId,
});
}
}
export class RefundService {
private stripe = new Stripe(stripeKeys.refundKey, {
apiVersion: '2024-04-10',
});
async refundCharge(chargeId: string, amount?: number) {
return this.stripe.refunds.create({
charge: chargeId,
amount,
});
}
}Creating these restricted keys in the Stripe dashboard takes minutes. Under API Keys > Restricted Keys, choose the operations your service needs, assign it a memorable name like production-webhook-processor, and copy the key. When you audit logs later, you'll see exactly which key was used for which operation—invaluable for incident response.
Rotate keys periodically
A key stolen today but rotated within 48 hours causes minimal damage. A key stolen two years ago and still active is a ticking time bomb. Periodic rotation limits the window of exposure if a breach occurs.
We recommend a schedule:
- Admin keys: Every 90 days, or immediately if access logs show unusual activity.
- Service keys (charge, refund, webhook): Every 180 days as part of a routine security calendar.
- Emergency rotation: Within 2 hours if logs suggest compromise.
In practice, rotation means creating a new restricted key with the same permissions, updating your secrets manager, redeploying the affected service, then deactivating the old key. Stripe lets you archive (not delete) keys so you can trace historical transactions.
Automate this with a checklist or ticket template in your issue tracker:
- Create a new restricted key in Stripe with identical permissions to the key being rotated.
- Update the secrets manager entry (e.g., AWS Secrets Manager, HashiCorp Vault) with the new key value.
- Deploy the service that uses this key. Most frameworks will pick up the new value from the secrets manager automatically.
- Verify the service is operational with the new key (check logs, run a test transaction).
- Archive the old key in Stripe and update the rotation log.
- Create a reminder for the next rotation date.
If you use infrastructure-as-code or a deployment pipeline, you might even script steps 2 and 3. The key is making rotation easy enough that your team actually does it on schedule rather than postponing indefinitely.
Store in a secrets manager
Environment variables in a .env file are not a secrets manager. Neither is a shared Google Doc or a Slack message. A proper secrets manager provides encryption at rest, encryption in transit, access controls, audit logs, and rotation helpers.
We standardize on one of these depending on infrastructure:
- AWS Secrets Manager: Excellent for AWS-native stacks. Integrates with RDS, Lambda, ECS, and EC2. Pricing is per secret per month.
- HashiCorp Vault: Platform-agnostic, self-hosted option with robust RBAC and audit trails. Steeper operational overhead but preferred in regulated industries.
- Google Secret Manager: Solid choice for GCP deployments.
- Doppler or 1Password Secrets: SaaS-hosted options, good for teams that don't want to manage infrastructure.
Here's a practical example using AWS Secrets Manager with a Node.js backend:
// services/secrets-service.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
export async function getStripeKeys() {
const command = new GetSecretValueCommand({
SecretId: 'prod/stripe-keys',
});
const response = await client.send(command);
const secret = JSON.parse(response.SecretString!);
return {
chargeKey: secret.STRIPE_RESTRICTED_CHARGE_KEY,
refundKey: secret.STRIPE_RESTRICTED_REFUND_KEY,
webhookKey: secret.STRIPE_RESTRICTED_WEBHOOK_KEY,
};
}
// In your application startup
import Stripe from 'stripe';
let cachedStripeInstances: Record = {};
export async function getStripeClient(purpose: 'charge' | 'refund' | 'webhook') {
if (!cachedStripeInstances[purpose]) {
const keys = await getStripeKeys();
const keyMap = {
charge: keys.chargeKey,
refund: keys.refundKey,
webhook: keys.webhookKey,
};
cachedStripeInstances[purpose] = new Stripe(keyMap[purpose], {
apiVersion: '2024-04-10',
});
}
return cachedStripeInstances[purpose];
}On deployment, your infrastructure passes the Secrets Manager ARN via an IAM role attached to your EC2 instance, ECS task, or Lambda function. The application assumes that role and fetches the secret at startup. No key ever lands in environment variables or config files.
Secrets Manager will also let you set up automatic rotation. You can configure a Lambda function that rotates the key in Stripe, updates the secret, and verifies the new key works—all without human intervention.
Alert on unusual API usage
Even with restricted keys and rotation, a compromised key is worthless only if you catch it quickly. Monitoring API usage patterns helps you detect anomalies within minutes rather than days.
Set up alerts for:
- Geographic anomalies: A key typically used in us-east-1 suddenly making requests from China or Eastern Europe.
- Velocity spikes: 10x normal request volume in 5 minutes (suggests automated exploitation).
- Unauthorized operations: A restricted refund key attempting to create customers or access billing info.
- Failed authentication: Repeated 401s on a single key (brute-force or outdated key).
- Off-hours activity: API calls outside your expected operating windows.
Stripe's own dashboard shows API request logs with latency, response codes, and requested resources. But integrate Stripe webhooks and events into your observability stack for real-time alerting:
// services/monitoring-service.ts
import axios from 'axios';
export async function alertOnAnomalousActivity(
event: Stripe.Event,
apiLatency: number,
requestOrigin: string
) {
const isAnomaly =
apiLatency > 5000 || // Unusually slow
(requestOrigin !== 'our-vpc' && event.type === 'charge.refunded') ||
(event.data.object as any).amount > 100000; // Large transaction outside normal flow
if (isAnomaly) {
await axios.post('https://hooks.slack.com/services/YOUR/WEBHOOK', {
text: `🚨 Anomalous Stripe API activity detected`,
attachments: [
{
color: 'danger',
fields: [
{ title: 'Event Type', value: event.type, short: true },
{ title: 'API Latency', value: `${apiLatency}ms`, short: true },
{ title: 'Request Origin', value: requestOrigin, short: true },
{ title: 'Timestamp', value: new Date(event.created * 1000).toISOString(), short: false },
],
},
],
});
}
}In AWS CloudWatch or DataDog, create dashboards that show API call volume, error rates, and request distribution by key and operation. Set thresholds that trigger PagerDuty alerts so on-call engineers are notified immediately.
Common mistakes
Using one admin key everywhere. We've seen teams create a single Stripe secret key and embed it in their backend, CLI tools, local development environments, and even scripts shared with partners. If any single copy leaks, the attacker has full Stripe access. Use restricted keys scoped to each service.
Storing secrets in version control. A .env file committed to Git, even if the repo is later marked private, is permanently exposed. Use .gitignore to exclude secrets files, and audit your Git history for accidentally committed credentials. If you find any, rotate those keys immediately.
Rotating keys without testing. We've caused customer-facing outages by rotating a key but failing to verify the new one worked before deactivating the old one. Always test in staging first, or run both keys in parallel for a short window during production rotation.
Ignoring audit logs. Many teams set up a secrets manager but never look at who accessed which secrets or when. Audit logs are your early warning system. Review them weekly, especially after any incident.
Assuming "offline" systems don't need keys. Batch reconciliation jobs, scheduled cleanup tasks, and data migration scripts all need API access. They get keys too, and those keys should be rotated and monitored like any other.
Key takeaways
- Create restricted keys with minimal permissions for each integration point. Don't rely on a single admin key.
- Rotate keys on a predictable schedule (90–180 days depending on criticality) and keep rotation easy through checklists and automation.
- Store all secrets in a dedicated secrets manager with encryption, audit logging, and access controls. Never commit to version control or store in plain-text environment files.
- Monitor API usage patterns for geographic, velocity, and operation anomalies. Alert your team within minutes of suspicious activity.
- Test key rotation in staging before rolling out to production. Run both keys in parallel if needed.
- Audit your secrets manager and Stripe logs regularly. Treat unusual activity as a potential breach and rotate keys immediately.






Comments
Loading comments…