Back to Blog
Technical10 min

API Keys and Tokens: Developer's Guide to Credential Security

Essential security practices for managing API keys, access tokens, and developer credentials.


title: "API Keys and Tokens: Developer's Guide to Credential Security" description: "Essential security practices for managing API keys, access tokens, and developer credentials." date: "2025-12-26" author: "Security Team" category: "Technical" readTime: "10 min" keywords: ["API key security", "token management", "developer credentials", "secrets management"]

Introduction

API keys and tokens are the passwords of the developer world. A leaked API key can result in service abuse, data breaches, and massive bills. This guide provides comprehensive security practices for developers managing credentials in code, CI/CD pipelines, and production environments.

Types of Developer Credentials

API Keys

What they are: Long-lived credentials for service authentication

Common services:

  • Cloud providers (AWS, Azure, GCP)
  • Payment processors (Stripe, PayPal)
  • Communication (Twilio, SendGrid)
  • Analytics (Google Analytics, Mixpanel)
  • Maps (Google Maps, Mapbox)

Security level: High - treat as passwords

Access Tokens

OAuth tokens: Short-lived, scoped access JWT tokens: Encoded claims with expiration Personal access tokens: GitHub, GitLab, Bitbucket

Characteristics:

  • Time-limited
  • Scope-restricted
  • Revocable
  • Renewable

Service Account Credentials

What they are: Machine-to-machine authentication

Examples:

  • GCP service account keys
  • AWS IAM credentials
  • Azure service principals
  • Database connection strings

Risk level: Critical - full service access

SSH Keys

Purpose: Server access, Git operations

Types:

  • RSA (legacy, 2048+ bits)
  • Ed25519 (modern, recommended)
  • ECDSA (alternative)

Protection: Passphrase-protected, limited access

Database Credentials

Connection strings: Username, password, host, port

Risk: Direct data access

Protection: Environment variables, secrets management

Common Security Mistakes

❌ Hardcoding Credentials

The mistake:

// NEVER DO THIS
const apiKey = "sk_live_51H8xYz2eZvKYlo2C...";
const dbPassword = "MyPassword123!";

Why dangerous:

  • Committed to version control
  • Visible in code reviews
  • Exposed in public repos
  • Difficult to rotate
  • Audit trail in Git history

Impact:

  • GitHub scans for secrets (alerts sent)
  • Bots scrape public repos
  • Instant exploitation
  • Costly service abuse

❌ Committing .env Files

The mistake:

# .env file committed to Git
DATABASE_URL=postgresql://user:pass@localhost/db
STRIPE_SECRET_KEY=sk_live_...
AWS_ACCESS_KEY_ID=AKIA...

Why dangerous:

  • Permanent Git history
  • Shared with all developers
  • Exposed if repo public
  • Difficult to remove

Prevention:

# .gitignore
.env
.env.local
.env.*.local
secrets.json
credentials.json

❌ Logging Credentials

The mistake:

console.log(`Connecting with key: ${apiKey}`);
logger.info(`Database password: ${dbPass}`);

Why dangerous:

  • Logs often unencrypted
  • Stored long-term
  • Accessible to many people
  • Sent to log aggregation services

Solution:

console.log(`Connecting with key: ${apiKey.substring(0, 8)}...`);
logger.info('Database connection established');

❌ Overly Permissive Keys

The mistake:

  • Admin access for read-only needs
  • Wildcard permissions
  • No expiration
  • Shared across environments

Impact:

  • Excessive blast radius
  • Difficult to audit
  • Compliance violations
  • Unnecessary risk

❌ No Key Rotation

The mistake:

  • Same keys for years
  • Never expire
  • No rotation policy
  • Forgotten keys active

Risk:

  • Increased exposure time
  • Compromised keys undetected
  • Compliance failures
  • Difficult incident response

Secure Credential Management

Environment Variables

Best practice:

// Load from environment
const apiKey = process.env.STRIPE_SECRET_KEY;
const dbUrl = process.env.DATABASE_URL;

// Validate presence
if (!apiKey) {
  throw new Error('STRIPE_SECRET_KEY not set');
}

Setup:

# .env (not committed)
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://...

# Load in application
require('dotenv').config();

Advantages:

  • Not in code
  • Environment-specific
  • Easy to rotate
  • Standard practice

Secrets Management Systems

Enterprise solutions:

  • HashiCorp Vault: Industry standard
  • AWS Secrets Manager: AWS integration
  • Azure Key Vault: Azure integration
  • GCP Secret Manager: GCP integration

Features:

  • Encryption at rest
  • Access control
  • Audit logging
  • Automatic rotation
  • Version history

Example (AWS Secrets Manager):

const AWS = require('aws-sdk');
const client = new AWS.SecretsManager();

async function getSecret(secretName) {
  const data = await client.getSecretValue({
    SecretId: secretName
  }).promise();
  
  return JSON.parse(data.SecretString);
}

const dbCreds = await getSecret('prod/database');

Configuration Management

Tools:

  • dotenv: Development
  • docker-compose: Container secrets
  • Kubernetes secrets: K8s deployments
  • AWS Parameter Store: AWS infrastructure

Best practices:

  • Separate per environment
  • Encrypted storage
  • Access controls
  • Audit trails

Password Managers for Developers

Team password managers:

  • 1Password (developer-friendly)
  • Bitwarden (open-source)
  • LastPass (team features)

Use for:

  • Shared credentials
  • Emergency access
  • Team onboarding
  • Secure notes

CLI integration:

# 1Password CLI
op run -- npm start

# Bitwarden CLI
bw get password "API Key" | pbcopy

Learn more: Password Manager Security

Development Workflow

Local Development

Setup:

# .env.local (gitignored)
DATABASE_URL=postgresql://localhost/dev_db
STRIPE_SECRET_KEY=sk_test_...
DEBUG=true

Best practices:

  • Use test/sandbox keys
  • Separate from production
  • Document required variables
  • Provide .env.example

Example .env.example:

# Copy to .env.local and fill in values
DATABASE_URL=
STRIPE_SECRET_KEY=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

CI/CD Pipelines

GitHub Actions:

name: Deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: npm run deploy

Best practices:

  • Use platform secrets (GitHub Secrets, GitLab CI/CD variables)
  • Mask sensitive output
  • Limit secret access
  • Rotate regularly
  • Audit usage

Other platforms:

  • GitLab CI: CI/CD variables (protected, masked)
  • CircleCI: Context secrets
  • Jenkins: Credentials plugin
  • Travis CI: Encrypted variables

Production Deployment

Container secrets:

# docker-compose.yml
services:
  app:
    image: myapp
    environment:
      - DATABASE_URL=${DATABASE_URL}
    secrets:
      - db_password

secrets:
  db_password:
    external: true

Kubernetes secrets:

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  api-key: <base64-encoded>
  db-password: <base64-encoded>

Best practices:

  • Use secrets management
  • Encrypt at rest
  • Limit pod access
  • Rotate regularly
  • Monitor access

API Key Security

Key Generation

Requirements:

  • Cryptographically random
  • Sufficient length (32+ characters)
  • Unpredictable
  • Unique per user/service

Example generation:

const crypto = require('crypto');

function generateApiKey() {
  return crypto.randomBytes(32).toString('hex');
}

// Result: 64-character hex string

Key Storage

Hash keys before storage:

const crypto = require('crypto');

function hashApiKey(key) {
  return crypto
    .createHash('sha256')
    .update(key)
    .digest('hex');
}

// Store hash, not plain key
const keyHash = hashApiKey(apiKey);
await db.apiKeys.create({ hash: keyHash });

Why hash:

  • Database breach doesn't expose keys
  • Similar to password hashing
  • One-way verification
  • Industry best practice

Key Rotation

Rotation policy:

  • Regular schedule (90 days)
  • After employee departure
  • Suspected compromise
  • Compliance requirements

Implementation:

// Support multiple active keys during rotation
const activeKeys = [
  process.env.API_KEY_CURRENT,
  process.env.API_KEY_PREVIOUS
];

function validateKey(providedKey) {
  return activeKeys.some(key => 
    crypto.timingSafeEqual(
      Buffer.from(hashKey(providedKey)),
      Buffer.from(hashKey(key))
    )
  );
}

Process:

  1. Generate new key
  2. Add to environment
  3. Update clients gradually
  4. Remove old key after transition
  5. Document rotation

Key Scoping

Principle of least privilege:

// Good: Scoped permissions
{
  "key": "sk_live_...",
  "permissions": ["read:users", "write:orders"],
  "rateLimit": 1000,
  "expiresAt": "2025-12-31"
}

// Bad: Admin access
{
  "key": "sk_live_...",
  "permissions": ["*"],
  "rateLimit": null,
  "expiresAt": null
}

Best practices:

  • Minimum required permissions
  • Time-limited when possible
  • Rate limiting
  • IP restrictions
  • Usage monitoring

Token Management

JWT Best Practices

Secure generation:

const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { 
    expiresIn: '15m',
    issuer: 'myapp.com',
    audience: 'myapp.com'
  }
);

Security considerations:

  • Strong secret (32+ characters)
  • Short expiration (15-60 minutes)
  • Refresh token pattern
  • Signature verification
  • Claims validation

Refresh tokens:

// Short-lived access token
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });

// Long-lived refresh token (stored securely)
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });

// Refresh endpoint
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  const decoded = jwt.verify(refreshToken, refreshSecret);
  const newAccessToken = jwt.sign({ userId: decoded.userId }, secret, { expiresIn: '15m' });
  res.json({ accessToken: newAccessToken });
});

OAuth Token Security

Storage:

  • Never in localStorage (XSS vulnerable)
  • HttpOnly cookies (preferred)
  • Secure, SameSite flags
  • Short expiration

Example:

res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000 // 15 minutes
});

Monitoring and Detection

Audit Logging

Log all credential usage:

logger.info('API key used', {
  keyId: key.id,
  userId: key.userId,
  endpoint: req.path,
  ip: req.ip,
  timestamp: new Date()
});

Monitor for:

  • Unusual usage patterns
  • Failed authentication attempts
  • Geographic anomalies
  • Rate limit violations
  • Permission escalation attempts

Alerting

Set up alerts for:

  • Multiple failed attempts
  • Access from new locations
  • High-value operations
  • Permission changes
  • Key rotation events

Tools:

  • Datadog
  • New Relic
  • Sentry
  • CloudWatch
  • Custom webhooks

Secret Scanning

Tools:

  • GitHub secret scanning: Automatic
  • GitGuardian: Comprehensive
  • TruffleHog: Open-source
  • git-secrets: Pre-commit hooks

Implementation:

# Install git-secrets
brew install git-secrets

# Initialize in repo
git secrets --install
git secrets --register-aws

# Scan history
git secrets --scan-history

Incident Response

If Credentials Are Leaked

Immediate actions (within 1 hour):

  1. Revoke compromised credentials
  2. Generate new credentials
  3. Update all services
  4. Review access logs
  5. Assess damage

Within 24 hours: 6. Identify leak source 7. Fix vulnerability 8. Rotate related credentials 9. Notify affected parties 10. Document incident

Prevention:

  • Implement secret scanning
  • Pre-commit hooks
  • Code review process
  • Security training
  • Regular audits

Removing from Git History

Tools:

# BFG Repo-Cleaner (recommended)
bfg --replace-text passwords.txt repo.git

# git-filter-branch (alternative)
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

Important:

  • Credentials still compromised
  • Must rotate immediately
  • Force push required
  • Notify all contributors

Best Practices Summary

✅ Do This

  • Use environment variables
  • Implement secrets management
  • Rotate credentials regularly
  • Scope permissions minimally
  • Enable audit logging
  • Use secret scanning
  • Hash API keys before storage
  • Implement rate limiting
  • Monitor for anomalies
  • Document all credentials

❌ Never Do This

  • Hardcode credentials
  • Commit secrets to Git
  • Log sensitive data
  • Share credentials
  • Use admin keys unnecessarily
  • Skip rotation
  • Ignore alerts
  • Store in plain text
  • Use same key everywhere
  • Disable security features

Developer Security Checklist

Before Committing

  • [ ] No hardcoded credentials
  • [ ] .env files gitignored
  • [ ] No sensitive data in logs
  • [ ] Secret scanning passed
  • [ ] Code review completed

Before Deploying

  • [ ] Environment variables set
  • [ ] Secrets management configured
  • [ ] Permissions scoped correctly
  • [ ] Monitoring enabled
  • [ ] Rotation schedule set

Regular Maintenance

  • [ ] Rotate credentials (90 days)
  • [ ] Review access logs
  • [ ] Update permissions
  • [ ] Remove unused keys
  • [ ] Audit secret usage

Conclusion

Developer credential security is critical:

  1. Never hardcode - Use environment variables or secrets management
  2. Rotate regularly - 90-day maximum
  3. Scope minimally - Least privilege principle
  4. Monitor actively - Audit logs and alerts
  5. Scan continuously - Detect leaks early

Your API keys are as valuable as passwords. Treat them with the same security rigor.

Start now: Audit your codebase for hardcoded credentials and implement proper secrets management today.

Learn more:

Ready to Create a Strong Password?

Use our free Strong Password Generator to create secure passwords instantly.