API Key Rotation Strategies for ChatGPT Apps
API key rotation is a critical security practice for ChatGPT applications that prevents unauthorized access, limits blast radius during security incidents, and ensures compliance with security standards. When you deploy a ChatGPT app to the App Store, your MCP server authenticates requests using API keys that, if compromised, could expose sensitive user data or allow malicious actors to abuse your service. Regular key rotation minimizes the window of opportunity for attackers while demonstrating security maturity to enterprise customers.
The challenge with key rotation lies in maintaining service availability during the transition period. A naive approach that immediately invalidates old keys will cause downtime for legitimate clients still using those credentials. Production-grade rotation strategies use dual-key support, graceful deprecation periods, and automated secret management to achieve zero-downtime key updates. This article presents comprehensive rotation strategies with production TypeScript implementations that you can deploy today, covering scheduled rotation, emergency rotation, secret vault integration, audit logging, and client communication workflows.
Modern ChatGPT apps integrate with enterprise systems that require SOC 2, ISO 27001, or HIPAA compliance—all of which mandate regular credential rotation. Beyond compliance, rotation reduces the impact of key leakage from logs, version control commits, or insider threats. By implementing the patterns in this guide, you'll build a rotation system that runs automatically, alerts your team during anomalies, and maintains detailed audit trails for security reviews. Let's explore how to architect secure, zero-downtime API key rotation for production ChatGPT applications.
Understanding Rotation Strategies
API key rotation strategies fall into three categories: scheduled rotation, emergency rotation, and automated rotation. Scheduled rotation follows a predetermined calendar (e.g., every 90 days) and represents proactive security hygiene. This approach allows you to plan rotation windows, communicate with API consumers in advance, and validate the rotation process during low-traffic periods. Most compliance frameworks require scheduled rotation as a baseline control.
Emergency rotation occurs in response to security incidents—compromised keys, unauthorized access, or employee departures. Unlike scheduled rotation, emergency rotation prioritizes speed over communication. You must immediately invalidate compromised credentials while minimizing service disruption. This requires pre-built automation that can execute rotation workflows on-demand without manual intervention. Your emergency rotation playbook should include automated key generation, secret distribution, client notification, and rollback procedures.
Automated rotation combines the predictability of scheduled rotation with the speed of emergency rotation. Tools like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault manage the entire rotation lifecycle: generating new keys, distributing them to authorized services, maintaining dual-key validity windows, and retiring old keys after the grace period expires. Automation eliminates human error, ensures consistent execution, and integrates with your CI/CD pipeline for seamless deployments.
The dual-key approach enables zero-downtime rotation by accepting both old and new keys during a transition period (typically 24-72 hours). When rotation begins, your system generates a new key but continues validating the old key. Clients gradually migrate to the new key, and once telemetry confirms 100% migration, the old key is retired. This pattern requires your authentication middleware to check multiple active keys and log which key each request uses, allowing you to monitor migration progress in real-time.
Manual vs. automated rotation represents a critical architectural decision. Manual rotation introduces consistency risks—forgotten rotation windows, human error during key distribution, or incomplete rollouts that leave some services using old credentials. Automated rotation, while requiring upfront engineering investment, provides long-term operational benefits: guaranteed rotation intervals, consistent audit trails, reduced operational burden, and faster incident response. Production ChatGPT apps serving thousands of users should always prioritize automation.
Rotation frequency depends on your threat model and compliance requirements. High-security environments (healthcare, finance) rotate every 30-60 days. Standard SaaS applications rotate every 90 days. Public-facing APIs with read-only access may extend to 180 days. Regardless of frequency, every rotation strategy must include validation testing, rollback procedures, and comprehensive monitoring to detect issues before they impact end users.
Implementing Zero-Downtime Rotation
Zero-downtime rotation requires a multi-phase approach that maintains service availability while transitioning to new credentials. The following TypeScript implementation demonstrates a production-ready rotation manager that coordinates key generation, dual-key validation, graceful cutover, and automated rollback.
// rotation-manager.ts - Zero-downtime API key rotation orchestrator
import { randomBytes, createHmac } from 'crypto';
import { EventEmitter } from 'events';
interface ApiKey {
id: string;
key: string;
prefix: string;
hash: string;
created: Date;
expires: Date;
status: 'active' | 'deprecating' | 'retired';
rotationId?: string;
metadata: {
environment: string;
purpose: string;
owner: string;
};
}
interface RotationConfig {
gracePeriodHours: number;
validationThreshold: number; // Percentage of requests using new key before retiring old
rollbackEnabled: boolean;
notificationWebhook?: string;
}
interface RotationMetrics {
oldKeyUsage: number;
newKeyUsage: number;
totalRequests: number;
migrationPercentage: number;
errorRate: number;
lastUpdated: Date;
}
class RotationManager extends EventEmitter {
private activeKeys: Map<string, ApiKey> = new Map();
private rotationMetrics: Map<string, RotationMetrics> = new Map();
private secretPrefix = 'mk_'; // MakeAIHQ key prefix
constructor(
private config: RotationConfig,
private keyStore: KeyStore,
private auditLogger: AuditLogger
) {
super();
this.startMetricsCollection();
}
/**
* Initiate scheduled or emergency rotation
* @param purpose - Reason for rotation (scheduled | emergency | manual)
*/
async rotateKey(
keyId: string,
purpose: 'scheduled' | 'emergency' | 'manual'
): Promise<{ oldKey: ApiKey; newKey: ApiKey }> {
const oldKey = this.activeKeys.get(keyId);
if (!oldKey) {
throw new Error(`Key ${keyId} not found`);
}
await this.auditLogger.log({
event: 'rotation_started',
keyId,
purpose,
timestamp: new Date(),
initiator: 'system'
});
try {
// Generate new key with cryptographically secure randomness
const newKey = await this.generateKey({
environment: oldKey.metadata.environment,
purpose: oldKey.metadata.purpose,
owner: oldKey.metadata.owner
});
// Calculate expiration based on rotation type
const gracePeriodMs = purpose === 'emergency'
? 3600000 // 1 hour for emergency
: this.config.gracePeriodHours * 3600000;
oldKey.status = 'deprecating';
oldKey.expires = new Date(Date.now() + gracePeriodMs);
oldKey.rotationId = newKey.id;
// Store both keys in dual-key mode
await this.keyStore.update(oldKey);
await this.keyStore.create(newKey);
this.activeKeys.set(oldKey.id, oldKey);
this.activeKeys.set(newKey.id, newKey);
// Initialize rotation metrics tracking
this.rotationMetrics.set(newKey.id, {
oldKeyUsage: 0,
newKeyUsage: 0,
totalRequests: 0,
migrationPercentage: 0,
errorRate: 0,
lastUpdated: new Date()
});
// Schedule automatic retirement after grace period
if (purpose !== 'emergency' || this.config.rollbackEnabled) {
this.scheduleRetirement(oldKey, newKey);
} else {
// Immediate retirement for emergency without rollback
await this.retireKey(oldKey.id);
}
this.emit('rotation_complete', { oldKey, newKey });
await this.auditLogger.log({
event: 'rotation_complete',
oldKeyId: oldKey.id,
newKeyId: newKey.id,
gracePeriodHours: gracePeriodMs / 3600000,
timestamp: new Date()
});
return { oldKey, newKey };
} catch (error) {
await this.auditLogger.log({
event: 'rotation_failed',
keyId,
error: error.message,
timestamp: new Date()
});
throw error;
}
}
/**
* Generate cryptographically secure API key
*/
private async generateKey(metadata: ApiKey['metadata']): Promise<ApiKey> {
const keyBytes = randomBytes(32);
const key = keyBytes.toString('base64url');
const keyId = randomBytes(16).toString('hex');
const hash = this.hashKey(key);
return {
id: keyId,
key: `${this.secretPrefix}${key}`,
prefix: `${this.secretPrefix}${key.substring(0, 8)}`,
hash,
created: new Date(),
expires: new Date(Date.now() + 365 * 24 * 3600000), // 1 year default
status: 'active',
metadata
};
}
/**
* Hash key for secure storage (never store plaintext)
*/
private hashKey(key: string): string {
return createHmac('sha256', process.env.KEY_HASH_SECRET!)
.update(key)
.digest('hex');
}
/**
* Validate incoming request using dual-key support
*/
async validateKey(providedKey: string): Promise<{
valid: boolean;
keyId?: string;
status?: ApiKey['status'];
warning?: string;
}> {
const hash = this.hashKey(providedKey);
for (const [keyId, key] of this.activeKeys) {
if (key.hash === hash) {
// Track which key was used for migration metrics
this.trackKeyUsage(keyId);
if (key.status === 'retired') {
return {
valid: false,
keyId,
status: 'retired',
warning: 'Key has been retired. Please use the new key.'
};
}
if (key.status === 'deprecating') {
return {
valid: true,
keyId,
status: 'deprecating',
warning: `Key is deprecated and will expire on ${key.expires.toISOString()}`
};
}
return { valid: true, keyId, status: 'active' };
}
}
await this.auditLogger.log({
event: 'invalid_key_attempt',
keyPrefix: providedKey.substring(0, 16),
timestamp: new Date()
});
return { valid: false };
}
/**
* Track key usage for migration monitoring
*/
private trackKeyUsage(keyId: string): void {
const key = this.activeKeys.get(keyId);
if (!key || !key.rotationId) return;
const metrics = this.rotationMetrics.get(key.rotationId);
if (!metrics) return;
metrics.totalRequests++;
if (key.status === 'deprecating') {
metrics.oldKeyUsage++;
} else {
metrics.newKeyUsage++;
}
metrics.migrationPercentage =
(metrics.newKeyUsage / metrics.totalRequests) * 100;
metrics.lastUpdated = new Date();
this.emit('metrics_updated', { keyId, metrics });
}
/**
* Schedule automatic retirement after grace period
*/
private scheduleRetirement(oldKey: ApiKey, newKey: ApiKey): void {
const retirementDelay = oldKey.expires.getTime() - Date.now();
setTimeout(async () => {
const metrics = this.rotationMetrics.get(newKey.id);
if (!metrics) {
await this.retireKey(oldKey.id);
return;
}
// Check if migration threshold met
if (metrics.migrationPercentage >= this.config.validationThreshold) {
await this.retireKey(oldKey.id);
} else {
// Extend grace period if migration incomplete
await this.auditLogger.log({
event: 'retirement_delayed',
oldKeyId: oldKey.id,
newKeyId: newKey.id,
migrationPercentage: metrics.migrationPercentage,
threshold: this.config.validationThreshold,
timestamp: new Date()
});
oldKey.expires = new Date(Date.now() + 24 * 3600000); // Extend 24h
await this.keyStore.update(oldKey);
this.scheduleRetirement(oldKey, newKey);
}
}, retirementDelay);
}
/**
* Retire old key after successful migration
*/
private async retireKey(keyId: string): Promise<void> {
const key = this.activeKeys.get(keyId);
if (!key) return;
key.status = 'retired';
await this.keyStore.update(key);
this.activeKeys.delete(keyId);
await this.auditLogger.log({
event: 'key_retired',
keyId,
timestamp: new Date()
});
this.emit('key_retired', { keyId });
}
/**
* Emergency rollback to old key
*/
async rollback(rotationId: string): Promise<void> {
if (!this.config.rollbackEnabled) {
throw new Error('Rollback not enabled in configuration');
}
const newKey = this.activeKeys.get(rotationId);
if (!newKey) {
throw new Error(`Rotation ${rotationId} not found`);
}
// Find the old key being replaced
let oldKey: ApiKey | undefined;
for (const key of this.activeKeys.values()) {
if (key.rotationId === rotationId) {
oldKey = key;
break;
}
}
if (!oldKey) {
throw new Error('Original key not found for rollback');
}
// Reactivate old key
oldKey.status = 'active';
oldKey.expires = new Date(Date.now() + 365 * 24 * 3600000);
delete oldKey.rotationId;
// Retire new key
newKey.status = 'retired';
await this.keyStore.update(oldKey);
await this.keyStore.update(newKey);
this.activeKeys.delete(newKey.id);
await this.auditLogger.log({
event: 'rotation_rollback',
oldKeyId: oldKey.id,
newKeyId: newKey.id,
timestamp: new Date()
});
this.emit('rollback_complete', { oldKey, newKey });
}
/**
* Start background metrics collection
*/
private startMetricsCollection(): void {
setInterval(() => {
for (const [rotationId, metrics] of this.rotationMetrics) {
this.emit('metrics_snapshot', { rotationId, metrics });
}
}, 60000); // Every minute
}
/**
* Get current rotation metrics
*/
getRotationMetrics(rotationId: string): RotationMetrics | undefined {
return this.rotationMetrics.get(rotationId);
}
/**
* List all active keys
*/
listKeys(status?: ApiKey['status']): ApiKey[] {
const keys = Array.from(this.activeKeys.values());
return status ? keys.filter(k => k.status === status) : keys;
}
}
// Example usage
interface KeyStore {
create(key: ApiKey): Promise<void>;
update(key: ApiKey): Promise<void>;
get(id: string): Promise<ApiKey | null>;
}
interface AuditLogger {
log(event: any): Promise<void>;
}
export { RotationManager, ApiKey, RotationConfig, RotationMetrics };
This rotation manager implements dual-key validation by maintaining both old and new keys in the activeKeys map during the grace period. The validateKey method checks all active keys and returns deprecation warnings for keys in transition. Metrics tracking monitors migration progress, allowing automated retirement only after the configured percentage of requests use the new key.
The graceful cutover mechanism uses scheduleRetirement to automatically retire old keys after the grace period expires. If migration metrics show incomplete adoption (below validationThreshold), the system automatically extends the grace period and sends alerts. This prevents premature retirement that would cause authentication failures for clients still using old keys.
Rollback support allows reverting to the old key if the new key causes issues. The rollback method reactivates the deprecated key and retires the new key, with full audit logging for compliance tracking. Emergency rotations can disable rollback for immediate key invalidation during security incidents.
Secret Management Integration
Production ChatGPT apps require centralized secret management to avoid hardcoding API keys in environment variables or configuration files. HashiCorp Vault, AWS KMS, and Azure Key Vault provide encrypted storage, automatic rotation, and fine-grained access controls. The following implementation integrates Vault's KV v2 secrets engine with the rotation manager.
// vault-secret-manager.ts - HashiCorp Vault integration for API key storage
import axios, { AxiosInstance } from 'axios';
import { ApiKey } from './rotation-manager';
interface VaultConfig {
address: string;
token?: string;
namespace?: string;
mount: string; // KV mount path (e.g., 'secret')
path: string; // Path within mount (e.g., 'chatgpt/api-keys')
}
interface VaultSecret {
data: {
data: Record<string, any>;
metadata: {
created_time: string;
deletion_time: string;
destroyed: boolean;
version: number;
};
};
}
interface VaultListResponse {
data: {
keys: string[];
};
}
class VaultSecretManager {
private client: AxiosInstance;
private kvPath: string;
constructor(private config: VaultConfig) {
this.client = axios.create({
baseURL: config.address,
headers: {
'X-Vault-Token': config.token || process.env.VAULT_TOKEN,
...(config.namespace && { 'X-Vault-Namespace': config.namespace })
},
timeout: 5000
});
this.kvPath = `/v1/${config.mount}/data/${config.path}`;
}
/**
* Store API key in Vault with encryption at rest
*/
async storeKey(key: ApiKey): Promise<void> {
try {
await this.client.post(this.kvPath + `/${key.id}`, {
data: {
key: key.key,
hash: key.hash,
prefix: key.prefix,
status: key.status,
created: key.created.toISOString(),
expires: key.expires.toISOString(),
rotationId: key.rotationId,
metadata: key.metadata
}
});
console.log(`[Vault] Stored key ${key.id} at ${this.kvPath}/${key.id}`);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`Vault storage failed: ${error.response?.data?.errors?.join(', ') || error.message}`
);
}
throw error;
}
}
/**
* Retrieve API key from Vault
*/
async getKey(keyId: string): Promise<ApiKey | null> {
try {
const response = await this.client.get<VaultSecret>(
this.kvPath + `/${keyId}`
);
const data = response.data.data.data;
return {
id: keyId,
key: data.key,
hash: data.hash,
prefix: data.prefix,
created: new Date(data.created),
expires: new Date(data.expires),
status: data.status,
rotationId: data.rotationId,
metadata: data.metadata
};
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
}
/**
* Update existing key in Vault
*/
async updateKey(key: ApiKey): Promise<void> {
await this.storeKey(key); // KV v2 creates new version
}
/**
* Delete key from Vault (soft delete - creates deletion marker)
*/
async deleteKey(keyId: string): Promise<void> {
try {
await this.client.delete(this.kvPath + `/${keyId}`);
console.log(`[Vault] Deleted key ${keyId}`);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`Vault deletion failed: ${error.response?.data?.errors?.join(', ') || error.message}`
);
}
throw error;
}
}
/**
* List all keys in Vault
*/
async listKeys(): Promise<string[]> {
try {
const listPath = `/v1/${this.config.mount}/metadata/${this.config.path}`;
const response = await this.client.request<VaultListResponse>({
method: 'LIST',
url: listPath
});
return response.data.data.keys || [];
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return [];
}
throw error;
}
}
/**
* Enable automated rotation policy in Vault Enterprise
*/
async enableAutoRotation(keyId: string, rotationPeriod: string): Promise<void> {
const policyPath = `/v1/${this.config.mount}/rotate/${this.config.path}/${keyId}`;
try {
await this.client.post(policyPath, {
rotation_period: rotationPeriod // e.g., "2160h" for 90 days
});
console.log(`[Vault] Enabled auto-rotation for ${keyId} (${rotationPeriod})`);
} catch (error) {
console.warn('[Vault] Auto-rotation not available (requires Enterprise)');
}
}
/**
* Renew Vault token to prevent expiration during long-running rotations
*/
async renewToken(): Promise<void> {
try {
await this.client.post('/v1/auth/token/renew-self');
console.log('[Vault] Token renewed');
} catch (error) {
console.error('[Vault] Token renewal failed:', error);
throw new Error('Vault token expired - rotation blocked');
}
}
/**
* Create encrypted backup of all keys
*/
async backupKeys(): Promise<Record<string, ApiKey>> {
const keyIds = await this.listKeys();
const backup: Record<string, ApiKey> = {};
for (const keyId of keyIds) {
const key = await this.getKey(keyId);
if (key && key.status !== 'retired') {
backup[keyId] = key;
}
}
return backup;
}
/**
* Health check for Vault connectivity
*/
async healthCheck(): Promise<{
healthy: boolean;
sealed: boolean;
version: string;
}> {
try {
const response = await this.client.get('/v1/sys/health');
return {
healthy: response.status === 200,
sealed: false,
version: response.data.version
};
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 503) {
return {
healthy: false,
sealed: true,
version: error.response.data.version
};
}
throw error;
}
}
}
// KeyStore implementation using Vault
class VaultKeyStore {
constructor(private vault: VaultSecretManager) {}
async create(key: ApiKey): Promise<void> {
await this.vault.storeKey(key);
}
async update(key: ApiKey): Promise<void> {
await this.vault.updateKey(key);
}
async get(id: string): Promise<ApiKey | null> {
return await this.vault.getKey(id);
}
async delete(id: string): Promise<void> {
await this.vault.deleteKey(id);
}
async list(): Promise<ApiKey[]> {
const keyIds = await this.vault.listKeys();
const keys: ApiKey[] = [];
for (const keyId of keyIds) {
const key = await this.vault.getKey(keyId);
if (key) keys.push(key);
}
return keys;
}
}
// Example usage with rotation manager
async function setupRotationWithVault() {
const vault = new VaultSecretManager({
address: 'https://vault.makeaihq.com',
mount: 'secret',
path: 'chatgpt/api-keys',
namespace: 'production'
});
// Health check before starting
const health = await vault.healthCheck();
if (!health.healthy) {
throw new Error('Vault is unhealthy or sealed');
}
const keyStore = new VaultKeyStore(vault);
const auditLogger = new VaultAuditLogger(vault);
const rotationManager = new RotationManager(
{
gracePeriodHours: 72,
validationThreshold: 95,
rollbackEnabled: true
},
keyStore,
auditLogger
);
// Schedule automatic rotation every 90 days
setInterval(async () => {
const keys = await keyStore.list();
for (const key of keys) {
const age = Date.now() - key.created.getTime();
const ninetyDays = 90 * 24 * 3600000;
if (age >= ninetyDays && key.status === 'active') {
console.log(`[Rotation] Auto-rotating key ${key.id} (90 days old)`);
await rotationManager.rotateKey(key.id, 'scheduled');
}
}
}, 24 * 3600000); // Check daily
return { rotationManager, vault };
}
class VaultAuditLogger {
constructor(private vault: VaultSecretManager) {}
async log(event: any): Promise<void> {
// Store audit logs in separate Vault path
const auditPath = `/v1/secret/data/chatgpt/audit-logs/${Date.now()}`;
await (this.vault as any).client.post(auditPath, { data: event });
}
}
export { VaultSecretManager, VaultKeyStore, setupRotationWithVault };
The Vault integration provides encrypted storage for API keys using HashiCorp Vault's KV v2 secrets engine. Keys are never stored in plaintext on disk or in environment variables. The VaultSecretManager handles authentication, secret versioning, and automatic token renewal to prevent interruptions during long-running rotations.
KV v2 versioning maintains historical key versions, allowing rollback to previous keys if needed. Soft deletes create deletion markers without permanently destroying secrets, satisfying compliance requirements for audit trails. The backupKeys method creates encrypted snapshots for disaster recovery scenarios.
Health checks (healthCheck) validate Vault connectivity before rotation operations. If Vault becomes sealed or unreachable, the rotation manager prevents new rotations and alerts operators. Token renewal (renewToken) ensures long-running rotation processes don't fail due to expired authentication tokens.
AWS KMS Encryption Wrapper
For teams using AWS infrastructure, AWS Key Management Service (KMS) provides hardware-backed encryption for API keys stored in DynamoDB, S3, or Secrets Manager. The following implementation wraps KMS encryption operations for secure key storage.
// kms-encryption.ts - AWS KMS integration for API key encryption
import {
KMSClient,
EncryptCommand,
DecryptCommand,
GenerateDataKeyCommand,
CreateKeyCommand,
DescribeKeyCommand
} from '@aws-sdk/client-kms';
import { SecretsManagerClient, CreateSecretCommand, GetSecretValueCommand, UpdateSecretCommand } from '@aws-sdk/client-secrets-manager';
interface KMSConfig {
region: string;
keyId: string; // KMS key ARN or alias
encryptionContext?: Record<string, string>;
}
class KMSEncryptionManager {
private kmsClient: KMSClient;
private secretsClient: SecretsManagerClient;
constructor(private config: KMSConfig) {
this.kmsClient = new KMSClient({ region: config.region });
this.secretsClient = new SecretsManagerClient({ region: config.region });
}
/**
* Encrypt API key using KMS
*/
async encryptKey(plaintext: string): Promise<string> {
const command = new EncryptCommand({
KeyId: this.config.keyId,
Plaintext: Buffer.from(plaintext, 'utf-8'),
EncryptionContext: this.config.encryptionContext
});
const response = await this.kmsClient.send(command);
if (!response.CiphertextBlob) {
throw new Error('KMS encryption failed');
}
return Buffer.from(response.CiphertextBlob).toString('base64');
}
/**
* Decrypt API key using KMS
*/
async decryptKey(ciphertext: string): Promise<string> {
const command = new DecryptCommand({
CiphertextBlob: Buffer.from(ciphertext, 'base64'),
EncryptionContext: this.config.encryptionContext
});
const response = await this.kmsClient.send(command);
if (!response.Plaintext) {
throw new Error('KMS decryption failed');
}
return Buffer.from(response.Plaintext).toString('utf-8');
}
/**
* Store encrypted key in AWS Secrets Manager
*/
async storeInSecretsManager(keyId: string, key: string, metadata: any): Promise<void> {
const encryptedKey = await this.encryptKey(key);
try {
await this.secretsClient.send(new CreateSecretCommand({
Name: `chatgpt/api-keys/${keyId}`,
SecretString: JSON.stringify({
encryptedKey,
metadata,
encryptedAt: new Date().toISOString()
}),
KmsKeyId: this.config.keyId
}));
} catch (error: any) {
if (error.name === 'ResourceExistsException') {
// Update existing secret
await this.secretsClient.send(new UpdateSecretCommand({
SecretId: `chatgpt/api-keys/${keyId}`,
SecretString: JSON.stringify({
encryptedKey,
metadata,
encryptedAt: new Date().toISOString()
}),
KmsKeyId: this.config.keyId
}));
} else {
throw error;
}
}
}
/**
* Retrieve decrypted key from Secrets Manager
*/
async getFromSecretsManager(keyId: string): Promise<{ key: string; metadata: any } | null> {
try {
const response = await this.secretsClient.send(new GetSecretValueCommand({
SecretId: `chatgpt/api-keys/${keyId}`
}));
if (!response.SecretString) return null;
const secret = JSON.parse(response.SecretString);
const key = await this.decryptKey(secret.encryptedKey);
return { key, metadata: secret.metadata };
} catch (error: any) {
if (error.name === 'ResourceNotFoundException') {
return null;
}
throw error;
}
}
/**
* Rotate KMS data key (envelope encryption)
*/
async rotateDataKey(): Promise<{ plaintextKey: Buffer; encryptedKey: Buffer }> {
const command = new GenerateDataKeyCommand({
KeyId: this.config.keyId,
KeySpec: 'AES_256',
EncryptionContext: this.config.encryptionContext
});
const response = await this.kmsClient.send(command);
if (!response.Plaintext || !response.CiphertextBlob) {
throw new Error('Data key generation failed');
}
return {
plaintextKey: Buffer.from(response.Plaintext),
encryptedKey: Buffer.from(response.CiphertextBlob)
};
}
}
export { KMSEncryptionManager };
The KMS encryption wrapper uses envelope encryption: data keys encrypt API keys locally, while KMS encrypts the data keys. This pattern reduces KMS API calls and latency. Encryption context provides additional security by binding encrypted data to specific metadata (e.g., key ID, environment).
Audit Logging System
Comprehensive audit logging tracks all rotation events, key access patterns, and security incidents. The following implementation creates structured logs compatible with SIEM systems and compliance frameworks.
// audit-logger.ts - Comprehensive rotation audit logging
import { EventEmitter } from 'events';
import { createWriteStream, WriteStream } from 'fs';
import { join } from 'path';
interface AuditEvent {
event: string;
timestamp: Date;
keyId?: string;
userId?: string;
ipAddress?: string;
userAgent?: string;
success: boolean;
details?: Record<string, any>;
severity: 'info' | 'warning' | 'error' | 'critical';
}
class RotationAuditLogger extends EventEmitter {
private logStream: WriteStream;
private eventBuffer: AuditEvent[] = [];
private flushInterval: NodeJS.Timeout;
constructor(
private logPath: string,
private options: {
bufferSize: number;
flushIntervalMs: number;
syslogEndpoint?: string;
}
) {
super();
this.logStream = createWriteStream(join(logPath, 'rotation-audit.log'), {
flags: 'a'
});
this.flushInterval = setInterval(() => this.flush(), options.flushIntervalMs);
}
/**
* Log rotation event with structured data
*/
async log(event: Partial<AuditEvent>): Promise<void> {
const auditEvent: AuditEvent = {
event: event.event || 'unknown',
timestamp: new Date(),
success: event.success ?? true,
severity: event.severity || 'info',
...event
};
this.eventBuffer.push(auditEvent);
this.emit('audit_event', auditEvent);
if (this.eventBuffer.length >= this.options.bufferSize) {
await this.flush();
}
// Send critical events immediately
if (auditEvent.severity === 'critical') {
await this.sendToSyslog(auditEvent);
}
}
/**
* Flush buffered events to log file
*/
private async flush(): Promise<void> {
if (this.eventBuffer.length === 0) return;
const events = [...this.eventBuffer];
this.eventBuffer = [];
for (const event of events) {
this.logStream.write(JSON.stringify(event) + '\n');
}
}
/**
* Send high-severity events to external SIEM
*/
private async sendToSyslog(event: AuditEvent): Promise<void> {
if (!this.options.syslogEndpoint) return;
try {
await fetch(this.options.syslogEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
} catch (error) {
console.error('[Audit] Failed to send to syslog:', error);
}
}
/**
* Query audit logs by key ID
*/
async queryByKeyId(keyId: string, startDate?: Date, endDate?: Date): Promise<AuditEvent[]> {
// In production, query from database or log aggregation service
return this.eventBuffer.filter(e =>
e.keyId === keyId &&
(!startDate || e.timestamp >= startDate) &&
(!endDate || e.timestamp <= endDate)
);
}
/**
* Generate compliance report
*/
async generateComplianceReport(startDate: Date, endDate: Date): Promise<{
totalRotations: number;
scheduledRotations: number;
emergencyRotations: number;
failedRotations: number;
averageRotationTime: number;
}> {
const events = this.eventBuffer.filter(e =>
e.timestamp >= startDate && e.timestamp <= endDate
);
const rotationStarts = events.filter(e => e.event === 'rotation_started');
const rotationCompletes = events.filter(e => e.event === 'rotation_complete');
const rotationFailures = events.filter(e => e.event === 'rotation_failed');
return {
totalRotations: rotationCompletes.length,
scheduledRotations: rotationStarts.filter(e => e.details?.purpose === 'scheduled').length,
emergencyRotations: rotationStarts.filter(e => e.details?.purpose === 'emergency').length,
failedRotations: rotationFailures.length,
averageRotationTime: 0 // Calculate from timestamps
};
}
/**
* Close logger and flush remaining events
*/
async close(): Promise<void> {
clearInterval(this.flushInterval);
await this.flush();
this.logStream.end();
}
}
export { RotationAuditLogger, AuditEvent };
The audit logger buffers events in memory and flushes to disk periodically, balancing write performance with data durability. Critical events (failed rotations, invalid key attempts) trigger immediate logging and optional SIEM integration.
Client Communication System
Effective rotation requires proactive client communication. The following implementation manages deprecation notices, migration guides, and automated email notifications.
// client-notifier.ts - Automated client communication for key rotation
import nodemailer from 'nodemailer';
import { ApiKey } from './rotation-manager';
interface NotificationConfig {
smtpHost: string;
smtpPort: number;
smtpUser: string;
smtpPass: string;
fromEmail: string;
supportEmail: string;
}
class ClientNotificationSystem {
private transporter: nodemailer.Transporter;
constructor(private config: NotificationConfig) {
this.transporter = nodemailer.createTransport({
host: config.smtpHost,
port: config.smtpPort,
secure: config.smtpPort === 465,
auth: {
user: config.smtpUser,
pass: config.smtpPass
}
});
}
/**
* Send deprecation notice when rotation starts
*/
async sendDeprecationNotice(
clientEmail: string,
oldKey: ApiKey,
newKey: ApiKey
): Promise<void> {
const gracePeriodHours = Math.ceil(
(oldKey.expires.getTime() - Date.now()) / 3600000
);
const html = `
<h2>API Key Rotation Notice</h2>
<p>Dear Developer,</p>
<p>Your MakeAIHQ API key is being rotated for security compliance.</p>
<h3>Action Required</h3>
<p>Please update your ChatGPT app to use the new API key before <strong>${oldKey.expires.toISOString()}</strong>.</p>
<h3>Migration Steps</h3>
<ol>
<li>Log in to <a href="https://makeaihq.com/dashboard/settings">MakeAIHQ Dashboard</a></li>
<li>Navigate to Settings → API Keys</li>
<li>Copy the new API key: <code>${newKey.prefix}...</code></li>
<li>Update your MCP server configuration</li>
<li>Verify connectivity using the <a href="https://makeaihq.com/docs/testing-api-keys">testing guide</a></li>
</ol>
<h3>Grace Period</h3>
<p>Your old key (${oldKey.prefix}...) will continue working for <strong>${gracePeriodHours} hours</strong>.</p>
<h3>Need Help?</h3>
<p>Contact our support team at <a href="mailto:${this.config.supportEmail}">${this.config.supportEmail}</a></p>
<p>Best regards,<br>MakeAIHQ Security Team</p>
`;
await this.transporter.sendMail({
from: this.config.fromEmail,
to: clientEmail,
subject: `Action Required: API Key Rotation (${gracePeriodHours}h grace period)`,
html
});
}
/**
* Send emergency rotation alert
*/
async sendEmergencyRotationAlert(
clientEmail: string,
reason: string
): Promise<void> {
const html = `
<h2 style="color: #d32f2f;">⚠️ Emergency API Key Rotation</h2>
<p>Dear Developer,</p>
<p>We have immediately rotated your API key due to: <strong>${reason}</strong></p>
<h3>Immediate Action Required</h3>
<p>Your old API key has been <strong>disabled immediately</strong> for security reasons.</p>
<ol>
<li>Log in to <a href="https://makeaihq.com/dashboard/settings">MakeAIHQ Dashboard</a> now</li>
<li>Retrieve your new API key from Settings → API Keys</li>
<li>Update your ChatGPT app configuration immediately</li>
</ol>
<p>If you did not request this rotation, contact us immediately at ${this.config.supportEmail}</p>
<p>Best regards,<br>MakeAIHQ Security Team</p>
`;
await this.transporter.sendMail({
from: this.config.fromEmail,
to: clientEmail,
subject: '⚠️ URGENT: API Key Disabled - Action Required',
html
});
}
/**
* Send migration reminder (48h before expiry)
*/
async sendMigrationReminder(
clientEmail: string,
oldKey: ApiKey,
hoursRemaining: number
): Promise<void> {
const html = `
<h2 style="color: #ff9800;">Reminder: API Key Expiring in ${hoursRemaining} Hours</h2>
<p>Dear Developer,</p>
<p>Your old API key (${oldKey.prefix}...) will stop working in <strong>${hoursRemaining} hours</strong>.</p>
<p>If you have not yet migrated to the new key, please do so immediately:</p>
<ol>
<li>Dashboard: <a href="https://makeaihq.com/dashboard/settings">https://makeaihq.com/dashboard/settings</a></li>
<li>Copy new API key from Settings → API Keys</li>
<li>Update your MCP server</li>
</ol>
<p>Best regards,<br>MakeAIHQ Team</p>
`;
await this.transporter.sendMail({
from: this.config.fromEmail,
to: clientEmail,
subject: `⏰ API Key Expires in ${hoursRemaining}h`,
html
});
}
}
export { ClientNotificationSystem };
The notification system sends deprecation notices when rotation starts, migration reminders as expiry approaches, and emergency alerts for immediate key disablement. Emails include migration guides, grace period details, and support contact information.
Emergency Rotation Handler
Emergency rotations respond to security incidents with immediate key invalidation. The following implementation orchestrates rapid rotation with minimal service disruption.
// emergency-rotation.ts - Rapid response for compromised keys
import { RotationManager } from './rotation-manager';
import { ClientNotificationSystem } from './client-notifier';
import { AuditLogger } from './audit-logger';
interface EmergencyRotationOptions {
reason: string;
immediateInvalidation: boolean;
notifyClients: boolean;
alertSecurity: boolean;
}
class EmergencyRotationHandler {
constructor(
private rotationManager: RotationManager,
private notifier: ClientNotificationSystem,
private auditLogger: AuditLogger
) {}
/**
* Execute emergency rotation workflow
*/
async executeEmergencyRotation(
keyId: string,
options: EmergencyRotationOptions
): Promise<void> {
const startTime = Date.now();
await this.auditLogger.log({
event: 'emergency_rotation_initiated',
keyId,
reason: options.reason,
timestamp: new Date(),
severity: 'critical'
});
try {
// Rotate with 1-hour grace period (or immediate)
const { oldKey, newKey } = await this.rotationManager.rotateKey(
keyId,
'emergency'
);
if (options.immediateInvalidation) {
// Skip grace period - retire immediately
await (this.rotationManager as any).retireKey(oldKey.id);
}
// Send alerts
if (options.notifyClients) {
// Get client email from key metadata
const clientEmail = oldKey.metadata.owner;
await this.notifier.sendEmergencyRotationAlert(
clientEmail,
options.reason
);
}
if (options.alertSecurity) {
await this.sendSecurityAlert(keyId, options.reason);
}
const duration = Date.now() - startTime;
await this.auditLogger.log({
event: 'emergency_rotation_complete',
keyId,
newKeyId: newKey.id,
duration,
timestamp: new Date(),
severity: 'critical'
});
console.log(`[Emergency] Rotation complete in ${duration}ms`);
} catch (error) {
await this.auditLogger.log({
event: 'emergency_rotation_failed',
keyId,
error: (error as Error).message,
timestamp: new Date(),
severity: 'critical'
});
throw error;
}
}
/**
* Send alert to security team
*/
private async sendSecurityAlert(keyId: string, reason: string): Promise<void> {
// Integration with PagerDuty, Slack, etc.
console.log(`[Security Alert] Emergency rotation: ${keyId} - ${reason}`);
}
}
export { EmergencyRotationHandler };
The emergency handler coordinates rapid rotation, immediate key invalidation, client notification, and security team alerts. Audit logs capture exact timings for incident response post-mortems.
Conclusion: Building Secure Rotation Systems
API key rotation is non-negotiable for production ChatGPT apps that handle enterprise data. The strategies and implementations in this guide provide a complete rotation system with zero-downtime cutover, automated secret management, comprehensive audit logging, and proactive client communication. By implementing dual-key support, your rotation processes maintain service availability while transitioning credentials. Vault and KMS integrations ensure keys are never stored in plaintext, satisfying compliance requirements for SOC 2, ISO 27001, and HIPAA.
Start with scheduled rotation every 90 days, then build emergency rotation workflows for incident response. Automate rotation scheduling, metrics collection, and client notifications to eliminate manual operational burden. Monitor migration metrics during grace periods to validate successful adoption before retiring old keys. Test rollback procedures regularly to ensure you can recover from failed rotations without extended downtime.
Ready to build production-grade ChatGPT apps with enterprise security? MakeAIHQ provides automated API key rotation, secret management integration, and compliance-ready audit logging—no coding required. Our platform generates rotation-ready MCP servers with Vault integration, KMS encryption, and automated client notifications built-in. Join thousands of developers shipping secure ChatGPT apps to the App Store with zero security headaches. Start your free trial today and deploy your first rotation-enabled ChatGPT app in 48 hours.
Related Resources
- ChatGPT App Security Hardening Guide - Complete security architecture for production apps
- OAuth Client Credentials Flow for ChatGPT - Implement OAuth 2.1 authentication
- Secret Management with Vault for ChatGPT Apps - HashiCorp Vault integration guide
- API Security Best Practices for ChatGPT - Comprehensive API security patterns
- API Rate Limiting Strategies - Protect against abuse and DDoS
- Monitoring and Alerting for ChatGPT Apps - Production observability patterns
External References
- HashiCorp Vault KV Secrets Engine - Official Vault documentation for KV v2 secrets
- AWS KMS Developer Guide - AWS Key Management Service best practices
- OWASP API Security Top 10 - API security best practices and rotation requirements