# API Key Management Guide This guide explains how to generate, manage, and use API keys with the Notifier service. The system uses a hybrid approach combining an in-memory cache for performance with PostgreSQL for persistence. ## Overview The API key management system provides: - **Secure Key Generation**: Cryptographically random 32-byte keys with `nk_` prefix - **Persistent Storage**: PostgreSQL backend with full audit trail - **Fast Lookups**: In-memory cache with write-through consistency - **Rate Limiting**: Per-key request rate limits (configurable per minute) - **Expiration**: Optional key expiration dates - **Key Rotation**: Ability to revoke and create new keys - **Audit Logging**: Track who created/revoked keys and when - **Role-Based Access**: Control which notifiers each key can access ## Architecture ### Hybrid Cache Strategy The system uses a write-through hybrid approach: ``` Request Flow: 1. Check in-memory cache (O(1) lookup) ← Fast path 2. If hit and valid, use immediately 3. If miss, create from database (fallback) 4. Update cache and return Write Flow: 1. Write to PostgreSQL database first 2. If successful, update in-memory cache 3. If DB fails, cache is not updated (consistency) ``` **Benefits**: - Fast authentication checks (cache lookup in microseconds) - Persistent storage for durability - Consistent state across restarts - Multi-instance support (all instances read from same DB) ## Setup ### Prerequisites - PostgreSQL 12+ database - Network access from notifier to PostgreSQL ### Configuration Add database configuration to `config.yaml`: ```yaml auth: enabled: true default_rate_limit: 100 # requests per minute database: url: "postgresql://user:password@localhost:5432/notifier" # Or use environment variable: NOTIFIER_AUTH_DATABASE_URL ``` **Environment Variable**: ```bash export NOTIFIER_AUTH_DATABASE_URL="postgresql://user:password@localhost:5432/notifier" export NOTIFIER_AUTH_ENABLED=true export NOTIFIER_AUTH_DEFAULT_RATE_LIMIT=100 ``` ### Database Setup The schema is automatically created on first startup: ```sql -- Tables created automatically: -- api_keys: Stores API key metadata -- api_key_audit_log: Tracks all key operations ``` To manually initialize the database: ```bash psql postgresql://user:password@localhost:5432/notifier < schema.sql ``` ## Bootstrap: Creating Initial Admin Key On first deployment, you need to create an initial admin key to bootstrap the system. ### Option 1: Environment Variable (Recommended for CI/CD) ```bash export NOTIFIER_BOOTSTRAP_ADMIN_KEY=true ./notifier serve ``` The service will: 1. Check if bootstrap has already been done 2. Create a random admin key with all permissions 3. Save it to `./notifier-admin-key.txt` 4. Print to stdout (make sure to capture and secure this!) **Output**: ``` ============================================================ NOTIFIER BOOTSTRAP: ADMIN KEY CREATED ============================================================ Key: nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ... ============================================================ ``` ### Option 2: Programmatic (Docker/Kubernetes) In your startup script: ```go import "github.com/igodwin/notifier/internal/auth" bootstrapCfg := &auth.BootstrapConfig{ Enabled: true, AdminKeyFileName: "/var/run/notifier/admin-key.txt", PrintToStdout: false, } adminKey, err := auth.BootstrapAdminKey(ctx, keyStore, bootstrapCfg, logger) if err != nil && err.Error() != "bootstrap already completed" { logger.Fatalf("Bootstrap failed: %v", err) } ``` ### Option 3: Docker Environment ```dockerfile FROM notifier:latest ENV NOTIFIER_AUTH_ENABLED=true ENV NOTIFIER_BOOTSTRAP_ADMIN_KEY=true ENV NOTIFIER_AUTH_DATABASE_URL=postgresql://user:pass@db:5432/notifier ENTRYPOINT ["/app/notifier", "serve"] ``` **Capture the key**: ```bash docker logs | grep "Key:" ``` ### Option 4: Kubernetes ```yaml apiVersion: batch/v1 kind: Job metadata: name: notifier-bootstrap spec: template: spec: containers: - name: notifier image: notifier:latest env: - name: NOTIFIER_AUTH_ENABLED value: "true" - name: NOTIFIER_BOOTSTRAP_ADMIN_KEY value: "true" - name: NOTIFIER_AUTH_DATABASE_URL valueFrom: secretKeyRef: name: notifier-db-secret key: url volumeMounts: - name: keys mountPath: /var/run/notifier volumes: - name: keys secret: secretName: notifier-keys restartPolicy: Never ``` After the job completes, extract the key from the secret or logs. ## Managing API Keys ### Creating New Keys Use the admin key to create additional keys via REST API: ```bash # Create a key for sending emails only curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_admin_key_here" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app-email", "roles": ["notify-email"], "rate_limit": 1000, "expires_in": "8760h" }' ``` **Response**: ```json { "key": "nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "name": "my-app-email-1698297600", "client_id": "my-app-email", "roles": ["notify-email"], "created_at": "2024-10-26T12:00:00Z", "expires_at": "2025-10-26T12:00:00Z", "rate_limit": 1000 } ``` ### Listing Keys List all keys for your client: ```bash curl -X GET http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_your_key" ``` List keys for specific client (admin only): ```bash curl -X GET "http://localhost:8080/api/v1/admin/keys?client_id=other-app" \ -H "Authorization: Bearer nk_admin_key" ``` ### Revoking Keys Disable a key (cannot be undone, but you can create a new one): ```bash curl -X DELETE http://localhost:8080/api/v1/admin/keys/nk_key_to_revoke \ -H "Authorization: Bearer nk_admin_key" ``` ### Rotating Keys Create a new key with same permissions, then revoke the old one: ```bash # 1. Create new key with same roles curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_admin_key" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app", "roles": ["notify-email", "notify-slack"], "rate_limit": 1000 }' # 2. Update your application to use the new key # 3. Verify everything works # 4. Revoke the old key curl -X DELETE http://localhost:8080/api/v1/admin/keys/nk_old_key \ -H "Authorization: Bearer nk_admin_key" ``` ### Viewing Audit Log See who created/revoked a key and when: ```bash curl -X GET "http://localhost:8080/api/v1/admin/keys/nk_key/audit?limit=50" \ -H "Authorization: Bearer nk_admin_key" ``` **Response**: ```json { "key_preview": "nk_o5p6", "audit_log": [ { "action": "created", "performed_by": "admin-bootstrap", "performed_at": "2024-10-26T12:00:00Z", "details": { "client_id": "my-app", "roles": ["notify-email"] } }, { "action": "deactivated", "performed_by": "admin-user", "performed_at": "2024-10-26T14:30:00Z", "details": null } ] } ``` ## Using API Keys ### With REST API Include the key in the `Authorization: Bearer` header: ```bash curl -X POST http://localhost:8080/api/v1/notifications \ -H "Authorization: Bearer nk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "type": "email", "subject": "Hello", "body": "World", "recipients": ["user@example.com"] }' ``` Or use the `X-API-Key` header: ```bash curl -X POST http://localhost:8080/api/v1/notifications \ -H "X-API-Key: nk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "type": "email", "subject": "Hello", "body": "World", "recipients": ["user@example.com"] }' ``` ### With gRPC Include the key in gRPC metadata: **Go Client**: ```go import "google.golang.org/grpc/metadata" ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer nk_your_api_key") client := pb.NewNotificationServiceClient(conn) resp, err := client.SendNotification(ctx, &pb.SendNotificationRequest{ // ... }) ``` **Python Client**: ```python import grpc metadata = [('authorization', 'Bearer nk_your_api_key')] channel = grpc.secure_channel('localhost:50051', grpc.ssl_channel_credentials()) client = pb.NotificationServiceStub(channel) response = client.SendNotification(request, metadata=metadata) ``` ## Key Naming and Organization ### Naming Convention Keys are generated with format: `nk_<32-hex-chars>` The system auto-generates a name based on client_id and timestamp: ``` my-app-email-1698297600 my-slack-integration-1698297700 ``` You can customize via the `name` field when creating keys. ### Recommended Organization **By Application**: ``` client_id: api-gateway client_id: worker-service client_id: monitoring-system ``` **By Permission**: ``` notify-email: Email notifications only notify-slack: Slack notifications only notify-all: All notification types admin: Key management + all notifications ``` **By Environment**: ``` my-app-prod-email my-app-staging-email my-app-dev-email ``` ## Security Best Practices ### Do's ✅ **Rotate keys regularly** - Create new keys every 90 days, revoke old ones ✅ **Use unique keys per service** - Don't share keys between different apps ✅ **Limit permissions** - Only grant roles needed (e.g., `notify-email` not `admin`) ✅ **Use reasonable rate limits** - Prevent accidental DoS from misconfiguration ✅ **Store in secure vaults** - Use Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault ✅ **Set expiration dates** - Keys should expire after a period ✅ **Monitor audit logs** - Regularly check who's creating/revoking keys ✅ **Use short-lived keys for CI/CD** - Rotate automatically during deployments ### Don'ts ❌ **Don't commit keys to version control** - Even private repos ❌ **Don't use admin key in production** - Create limited permission keys ❌ **Don't share keys between teams** - Each team/app gets its own ❌ **Don't set unlimited rate limits** - Prevents accidental overload ❌ **Don't use expired keys** - Revoke and create new ones ❌ **Don't store plaintext in logs** - Only last 4 characters should be visible ## Rate Limiting Each API key has a configurable rate limit (requests per minute): ```bash # Create key with 1000 req/min limit curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_admin_key" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app", "roles": ["notify-email"], "rate_limit": 1000 }' ``` **Rate Limit Errors**: - Exceeding limit returns HTTP 429 Too Many Requests - Limit resets every minute - Set `rate_limit: 0` for unlimited (not recommended) ### Recommended Limits - Admin/Testing: 10,000 req/min - Production Email: 1,000-5,000 req/min - Production Slack: 500-1,000 req/min - Production Ntfy: 500-1,000 req/min - Development: 100-500 req/min ## Expiration Dates Keys can optionally expire: ```bash # Create key that expires in 24 hours curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_admin_key" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app", "roles": ["notify-email"], "expires_in": "24h" }' # Expires in 30 days "expires_in": "720h" # Expires in 1 year "expires_in": "8760h" ``` **Duration Format**: Go duration format - `s` - seconds (30s) - `m` - minutes (30m) - `h` - hours (24h) - `d` - days (not supported, use 24h instead) Expired keys are automatically filtered when loading cache at startup. ## Troubleshooting ### Key Creation Fails **Error**: `Failed to create API key` **Causes**: - Database connection issue - PostgreSQL not running - Network connectivity problem **Solution**: ```bash # Test PostgreSQL connection psql postgresql://user:password@localhost:5432/notifier -c "SELECT 1" ``` ### Authentication Fails **Error**: `401 Unauthorized` or `API key not found` **Causes**: - Wrong key format - Key is revoked/expired - Key not in cache (distributed setup issue) **Solutions**: ```bash # Verify key format echo $API_KEY | grep "^nk_" # List your keys curl -X GET http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer $API_KEY" # Check if key is active curl -X GET "http://localhost:8080/api/v1/admin/keys?client_id=your-app" \ -H "Authorization: Bearer $ADMIN_KEY" ``` ### Rate Limit Exceeded **Error**: `429 Too Many Requests` **Causes**: - Key rate limit exceeded in current minute - Misconfiguration sending too many requests **Solutions**: - Wait a minute for limit window to reset - Check your request volume - Increase rate limit for the key: ```bash # Create new key with higher limit curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer nk_admin_key" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app", "roles": ["notify-email"], "rate_limit": 5000 }' ``` ### Bootstrap Key Lost **If you lose the bootstrap admin key**: 1. Create a new database admin user with direct SQL access 2. Insert a new admin key into the database: ```sql INSERT INTO api_keys ( key, name, client_id, roles, is_active, rate_limit, created_by ) VALUES ( 'nk_your_new_key_here', 'recovery-admin', 'admin-recovery', ARRAY['admin', 'notify-email', 'notify-slack', 'notify-ntfy'], true, 0, 'system-recovery' ); ``` ## API Reference ### POST /api/v1/admin/keys Create a new API key. **Headers**: - `Authorization: Bearer ` (required, admin role) - `Content-Type: application/json` **Request Body**: ```json { "client_id": "string", // Required: client identifier "roles": ["string"], // Required: array of role names "rate_limit": 100, // Optional: requests per minute "expires_in": "24h" // Optional: expiration duration } ``` **Response**: 201 Created ```json { "key": "string", "name": "string", "client_id": "string", "roles": ["string"], "created_at": "RFC3339", "expires_at": "RFC3339", "rate_limit": 100 } ``` ### GET /api/v1/admin/keys List API keys. **Headers**: - `Authorization: Bearer ` (required) **Query Parameters**: - `client_id`: Filter by client (requires admin role if different from authenticated client) **Response**: 200 OK ```json { "keys": [ { "key_preview": "nk_xxxx", "name": "string", "client_id": "string", "roles": ["string"], "created_at": "RFC3339", "last_used_at": "RFC3339", "expires_at": "RFC3339", "is_active": true, "rate_limit": 100 } ] } ``` ### DELETE /api/v1/admin/keys/{key} Revoke an API key. **Headers**: - `Authorization: Bearer ` (required, admin role) **Request Body** (optional): ```json { "reason": "Compromised key" } ``` **Response**: 204 No Content ### GET /api/v1/admin/keys/{key}/audit Get audit log for a key. **Headers**: - `Authorization: Bearer ` (required, admin role) **Query Parameters**: - `limit`: Number of log entries to return (default: 100, max: 1000) **Response**: 200 OK ```json { "key_preview": "nk_xxxx", "audit_log": [ { "action": "created|deactivated", "performed_by": "string", "performed_at": "RFC3339", "details": {} } ] } ``` ## Complete Example ### Step 1: Bootstrap ```bash export NOTIFIER_BOOTSTRAP_ADMIN_KEY=true export NOTIFIER_AUTH_ENABLED=true export NOTIFIER_AUTH_DATABASE_URL="postgresql://user:pass@localhost:5432/notifier" ./notifier serve # Output: # NOTIFIER BOOTSTRAP: ADMIN KEY CREATED # Key: nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ``` ### Step 2: Save Admin Key ```bash export ADMIN_KEY="nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" echo $ADMIN_KEY > ~/.notifier-admin-key chmod 600 ~/.notifier-admin-key ``` ### Step 3: Create Service Keys ```bash # Email service key curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app-email", "roles": ["notify-email"], "rate_limit": 1000, "expires_in": "8760h" }' | jq -r '.key' > ~/.notifier-email-key # Slack service key curl -X POST http://localhost:8080/api/v1/admin/keys \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-app-slack", "roles": ["notify-slack"], "rate_limit": 500 }' | jq -r '.key' > ~/.notifier-slack-key ``` ### Step 4: Use Keys in Application ```bash export NOTIFIER_EMAIL_KEY=$(cat ~/.notifier-email-key) export NOTIFIER_SLACK_KEY=$(cat ~/.notifier-slack-key) # Send email curl -X POST http://localhost:8080/api/v1/notifications \ -H "Authorization: Bearer $NOTIFIER_EMAIL_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "email", "subject": "Hello", "body": "World", "recipients": ["user@example.com"] }' # Send Slack curl -X POST http://localhost:8080/api/v1/notifications \ -H "Authorization: Bearer $NOTIFIER_SLACK_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "slack", "subject": "Hello", "body": "World", "recipients": ["#general"] }' ``` ## References - [API Authentication Guide](./AUTH.md) - [REST API Documentation](./REST_API.md) - [PostgreSQL Documentation](https://www.postgresql.org/docs/)