Autional logo
Security 7 minutes #API Key#Key Management#Security Practices

API Key Management Best Practices: From Hardcoding to Secure Rotation

A $2 Million String

In March 2024, the Chief Security Officer of a FinTech company was jolted awake by a phone call at 3 AM.

The incident: a junior developer committed a configuration file for a personal project to GitHub, which happened to contain the company’s production AWS access keys. Within 47 seconds of the commit, an attacker’s automated scanning bot captured the credentials. Over the next 3 hours, the attacker used those keys to:

  • Spin up 128 EC2 instances for cryptocurrency mining
  • Export an S3 bucket containing 470,000 user records
  • Execute a data dump on an RDS database

Total damage: over $2 million in cloud resource costs + incalculable consequences from the data breach.

This is not an isolated incident. In 2024, GitHub detected over 12.7 million hardcoded keys pushed to public repositories. On average, a leaked key remains exposed for over 300 seconds before being removed — an infinite window for automated attackers.

Five Anti-Patterns in API Key Management

Before discussing best practices, let’s look at the most common security traps:

Anti-Pattern 1: Hardcoded in Source Code

# You think it's hidden well, but it lives forever in git history
API_KEY = "sk-7b3f8a2d1e4c5f6g7h8i9j0k1l2m3n4o5p"

Whether you delete this line later or not, once committed, it exists in git history forever. Even if the repository is private, if anyone’s account is compromised, attackers can scan all historical commits.

Anti-Pattern 2: Stored in Configuration Files

# config.yaml
api:
  key: "prod-api-key-2024"

Configuration files are usually deployed alongside code. Anyone with filesystem access to the server (including attackers who gain entry via vulnerabilities) can read them. Config files are also more prone to accidental commits to version control.

Anti-Pattern 3: Accidental Exposure in Logs

// Debug code forgotten in production
logger.Info("calling external API", 
    slog.String("api_key", apiKey),  // Leaked in production
    slog.String("url", url))

API requests may be recorded in log files, monitoring systems, and error tracking platforms. If the logging system lacks adequate access control, keys spread outward through logs.

Anti-Pattern 4: No Rotation

“This key has been in use for two years and never caused a problem.”

No problems don’t mean secure. The key may have been exposed in a data breach without the attacker having used it yet. The longer a key’s “lifetime,” the greater the probability of exposure.

Anti-Pattern 5: Shared Keys, No Scope Restrictions

{
  "api_key": "shared-master-key",
  "permissions": ["*"]  // A master key with full access to all resources
}

A master key with all permissions is shared across multiple services, developers, and environments. A leak from any single user means the entire system is compromised.

From Chaos to Order: API Key Management Maturity Model

Building a secure key management system is not a one-time project but a phased, continuous improvement process:

Stage 1: Eliminate Hardcoding

At the most basic level, separate keys from code:

  • Use environment variables to inject keys (os.Getenv("API_KEY"))
  • Use dedicated key management services (e.g., HashiCorp Vault, AWS Secrets Manager)
  • .gitignore strictly excludes any files containing keys

Autional’s base/config module enforces that all sensitive configuration is injected via environment variables, with compile-time static checks prohibiting hardcoded key patterns.

Stage 2: Store Hashes, Not Plaintext

This is the most overlooked yet critical step.

Never store API Key plaintext in the database. Keys should be stored exactly like passwords — only their hash.

// When generating a key
apiKey := random_util.GenerateHex(32)        // "tk_a1b2c3d4..."
keyHash := sha256.Sum256([]byte(apiKey))      // Store this hash
db.Insert(&APIKey{KeyHash: hex.EncodeToString(keyHash[:])})

// When verifying a key
providedHash := sha256.Sum256([]byte(providedKey))
db.Where("key_hash = ?", hex.EncodeToString(providedHash[:])).Find(&apiKey)

This way, even if the database is breached, attackers cannot recover the original API Key — they only see meaningless hash values.

Autional’s key model follows this principle:

type APIKey struct {
    ID          string    // ULID
    Name        string    // Human-readable name
    Prefix      string    // "tk_" — first 4 visible characters of the full key
    KeyHash     string    // SHA-256(apiKey), the only field stored in DB
    LastUsed    time.Time
    
    // Plaintext key is returned only once at creation
    // Not readable afterwards
}

Key design: The full key is returned once in the API response at creation time. After that, Autional does not store, recover, or display it. The Prefix field lets administrators identify keys (e.g., tk_a1b2***) without being able to reconstruct the full key.

Stage 3: Fine-Grained Permission Control

Not all keys are created equal. Create dedicated keys for each use case with least-privilege permissions:

# Different scenarios, different keys, different permissions
- key: billing-read-key
  scopes: ["billing:read", "invoice:read"]    # Can only read billing data
  resources: ["tenant_123"]                     # Can only access specified tenant
  
- key: user-sync-key
  scopes: ["user:read", "user:create"]         # Can only operate on users
  ip_whitelist: ["10.0.1.0/24"]               # Can only be used from internal network
  
- key: ci-deploy-key
  scopes: ["config:read", "service:restart"]   # CI/CD specific
  ttl: 24h                                     # Auto-expires after 24 hours

Autional supports multi-dimensional permission constraints:

  • Scope restrictions: Defines the specific API scope a key can call
  • Resource restrictions: Limits a key to specific tenants or resources
  • IP whitelist: Keys can only be used from specified IP ranges
  • Time restrictions: Supports automatic key expiration for temporary authorization
  • Rate limiting: Independent QPS limits per key to prevent abuse

Stage 4: Automated Rotation

Key rotation should not be a “once a year” operation — it should be an automated process:

┌──────────────────────────────────────────────────┐
│              Key Rotation Automation Flow         │
├──────────────────────────────────────────────────┤
│  1. Generate new key, store hash                  │
│  2. Old and new keys active in parallel (15 min)  │
│  3. Monitor old key usage — confirm client migration│
│  4. Old key usage drops to 0 → revoke old key      │
│  5. If usage hasn't dropped → alert, manual review  │
└──────────────────────────────────────────────────┘

Autional provides a complete rotation lifecycle:

  • Grace period: Old and new keys active simultaneously for seamless client migration
  • Usage monitoring: Real-time tracking of call frequency and last-used time per key
  • Auto-expiration: Keys auto-revoke after expiry without manual intervention
  • Rotation notifications: Webhook or email alerts before key expiration
  • Audit trail: Every key creation, usage, rotation, and revocation is fully recorded

Stage 5: Usage Monitoring and Anomaly Detection

Keys shouldn’t just “sit there.” Continuous monitoring of key usage patterns can detect anomalies:

  • Volume anomaly: A key used 100 times daily suddenly spikes to 1000 times per minute
  • Access pattern anomaly: A key that only calls user query APIs suddenly tries billing endpoints
  • Geographic anomaly: A key with an IP whitelist configured receives requests from unknown IPs
  • Failure rate anomaly: A sudden spike in verification failures — possibly a brute-force attempt

Autional’s anomaly detection engine continuously monitors these metrics. When anomalies are detected:

  • Automatically notify the key owner
  • Impose temporary rate limits on suspicious keys
  • Auto-revoke keys in extreme cases

Complete Key Lifecycle

In Autional, an API Key goes through a complete lifecycle from birth to death:

Create → Activate → Monitor → Expiry Warning → Rotate → Revoke → Archive
  │              │         │          │          │        │        │
  └─ Show once   └─ Dashboard └─ Webhook └─ Grace   └─ Audit └─ Compliance
     Full key      Live        Notify     Period     Log      Retention

Every state transition generates an audit log — yes, the kind protected by a hash chain.

Best Practices Checklist

For implementing API Key security management, here is an actionable checklist:

  • All keys generated via crypto/rand with at least 256-bit entropy
  • Database stores only key hashes (SHA-256), never plaintext
  • Key prefix (first 4 visible characters) used for identification but insufficient for reconstruction
  • Full key returned only once via API response at creation
  • Each key bound to least-privilege permissions (Scope + Resource + IP whitelist)
  • Keys have expiration dates with support for automatic rotation
  • Rotation uses a parallel grace period ensuring zero downtime
  • All key creation, usage, modification, and revocation events are fully audit-logged
  • Real-time monitoring of key usage volume and anomaly patterns
  • Keys are automatically redacted in logs, replaced with tk_a1b2*** in slog output
  • Production keys are fully isolated from development/test environment keys
  • Regular (at least quarterly) audit of all active keys and their permission scopes

Conclusion

An API Key is the bridge connecting your system to the outside world. When that bridge is breached, attackers can walk right into your system — no password cracking, no privilege escalation needed. They have the key.

From hardcoding to automated rotation, the evolution of API Key management is not just a technical upgrade — it’s a fundamental shift in security mindset: from “trusting keys won’t be leaked” to “assuming keys will always be leaked, so minimize the blast radius of every leak.”

Autional bakes this complete key lifecycle management directly into the platform. You don’t need to integrate Vault separately, write your own rotation scripts, or worry about developers leaving TODO: remove this key in the codebase.

Security shouldn’t be an extra burden. It should be the default configuration that’s on from the start.