# Security Security configuration for AgentIdP — JWT key management, CORS, and secret storage. --- ## JWT Key Management AgentIdP uses RS256 (RSA + SHA-256) to sign and verify JWT access tokens. This asymmetric scheme means: - The **private key** signs tokens — must be kept secret, known only to the server - The **public key** verifies tokens — can be shared with any system that needs to validate tokens ### Generate a keypair Generate a 2048-bit RSA keypair: ```bash # Generate private key openssl genrsa -out private.pem 2048 # Extract public key openssl rsa -in private.pem -pubout -out public.pem ``` Verify the files: ```bash # Confirm private key is valid RSA openssl rsa -in private.pem -check -noout # Expected: RSA key ok # Confirm public key is readable openssl rsa -in public.pem -pubin -noout -text | head -5 ``` ### Load keys into environment **Option 1 — Inline in `.env` (development only)** Encode newlines as `\n` and wrap in double quotes: ```bash echo "JWT_PRIVATE_KEY=\"$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' private.pem)\"" >> .env echo "JWT_PUBLIC_KEY=\"$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' public.pem)\"" >> .env ``` **Option 2 — Load from file at runtime (recommended for production)** In the startup script, read the key files and export as environment variables before running the server: ```bash export JWT_PRIVATE_KEY="$(cat /run/secrets/jwt-private.pem)" export JWT_PUBLIC_KEY="$(cat /run/secrets/jwt-public.pem)" npm start ``` With Docker secrets or a secrets manager (Vault, AWS Secrets Manager), mount the key as a file and read it this way. ### Key rotation Rotating the JWT keys invalidates all currently active tokens — every authenticated request will fail until clients re-authenticate. Plan rotation for low-traffic windows. **Rotation procedure:** 1. Generate a new RSA keypair: ```bash openssl genrsa -out private-new.pem 2048 openssl rsa -in private-new.pem -pubout -out public-new.pem ``` 2. Update `JWT_PRIVATE_KEY` and `JWT_PUBLIC_KEY` in your environment or secrets store. 3. Restart the application: ```bash # Graceful restart — send SIGTERM, let in-flight requests complete, then start with new keys kill -SIGTERM npm start # or docker restart ``` 4. All previously issued tokens are now invalid (wrong signature). Clients will receive `401 UNAUTHORIZED` and must call `POST /token` again with their `client_id` and `client_secret` to get a new token. 5. Remove the old key files: ```bash rm private-old.pem public-old.pem ``` **Important:** There is no grace period or dual-key support in Phase 1. All tokens issued with the old private key are immediately rejected after rotation. If zero-downtime key rotation is required, it is a Phase 2 feature. --- ## CORS Configuration Cross-Origin Resource Sharing is configured via the `CORS_ORIGIN` environment variable. | Value | Behaviour | |-------|-----------| | `*` (default) | All origins permitted — appropriate for a public API | | `https://app.example.ai` | Only the specified origin permitted | Set in `.env`: ``` CORS_ORIGIN=https://app.example.ai ``` The CORS header is set by the `cors` middleware applied globally in `src/app.ts`. Credentials (cookies) are not used — all auth is Bearer token. For production deployments where the API is only called server-to-server (agent to AgentIdP), setting `CORS_ORIGIN` to a specific origin or removing browser-facing CORS entirely is recommended. --- ## Client Secret Storage Client secrets are **never stored in plaintext**. The flow: 1. On credential generation or rotation, AgentIdP generates a random secret string (`sk_live_...`) 2. The plaintext is returned to the caller **once only** in the API response 3. AgentIdP immediately hashes the secret with **bcrypt** (cost factor from `bcryptjs` defaults) and stores only the hash in the `credentials.secret_hash` column 4. On every `POST /token` call, the provided `client_secret` is verified against the stored hash using `bcrypt.compare()` **Implication:** If a client loses their `client_secret`, it cannot be recovered. They must rotate the credential to get a new one. --- ## Secret Storage Guidance | Environment | Recommendation | |-------------|---------------| | Local development | `.env` file, not committed to git | | CI/CD | Environment variables injected by the CI platform (GitHub Actions secrets, GitLab CI variables, etc.) | | Production (Docker) | Docker secrets or bind-mounted files from a secrets manager | | Production (cloud) | AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault (Phase 2) | **Never:** - Commit `.env` to version control - Log environment variables - Pass secrets as command-line arguments (visible in `ps aux`) - Store keys in the database Add `.env` to `.gitignore`: ```bash echo ".env" >> .gitignore echo "*.pem" >> .gitignore ``` --- ## Token Lifetime JWT access tokens expire after **3600 seconds (1 hour)**. This is hardcoded in `src/utils/jwt.ts`. There is no refresh token — clients must re-authenticate via `POST /token` when the token expires. The 1-hour lifetime is a balance between security (short-lived tokens limit exposure if stolen) and operational load (clients don't need to authenticate every few minutes).