Skip to content

API Keys Management

This document describes how API keys work for organization integrations, including encryption, generation, rotation, and usage.

Overview

Organizations use API keys to integrate with external services. These keys are:

  • Stored encrypted in the database using AES-256-GCM
  • Organization-scoped - Each organization has its own set of keys
  • Service-specific - Keys are tied to specific integration services
  • Admin-only access - Only organization admins can view or manage keys

Database Storage

Schema

API keys are stored in the organization_integrations table:

sql
CREATE TABLE organization_integrations (
    id              BIGSERIAL PRIMARY KEY,
    organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    title           TEXT,                               -- Human-readable name
    service         integration_service NOT NULL,       -- enum of supported external services
    api_key_encrypted BYTEA NOT NULL,                   -- AES-256-GCM encrypted
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Field Details

FieldTypeDescription
idBIGSERIALPrimary key
organization_idBIGINTForeign key to organizations table
titleTEXTHuman-readable label (e.g., "CRM Production", "CRM Staging")
serviceintegration_serviceEnum of supported external services
api_key_encryptedBYTEAEncrypted API key (binary data)
created_atTIMESTAMPTZWhen the key was created
updated_atTIMESTAMPTZWhen the key was last updated

Encryption

Algorithm

API keys are encrypted using AES-256-GCM (Galois/Counter Mode), which provides:

  • Confidentiality - Data is encrypted and unreadable without the key
  • Integrity - Tampered ciphertext will fail authentication
  • Authentication - Ensures data hasn't been modified

Storage Format

The encrypted data is stored as PostgreSQL bytea (binary data):

[Nonce (12 bytes)][Ciphertext (variable)][Auth Tag (16 bytes)]
  • Nonce: Random value generated for each encryption operation
  • Ciphertext: The encrypted API key
  • Auth Tag: Authentication tag for verifying integrity

Encryption Key Management

The encryption key is stored as an environment variable on the application server:

bash
API_KEY_ENCRYPTION_KEY=hex_encoded_256_bit_key_here

Important:

  • The encryption key MUST be 32 bytes (256 bits)
  • Store it in a secure secret manager (AWS Secrets Manager, HashiCorp Vault, etc.)
  • Never commit encryption keys to version control
  • Rotate encryption keys periodically (requires re-encrypting all API keys)

Encryption Flow

  1. Application receives plaintext API key (e.g., from admin input or external API)
  2. Generate random 12-byte nonce
  3. Encrypt using AES-256-GCM:
    • Key: From environment variable
    • Nonce: Randomly generated
    • Plaintext: The API key
    • Associated Data: Organization ID (prevents key substitution attacks)
  4. Concatenate: [nonce][ciphertext][auth_tag]
  5. Store as BYTEA in api_key_encrypted column

Decryption Flow

  1. Retrieve encrypted data from database
  2. Extract components:
    • First 12 bytes = nonce
    • Last 16 bytes = auth tag
    • Middle bytes = ciphertext
  3. Decrypt using AES-256-GCM:
    • Key: From environment variable
    • Nonce: Extracted from data
    • Ciphertext: Extracted from data
    • Auth Tag: Extracted from data
    • Associated Data: Organization ID
  4. Verify auth tag (automatic in GCM mode)
  5. Return plaintext API key

If the auth tag verification fails, decryption is aborted (indicates tampering).

Generation

Creating a New API Key Integration

  1. Admin obtains API key from external service
  2. Admin creates integration via UI or API:
    bash
    POST /v1/organizations/1/integrations
    Content-Type: application/json
    
    {
      "title": "Service Production",
      "service": "service_name",
      "api_key": "sk_live_abc123def456ghi789jkl012mno345pqr678"
    }
  3. Backend encrypts the key and stores it:
    go
    encrypted, err := crypto.EncryptAPIKey(orgID, apiKey)
    if err != nil {
        return err
    }
    
    integration := &models.OrganizationIntegration{
        OrganizationID: orgID,
        Title:          "Service Production",
        Service:        "service_name",
        APIKeyEncrypted: encrypted,
    }
    
    db.Create(integration)

Supported Services

Currently supported integration services (enum integration_service):

  • (none defined yet — extend the enum as integrations are added)

To add a new service:

  1. Add to the integration_service enum in the database
  2. Update application code to handle the new service
  3. Document the service-specific API key format and requirements

Rotation

Why Rotate API Keys?

  • Security best practice - Regular rotation limits exposure window
  • Compliance requirements - Some regulations require periodic key rotation
  • Breach response - Immediate rotation if a key is compromised
  • Staff changes - Rotate when admin access changes

Rotation Process

  1. Generate new API key in external service
  2. Update integration via API:
    bash
    PUT /v1/organizations/1/integrations/5
    Content-Type: application/json
    
    {
      "api_key": "sk_live_NEW_KEY_HERE"
    }
  3. Backend re-encrypts and stores new key
  4. Test integration to ensure new key works
  5. Revoke old key in external service
  6. Audit log records the rotation event

Zero-Downtime Rotation

For critical integrations, use a dual-key approach:

  1. Create second integration with new API key
  2. Update application to try both keys (fallback logic)
  3. Monitor to ensure new key works
  4. Delete old integration after confirmation
  5. Revoke old key in external service

Usage in External Integrations

Retrieving API Keys

To use an API key for making external API calls:

go
// Get integration by organization and service
integration, err := db.GetOrganizationIntegration(orgID, "service_name")
if err != nil {
    return err
}

// Decrypt the API key
apiKey, err := crypto.DecryptAPIKey(orgID, integration.APIKeyEncrypted)
if err != nil {
    return err
}

// Use the API key in HTTP requests
client := externalservice.NewClient(apiKey)
response, err := client.DoSomething()

Caching Decrypted Keys

DO NOT cache decrypted API keys in memory or on disk. Always decrypt on-demand:

  • Security: Limits exposure if memory is dumped or process crashes
  • Rotation: Ensures new keys are used immediately
  • Compliance: Reduces plaintext key lifetime

If performance is a concern, cache only for the duration of a single request context.

Rate Limiting

The API endpoint for retrieving decrypted keys is rate limited:

  • GET /v1/organizations/{id}/api-keys
    • Max 10 requests per minute per organization
    • Max 100 requests per hour per organization

This prevents:

  • Brute force attacks
  • Excessive decryption load
  • Accidental loops in application code

Security Best Practices

Access Control

  1. Admin-only access to API keys endpoint
  2. RLS policies enforce organization boundaries
  3. Audit logging for all API key operations
  4. Rate limiting prevents abuse

Encryption Key Management

  1. Store encryption key in secret manager (AWS Secrets Manager, Vault)
  2. Rotate encryption key annually (requires re-encrypting all keys)
  3. Use different keys per environment (dev, staging, production)
  4. Never log encryption keys or decrypted API keys

API Key Hygiene

  1. Use service-specific keys when possible (not master keys)
  2. Rotate keys every 90 days or per compliance requirements
  3. Revoke keys immediately when admin access is removed
  4. Monitor API key usage in external services for anomalies

Audit Logging

All API key operations are logged to audit_log:

ActionDescriptionLogged Fields
integration.createdNew API key addedorganization_id, service, admin_user_id
integration.updatedAPI key rotatedorganization_id, service, admin_user_id
integration.deletedAPI key removedorganization_id, service, admin_user_id
integration.viewedAPI key decrypted via APIorganization_id, service, admin_user_id, ip_address

Example audit log entry:

json
{
  "id": 12345,
  "organization_id": 1,
  "user_id": 42,
  "action": "integration.viewed",
  "entity_type": "organization_integration",
  "entity_id": 5,
  "changes": {
    "service": "service_name",
    "title": "Service Production"
  },
  "ip_address": "192.168.1.100",
  "user_agent": "Mozilla/5.0...",
  "request_path": "GET /v1/organizations/1/api-keys",
  "created_at": "2025-01-15T14:22:00Z"
}

Infrastructure Encryption

In addition to application-level encryption, API keys benefit from:

  1. Encryption at rest - AWS RDS encrypts all database storage
  2. Encryption in transit - TLS 1.2+ for all database connections
  3. Encrypted backups - RDS snapshots are encrypted
  4. Encrypted replication - Replica databases use encrypted connections

This provides defense in depth: even if one layer fails, keys remain protected.

Compliance

HIPAA

API keys for healthcare integrations must:

  • Be encrypted at rest (✓ AES-256-GCM)
  • Be encrypted in transit (✓ TLS)
  • Have access logging (✓ audit_log)
  • Support key rotation (✓)
  • Have admin-only access (✓ RLS policies)

GDPR

API keys are not considered personal data, but access logs are:

  • Audit logs containing user_id are subject to GDPR
  • Export user data includes their API key access history
  • Anonymize user_id when user requests deletion

Troubleshooting

Decryption Fails

Symptoms: Error decrypting API key, invalid auth tag

Possible Causes:

  1. Encryption key changed (environment variable updated)
  2. Data corrupted in database
  3. Associated data mismatch (organization_id changed)

Resolution:

  1. Check encryption key environment variable
  2. Re-encrypt API key using correct key
  3. Verify organization_id hasn't changed

Integration Not Working

Symptoms: External API calls fail with authentication errors

Possible Causes:

  1. API key expired in external service
  2. API key revoked in external service
  3. Wrong service selected in integration

Resolution:

  1. Verify API key in external service dashboard
  2. Rotate API key to new valid key
  3. Check service enum matches external service

Rate Limit Errors

Symptoms: 429 rate_limited when accessing /v1/organizations/{id}/api-keys

Possible Causes:

  1. Application polling API keys too frequently
  2. Admin repeatedly accessing keys in UI
  3. Bug causing infinite loop

Resolution:

  1. Cache keys in request context (not globally)
  2. Reduce polling frequency
  3. Check application logs for loops

Migration Guide

Migrating from Plaintext Keys

If you're migrating from plaintext API key storage:

  1. Add encryption key to environment variables
  2. Create migration script:
    go
    func MigrateAPIKeys() error {
        var integrations []OrganizationIntegration
        db.Find(&integrations)
    
        for _, integration := range integrations {
            // Read plaintext key from old column
            plaintext := integration.APIKeyPlaintext
    
            // Encrypt using new encryption function
            encrypted, err := crypto.EncryptAPIKey(
                integration.OrganizationID,
                plaintext,
            )
            if err != nil {
                return err
            }
    
            // Save encrypted key, clear plaintext
            integration.APIKeyEncrypted = encrypted
            integration.APIKeyPlaintext = ""
            db.Save(&integration)
        }
    
        return nil
    }
  3. Run migration in production
  4. Verify all keys still work
  5. Drop old column from database

Re-encrypting with New Key

If you need to rotate the encryption key itself:

  1. Generate new 256-bit encryption key
  2. Store in environment as API_KEY_ENCRYPTION_KEY_NEW
  3. Create re-encryption script:
    go
    func ReencryptAPIKeys() error {
        oldKey := os.Getenv("API_KEY_ENCRYPTION_KEY")
        newKey := os.Getenv("API_KEY_ENCRYPTION_KEY_NEW")
    
        var integrations []OrganizationIntegration
        db.Find(&integrations)
    
        for _, integration := range integrations {
            // Decrypt with old key
            plaintext, err := crypto.DecryptAPIKeyWithKey(
                oldKey,
                integration.OrganizationID,
                integration.APIKeyEncrypted,
            )
            if err != nil {
                return err
            }
    
            // Re-encrypt with new key
            encrypted, err := crypto.EncryptAPIKeyWithKey(
                newKey,
                integration.OrganizationID,
                plaintext,
            )
            if err != nil {
                return err
            }
    
            // Save
            integration.APIKeyEncrypted = encrypted
            db.Save(&integration)
        }
    
        return nil
    }
  4. Run re-encryption during maintenance window
  5. Replace old key with new key in environment
  6. Verify all integrations still work
  7. Remove API_KEY_ENCRYPTION_KEY_NEW from environment