API Key Security in Multi-Tenant SaaS: The Right Way to Store and Rotate Them

Meta description: Learn how to securely store and rotate API keys in a multi-tenant SaaS — envelope encryption, AWS KMS, key rotation with grace periods, and SOC 2 compliance.

Last updated: June 22, 2026


A few years ago, I pushed a commit to a private repo and got an automated GitHub alert within 47 seconds: a live AWS access key had just leaked into version control. We invalidated it, rotated every credential in that environment, and spent two days auditing logs. That incident completely changed how I think about API key management in multi-tenant SaaS systems — especially when compliance frameworks like SOC 2 and HIPAA are part of the picture.

This article is what I wish I had read before that happened.


TL;DR

  • Never store raw API keys in a database — always encrypt at rest using envelope encryption or a dedicated secret manager.
  • Rotate keys automatically using a scheduled job + grace period strategy to avoid breaking tenant integrations.
  • Audit every access to secrets and enforce least-privilege policies — this is non-negotiable for SOC 2 and HIPAA compliance.

Why API Key Security Matters More in Multi-Tenant Systems

In a single-tenant app, a leaked key is bad. In a multi-tenant SaaS, a leaked key can expose every tenant’s data at once. The blast radius is enormous.

Most SaaS platforms store API keys issued by third parties (Stripe, Twilio, SendGrid) on behalf of their tenants. If your database is compromised and those keys are stored in plaintext, every one of your customers is at risk — and you’re legally liable depending on your compliance posture.

US-based SaaS companies operating under SOC 2, HIPAA, or PCI-DSS face explicit requirements around secret management, key rotation, and audit logging. Ignoring these isn’t a “we’ll fix it later” item — it’s a blocker for enterprise sales and a liability in the event of a breach.


Prerequisites

Before following this guide, you should have:

  • A SaaS app with a PostgreSQL or similar relational database
  • AWS, GCP, or Azure account (I’ll use AWS examples, but the concepts apply everywhere)
  • Basic familiarity with Node.js or Python
  • A deployment pipeline (GitHub Actions, CircleCI, etc.)

[INTERNAL LINK: related article on setting up a multi-tenant database schema]


Step-by-Step: Secure API Key Storage and Rotation

Step 1: Never Store Plaintext Keys — Use Envelope Encryption

Envelope encryption means you encrypt the secret with a Data Encryption Key (DEK), then encrypt that DEK with a Key Encryption Key (KEK) managed by your cloud provider’s KMS (Key Management Service).

Here’s how I implement this in Node.js using AWS KMS:

const { KMSClient, GenerateDataKeyCommand, DecryptCommand } = require("@aws-sdk/client-kms");
const crypto = require("crypto");

const kms = new KMSClient({ region: "us-east-1" });

async function encryptSecret(plaintextSecret) {
  // Step 1: Ask KMS for a data key
  const { CiphertextBlob, Plaintext } = await kms.send(
    new GenerateDataKeyCommand({
      KeyId: process.env.KMS_KEY_ARN,
      KeySpec: "AES_256",
    })
  );

  // Step 2: Encrypt the secret locally with the plaintext data key
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv("aes-256-gcm", Plaintext, iv);
  const encrypted = Buffer.concat([cipher.update(plaintextSecret, "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();

  // Step 3: Store only the encrypted secret + encrypted data key (never the plaintext DEK)
  return {
    encryptedSecret: encrypted.toString("base64"),
    encryptedDek: Buffer.from(CiphertextBlob).toString("base64"),
    iv: iv.toString("base64"),
    authTag: authTag.toString("base64"),
  };
}

Security Note: You should never log the plaintext DEK or the plaintext secret anywhere — not in CloudWatch, not in your APM tool. I made this mistake once and had to rotate every affected key plus file an internal security incident report.

This gives you an encrypted blob stored in your database with the DEK also encrypted by KMS. Even if someone dumps your database, they get nothing useful without KMS access.

[SOURCE: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping]


Step 2: Use a Dedicated Secret Manager for System-Level Secrets

For secrets your own application uses (database passwords, internal service tokens), skip the custom encryption and use AWS Secrets Manager or HashiCorp Vault directly.

# Store a secret in AWS Secrets Manager
aws secretsmanager create-secret \
  --name "prod/myapp/stripe-webhook-secret" \
  --secret-string '{"webhookSecret":"whsec_abc123..."}' \
  --region us-east-1

# Retrieve it at runtime (inject into env, don't hardcode)
aws secretsmanager get-secret-value \
  --secret-id "prod/myapp/stripe-webhook-secret" \
  --query SecretString \
  --output text

In my production setup, I never pass secrets as environment variables in CI pipelines. Instead, I use IAM roles attached to ECS tasks or Lambda functions, and the app fetches secrets at startup via the SDK. This removes secrets from your deployment artifacts entirely.


Step 3: Implement Per-Tenant Key Isolation

Each tenant’s secrets should be encrypted with a tenant-specific DEK. This means a compromise of one tenant’s encryption context cannot decrypt another tenant’s data.

# Pseudocode for per-tenant encryption context
def encrypt_tenant_api_key(tenant_id: str, api_key: str) -> dict:
    encryption_context = {"tenant_id": tenant_id, "purpose": "third-party-api-key"}

    response = kms_client.generate_data_key(
        KeyId=KMS_KEY_ARN,
        KeySpec="AES_256",
        EncryptionContext=encryption_context  # Tied to this tenant
    )
    # ... rest of encryption logic

The EncryptionContext in AWS KMS acts as additional authenticated data — decrypting with the wrong context will fail. This is a critical isolation layer in multi-tenant systems.


Step 4: Build a Rotation Strategy with a Grace Period

Key rotation is the most overlooked part of API key management. Most teams rotate keys manually, only after an incident. Instead, I build rotation into the system from day one.

Here’s the pattern I use: dual-key rotation with a grace period.

  1. Generate a new key and mark it as pending_primary.
  2. Set the old key status to pending_revocation with a revoke_at timestamp (e.g., 7 days out).
  3. Notify the tenant via email/webhook that rotation is happening.
  4. After the grace period, a scheduled job finalizes the revocation.
-- Schema for key rotation state machine
CREATE TABLE tenant_api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  encrypted_key TEXT NOT NULL,
  encrypted_dek TEXT NOT NULL,
  iv TEXT NOT NULL,
  auth_tag TEXT NOT NULL,
  status VARCHAR(20) NOT NULL CHECK (status IN ('active', 'pending_revocation', 'revoked')),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  revoke_at TIMESTAMPTZ,
  rotated_from UUID REFERENCES tenant_api_keys(id)
);
# Example cron job (runs daily via GitHub Actions or AWS EventBridge)
# Revokes keys past their revoke_at timestamp
node scripts/rotate-expired-keys.js --dry-run false

Pro Tip: Always run rotation jobs with --dry-run true in staging first. I once triggered a rotation job in production prematurely and locked out 12 tenants from their Stripe integrations. The dry-run flag saved me twice after that.


Step 5: Enforce Audit Logging for Compliance

For SOC 2 and HIPAA, you need to log who accessed what secret, when, and from where. AWS CloudTrail automatically logs KMS API calls, but you should also add application-level audit logs.

// Log every key access to your audit trail
async function logSecretAccess({ tenantId, secretType, action, userId, ipAddress }) {
  await db.auditLog.create({
    data: {
      tenant_id: tenantId,
      actor_id: userId,
      action,               // e.g., "api_key.read", "api_key.rotated"
      resource_type: secretType,
      ip_address: ipAddress,
      timestamp: new Date(),
    },
  });
}

[SOURCE: https://owasp.org/www-project-cheat-sheets/cheatsheets/Key_Management_Cheat_Sheet.html]


Real-World Tips I Use in Production

Tip 1: Cache decrypted secrets in memory, not in the database. Decrypting via KMS on every API request adds 10–30ms of latency. I use a short-lived in-memory cache (TTL: 5 minutes) with an LRU eviction policy.

Tip 2: Use dead letter queues for rotation jobs. Key rotation failures are silent killers. Any rotation worker should publish failures to an SQS DLQ or equivalent, with an alert that wakes someone up.

Tip 3: Separate read and write IAM policies. Your API servers should only have kms:Decrypt permission. Only your rotation service should have kms:GenerateDataKey. This limits what an attacker can do if they compromise your web tier.


How Do You Rotate API Keys Without Breaking Tenant Integrations?

This is the question I get most often, and it’s exactly what the grace period pattern is designed to solve. The short answer: you don’t rotate a key and immediately revoke the old one — you run both keys in parallel, notify tenants, and give them a window to migrate. The common errors below cover what happens when that process goes wrong.


Common Errors and How I Fixed Them

Error: InvalidCiphertextException on decryption This almost always means the EncryptionContext doesn’t match what was used during encryption. I hit this after a database migration that truncated the tenant_id field. Fix: always validate that context values haven’t changed before migrating data.

Error: ThrottlingException from KMS during high-traffic periods AWS KMS has a default quota of 5,000 requests/second per region. If you’re decrypting on every request without caching, you’ll hit this. Fix: implement the in-memory cache described above.

Error: Silent rotation failures leaving tenants locked out I discovered this after a Node.js uncaught exception swallowed a rotation error. Fix: always wrap rotation jobs in explicit try/catch, log to a structured error tracker, and alert on non-zero exit codes.


FAQ

Q: How do I store API keys securely in a multi-tenant SaaS without a dedicated secret manager? A: The minimum viable approach is AES-256-GCM encryption using a master key stored in an environment variable or hardware security module (HSM). That said, I strongly recommend graduating to AWS Secrets Manager or HashiCorp Vault as soon as possible — the operational overhead is low, and the compliance benefits are significant.

Q: What is the best API key rotation strategy for SaaS applications? A: The dual-key grace period pattern is the most tenant-friendly approach. You issue a new key, keep the old one valid for 7–14 days, notify the tenant, and then revoke the old key after the window. This gives tenants time to update their integrations without downtime.

Q: How often should API keys be rotated in a HIPAA-compliant SaaS? A: HIPAA doesn’t mandate a specific rotation interval, but NIST SP 800-57 recommends rotating symmetric keys at least annually, or immediately after a suspected compromise. For tenant-issued third-party API keys, I recommend offering on-demand rotation and automated rotation every 90 days.

Q: What is envelope encryption and why should I use it for API key storage? A: Envelope encryption separates the key used to encrypt data (DEK) from the key used to protect that DEK (KEK). The KEK lives in your cloud provider’s KMS and never touches your servers. This way, even a full database dump is useless without KMS access.

Q: How do I handle key rotation without locking out tenants in production? A: Use the grace period state machine described in Step 4. Notify tenants via email or webhook when rotation is scheduled, keep both keys valid during the grace period, and only revoke the old key after confirmation or after the deadline passes.


Conclusion

Secure API key management in a multi-tenant SaaS isn’t a one-time task — it’s an ongoing system. When I first built this out, I underestimated how much the rotation logic mattered. The encryption part is straightforward; the operational resilience around rotation, audit logging, and tenant communication is where most teams stumble.

Start with envelope encryption and a proper secret manager. Add per-tenant key isolation. Build rotation with a grace period. Log everything. That combination will put you in a strong position for SOC 2 audits and will protect your tenants if the worst happens.


About the Author

I’m a senior software engineer with over 10 years of experience building and securing cloud-native SaaS applications. My stack centers on Node.js, Python, AWS, and PostgreSQL, with a heavy focus on compliance-driven architecture for B2B platforms. I’ve led security reviews for SOC 2 Type II and HIPAA certifications and have a hard-won appreciation for the operational side of secret management that documentation rarely covers.