API Manager
API Manager
Section titled “API Manager”API Manager (Fastify 5) handles service-to-service API token generation, encryption, storage, and rotation.
Purpose
Section titled “Purpose”Microservices need secure tokens to authenticate with each other. API Manager provides:
- Centralized token storage — Single source of truth
- Encryption at rest — AES-256-GCM with a per-record random salt
- Auto-rotation — 7-day automatic refresh cycle
- Rate limiting — 100 requests/15 min per IP
- Admin endpoints — Status, manual rotate/revoke, force rotation
Port & Access
Section titled “Port & Access”- Port: 3200
- Health:
GET /health - Internal URL:
http://api-manager:3200 - Framework: Fastify
^5.9.0(migrated from Fastify 4 for CVE remediation — see CHANGELOG) - Runtime: Node.js
>=20.0.0
Token Lifecycle
Section titled “Token Lifecycle”Fetching a Token
Section titled “Fetching a Token”GET /v1/token/:serviceX-Service-Secret: <SERVICE_SECRET>API Manager:
- Looks up
:servicein encrypted/datavolume - If token exists and < 7 days old → return it (cached)
- If missing or expired → call Strapi admin API to generate new token
- Encrypt token with AES-256-GCM
- Store in
/data/tokens/:service.enc - Return plaintext token to requestor
Token Storage
Section titled “Token Storage”/data/├── tokens/│ ├── git-connector.enc│ ├── cloud-connector.enc│ └── webhook-publisher.encEach encrypted token file is a single string with 4 colon-separated hex segments — see Encryption Details below.
Auto-Rotation
Section titled “Auto-Rotation”Every 7 days (configurable):
Cron job (every 6 hours, checks expiry) → For each token: if age > 7 days → Fetch new token from Strapi → Encrypt + store → Continue serving new tokenManual Rotation
Section titled “Manual Rotation”curl -X POST http://api-manager:3200/v1/admin/rotate/git-connector \ -H "X-Admin-Key: <ADMIN_KEY>"
# Response: { "success": true, "message": "Token rotated for git-connector (local)", "data": { ... } }Configuration
Section titled “Configuration”.env variables — two layers:
API Manager itself only ever reads ENCRYPTION_KEY (see api-manager/.env.example) — its code has no knowledge of any API_MANAGER_-prefixed name. The orchestration layer defines the same secret as API_MANAGER_ENCRYPTION_KEY in orchestration/.env.example, and docker-compose.core.yml maps it into the container as plain ENCRYPTION_KEY:
environment: - ENCRYPTION_KEY=${API_MANAGER_ENCRYPTION_KEY}So when setting the key, edit API_MANAGER_ENCRYPTION_KEY in orchestration/.env (the orchestration-level name); if running api-manager standalone (outside orchestration, via its own .env), set ENCRYPTION_KEY directly.
# Orchestration-level name (orchestration/.env)API_MANAGER_ENCRYPTION_KEY=<base64-32-bytes>
# In-process name api-manager's own code reads (api-manager/.env, standalone dev)ENCRYPTION_KEY=<base64-32-bytes>
# Admin API key for admin/* endpointsADMIN_API_KEY=<random-secret>
# Service secrets (one per consumer)SERVICE_SECRET_GIT_CONNECTOR=<secret>SERVICE_SECRET_CLOUD_CONNECTOR=<secret>SERVICE_SECRET_WEBHOOK_PUBLISHER=<secret>
# Strapi backend (per configured region, e.g. "local")STRAPI_BACKEND_local_URL=<strapi-url>STRAPI_BACKEND_local_ADMIN_EMAIL=admin@sencai.spaceSTRAPI_BACKEND_local_ADMIN_PASSWORD=<password>Rotation interval and other per-service settings (e.g. rotationDays) are configured per service in src/config/services/*.json, not as a single global .env value.
Service-to-Service Auth
Section titled “Service-to-Service Auth”All internal requests use X-Service-Secret header:
curl -H "X-Service-Secret: SERVICE_SECRET_GIT_CONNECTOR" \ http://api-manager:3200/v1/token/git-connectorEndpoints
Section titled “Endpoints”Routes are versioned under /v1/ (F3.API.01). Legacy unprefixed /api/ paths still work as deprecated aliases — every response on those paths carries a Sunset header (RFC 8594) and a Link: <.../v1/...>; rel="successor-version" header pointing at the /v1 successor. Every response under both /v1/* and /api/* also carries X-API-Version: 1.
Fetch Token
Section titled “Fetch Token”GET /v1/token/:serviceHeaders: X-Service-Secret: <service-secret>
Response:{ "success": true, "data": { "token": "eyJ...", "expiresAt": "2026-04-26T12:00:00Z", "region": "local", "strapiBackendUrl": "http://backend:1337" }}Health Check
Section titled “Health Check”GET /health
Response:{ "status": "up", "timestamp": "2024-01-01T12:00:00Z"}Rotate Token (Admin)
Section titled “Rotate Token (Admin)”POST /v1/admin/rotate/:serviceHeaders: X-Admin-Key: <admin-key>
Response:{ "success": true, "message": "Token rotated for git-connector (local)", "data": { "serviceName": "git-connector", "region": "local", "rotatedAt": "2026-04-26T12:00:00Z", "expiresAt": "2026-05-03T12:00:00Z" }}Revoke Token (Admin)
Section titled “Revoke Token (Admin)”POST /v1/admin/revoke/:serviceHeaders: X-Admin-Key: <admin-key>
Response:{ "success": true, "message": "Token revoked for git-connector (local)"}Status (Admin)
Section titled “Status (Admin)”GET /v1/admin/statusHeaders: X-Admin-Key: <admin-key>
Response:{ "success": true, "data": { "tokens": [ { "serviceName": "git-connector", "region": "local", "createdAt": "...", "expiresAt": "...", "rotatedAt": "..." } ], "stats": { ... }, "rotator": { ... } }}Force Rotation Check (Admin)
Section titled “Force Rotation Check (Admin)”POST /v1/admin/force-rotationHeaders: X-Admin-Key: <admin-key>
Response:{ "success": true, "message": "Rotation check completed"}curl example:
curl -X POST http://api-manager:3200/v1/admin/force-rotation \ -H "X-Admin-Key: <ADMIN_KEY>"Encryption Details
Section titled “Encryption Details”Algorithm: AES-256-GCM, key derived via crypto.scryptSync(masterKey, salt, 32)
Key Size: 256 bits (32 bytes)
IV Size: 128 bits (16 bytes)
Auth Tag Size: 128 bits (16 bytes)
Current format (v2) — per-record random salt:
iv(hex):authTag(hex):ciphertext(hex):salt(hex)Each encryption call generates a fresh random 16-byte salt, which is stored alongside the ciphertext (4 colon-separated hex segments) and used to derive the per-record key via scrypt. This replaced an earlier fixed-format encoding that derived the key from a single static salt shared by every token — a security fix documented in the service CHANGELOG.md.
Legacy format (v1) — decrypt-only backward compatibility:
iv(hex):authTag(hex):ciphertext(hex)Older tokens with only 3 segments (no stored salt) are still decrypted correctly by falling back to the legacy static salt. New encryptions always write the 4-segment format; the 3-segment format is never written going forward.
Rate Limiting
Section titled “Rate Limiting”API Manager enforces rate limits:
- Limit: 100 requests
- Window: 15 minutes
- Per: IP address
Response when limit exceeded:
{ "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded. Try again in 15 minutes."}Troubleshooting
Section titled “Troubleshooting”Q: “401 Unauthorized” on token fetch
A: X-Service-Secret header missing or doesn’t match SERVICE_SECRET_* in .env.
Q: “Token generation failed”
A: Strapi admin API might be unreachable. Check:
- Strapi is running (
docker logs backend) STRAPI_BACKEND_local_ADMIN_EMAILandSTRAPI_BACKEND_local_ADMIN_PASSWORDare correct- Network connectivity to Strapi
Q: Can’t decrypt tokens
A: ENCRYPTION_KEY (in-process) / API_MANAGER_ENCRYPTION_KEY (orchestration) mismatch. Ensure the key is consistent across restarts and across all layers.
Q: Rotation never happens
A: Check logs (docker logs api-manager). Ensure the rotation cron is running and Strapi is accessible.