Overview
Conductor is secure by default. All API routes require authentication unless explicitly configured otherwise. This guide covers:
- Two Execution Paths - Triggers vs API routes
- API Authentication - Bearer tokens and API keys
- Permission System - Fine-grained access control
- API Key Management - CLI commands for key generation
- Configuration - Security settings in
conductor.config.ts
- SSRF Protection - Built-in protection against server-side request forgery
Two Execution Paths
Conductor provides two ways to execute ensembles and agents:
Path 1: HTTP Triggers (Recommended for Public APIs)
Triggers are defined in ensemble YAML and go through the full routing system:
name: public-api
trigger:
- type: http
path: /api/users/:id
methods: [GET]
public: true # Must be explicit!
flow:
- agent: fetch-user
Security Features:
- ✅ Explicit
public: true required for unauthenticated access
- ✅ Per-trigger auth configuration
- ✅ Rate limiting support
- ✅ CORS configuration
- ✅ Path-based routing
Path 2: API Execute Routes (For Service-to-Service)
The /api/v1/execute/* routes provide direct access to run ensembles and agents:
# Execute an ensemble
POST /api/v1/execute/ensemble/invoice-pdf
Authorization: Bearer <token>
Content-Type: application/json
{"input": {"orderId": "12345"}}
# Execute an agent directly (if enabled)
POST /api/v1/execute/agent/http
Authorization: Bearer <token>
Content-Type: application/json
{"input": {"url": "https://api.example.com/data"}}
Security Features:
- ✅ Authentication required by default
- ✅ Permission-based access control
- ✅ Direct agent execution can be disabled
- ✅ Works with any auth provider (JWT, API keys, Unkey)
API Authentication
All /api/v1/* routes require authentication by default. Conductor supports multiple authentication methods:
Bearer Token (JWT)
curl -X POST https://your-worker.workers.dev/api/v1/execute/ensemble/my-workflow \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"input": {}}'
Configure JWT validation in your auth provider or use the built-in bearer provider:
# In ensemble trigger config
auth:
type: bearer
secret: ${env.JWT_SECRET}
API Key (Recommended Default)
The simplest authentication method uses API keys stored in Cloudflare KV:
curl -X POST https://your-worker.workers.dev/api/v1/execute/ensemble/my-workflow \
-H "X-API-Key: cnd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"input": {}}'
Setup:
- Create a KV namespace for API keys:
wrangler kv:namespace create "API_KEYS"
- Add to
wrangler.toml:
[[kv_namespaces]]
binding = "API_KEYS"
id = "your-kv-namespace-id"
- Configure in your trigger:
trigger:
- type: http
path: /api/data
auth:
type: apiKey # Uses API_KEYS KV namespace
- Generate keys with the CLI:
ensemble conductor keys generate --name "my-service" --permissions "ensemble:*:execute"
Unkey Integration (Advanced)
For advanced API key management with built-in rate limiting, usage analytics, and key rotation, you can optionally use Unkey:
Unkey requires installing a plugin and an Unkey account. For most use cases, the simple apiKey method above is sufficient and recommended.
// conductor.config.ts
export default {
auth: {
provider: 'unkey',
rootKey: process.env.UNKEY_ROOT_KEY,
},
}
Comparison:
| Feature | Simple apiKey (Default) | Unkey (Advanced) |
|---|
| Setup | Just KV binding | Plugin + API key |
| Storage | Cloudflare KV | Unkey service |
| Rate limiting | Manual | Built-in |
| Key rotation | Manual | Built-in |
| Usage analytics | Manual | Built-in |
| Cost | Free (KV included) | Unkey pricing |
| Dependencies | None | @unkey/api package |
Recommendation: Start with simple apiKey. Only switch to Unkey if you need its advanced features like built-in rate limiting or usage analytics.
Permission System
Conductor uses an industry-standard permission format compatible with OAuth 2.0 / RBAC patterns:
{resource}:{name}:{action}
Permission Examples
| Permission | Description |
|---|
* | Superuser - full access |
ensemble:* | All ensemble permissions |
ensemble:*:execute | Execute any ensemble |
ensemble:invoice-pdf:execute | Execute specific ensemble |
ensemble:billing-*:execute | Execute ensembles starting with “billing-” |
agent:*:execute | Execute any agent |
agent:http:execute | Execute specific agent |
Wildcard Patterns
The permission system supports glob-style wildcards:
// These all match "ensemble:billing-invoice:execute"
"*" // Superuser
"ensemble:*" // All ensemble permissions
"ensemble:*:execute" // Execute any ensemble
"ensemble:billing-*:execute" // Pattern matching
Auto-Permissions
When autoPermissions is enabled, Conductor automatically requires the appropriate permission for each resource:
// conductor.config.ts
export default {
security: {
autoPermissions: true,
},
}
With this enabled:
POST /api/v1/execute/ensemble/invoice-pdf requires ensemble:invoice-pdf:execute
POST /api/v1/execute/agent/http requires agent:http:execute
API Key Management
Conductor includes CLI commands for managing API keys stored in Cloudflare KV.
Generate a Key
# Basic key with full access
ensemble conductor keys generate --name "admin-key" --permissions "*"
# Scoped key for specific ensembles
ensemble conductor keys generate \
--name "billing-service" \
--permissions "ensemble:billing-*:execute,ensemble:invoice-*:execute" \
--expires "90d"
# Key that never expires
ensemble conductor keys generate \
--name "internal-service" \
--permissions "ensemble:*:execute" \
--expires "never"
Output:
✓ API Key generated
Key ID: key_abc123
Key: cnd_live_xxxxxxxxxxxxxxxxxxxx
Permissions: ensemble:billing-*:execute, ensemble:invoice-*:execute
Expires: 2024-05-15
⚠️ Save this key now - it won't be shown again!
List Keys
ensemble conductor keys list
# Output as JSON
ensemble conductor keys list --json
Revoke a Key
ensemble conductor keys revoke key_abc123
Get Key Info
ensemble conductor keys info key_abc123
Rotate a Key
Generate a new key while keeping the same metadata:
ensemble conductor keys rotate key_abc123
Configuration
Security Configuration
// conductor.config.ts
export default {
auth: {
/**
* Require authentication on /api/* routes
* @default true (SECURE BY DEFAULT)
*/
requireAuth: true,
/** List of valid API keys (for simple setups) */
apiKeys: [process.env.API_KEY],
/** Allow anonymous access (not recommended for production) */
allowAnonymous: false,
},
security: {
/**
* Allow direct agent execution via /api/v1/execute/agent/:name
* Set to false to only allow ensemble execution
* @default true
*/
allowDirectAgentExecution: true,
/**
* Automatically require resource-specific permissions
* When true, executing ensemble "foo" requires permission "ensemble:foo:execute"
* @default false
*/
autoPermissions: false,
},
/**
* API execution controls
* Controls which agents and ensembles can be executed via the Execute API
*/
api: {
execution: {
agents: {
/**
* When true, agents must have apiExecutable: true to be executable via API
* When false (default), all agents are executable unless apiExecutable: false
*/
requireExplicit: false,
},
ensembles: {
/**
* When true, ensembles must have apiExecutable: true to be executable via API
* When false (default), all ensembles are executable unless apiExecutable: false
*/
requireExplicit: false,
},
},
},
}
API Execution Access Control
The apiExecutable flag on agents and ensembles controls whether they can be executed via the Execute API (/api/v1/execute/*).
Per-Agent/Ensemble Configuration:
# Ensemble that cannot be executed via API
name: internal-workflow
apiExecutable: false # Prevents API execution
# Agent that requires explicit opt-in
agent: sensitive-operation
apiExecutable: true # Required when requireExplicit: true
Behavior Matrix:
requireExplicit | apiExecutable | Result |
|---|
false (default) | undefined | ✅ Allowed |
false | true | ✅ Allowed |
false | false | ❌ Denied |
true | undefined | ❌ Denied |
true | true | ✅ Allowed |
true | false | ❌ Denied |
Example: Strict Production Setup:
// conductor.config.ts
export default {
api: {
execution: {
ensembles: { requireExplicit: true }, // Require opt-in
agents: { requireExplicit: true },
},
},
}
# This ensemble CAN be executed via API
name: public-workflow
apiExecutable: true
# This ensemble CANNOT be executed via API (no apiExecutable)
name: internal-only
# apiExecutable defaults to undefined, so blocked when requireExplicit: true
When requireExplicit: true, you get an allowlist model - only explicitly marked agents/ensembles are accessible via the Execute API. This is recommended for production environments.
Docs Authentication
By default, documentation follows the same security model as other routes. The built-in docs-serve ensemble explicitly sets public: true for convenience:
# Default docs-serve ensemble (built-in)
name: docs-serve
trigger:
- type: http
paths:
- path: /docs
methods: [GET]
- path: /docs/:slug
methods: [GET]
public: true # Explicitly public for docs
To require authentication for docs, create a custom docs ensemble:
# ensembles/docs-internal.yaml
name: docs-internal
trigger:
- type: http
path: /docs/internal
methods: [GET]
public: false # Requires authentication
flow:
- name: render
agent: docs
config:
title: Internal Documentation
basePath: /docs/internal
output:
_raw: ${render.output}
The public: false setting means the route requires authentication. Conductor will enforce auth based on the request headers (Bearer token, API key, etc.).
SSRF Protection
Conductor includes built-in SSRF (Server-Side Request Forgery) protection for all HTTP operations. This prevents attackers from using your agents to probe internal network resources, cloud metadata services, or localhost.
How It Works
When agents make HTTP requests using user-provided URLs, Conductor automatically validates the URL before making the request:
// This happens automatically in agents that accept user URLs
import { safeFetch, validateURL } from '@ensemble-edge/conductor'
// safeFetch blocks requests to private IPs
await safeFetch(userProvidedUrl) // Throws if URL is unsafe
// You can also validate URLs manually
validateURL(url) // Throws if URL points to private/internal address
Blocked Address Ranges
The following are automatically blocked:
| Range | Description |
|---|
127.0.0.0/8 | Localhost |
10.0.0.0/8 | Private Class A |
172.16.0.0/12 | Private Class B |
192.168.0.0/16 | Private Class C |
169.254.0.0/16 | Link-local (AWS/GCP/Azure metadata service) |
0.0.0.0/8 | Current network |
::1 | IPv6 localhost |
fc00::/7 | IPv6 unique local |
fe80::/10 | IPv6 link-local |
localhost, *.local, *.internal | Internal hostnames |
Bypassing SSRF Protection (Use Carefully)
In rare cases where you need to access internal resources (e.g., internal microservices), you can bypass SSRF protection:
import { safeFetch } from '@ensemble-edge/conductor'
// WARNING: Only use this for trusted, hardcoded URLs
// Never use allowInternalRequests with user-provided URLs
const response = await safeFetch(internalServiceUrl, {
allowInternalRequests: true,
})
Never enable allowInternalRequests for user-provided URLs. This completely bypasses SSRF protection and could allow attackers to access internal services, cloud metadata, and sensitive resources.
Universal Coverage
SSRF protection is enabled by default for ALL agents - both built-in and user-created:
| Agent Type | Coverage |
|---|
| Built-in agents (RAG, HITL) | ✅ Automatic |
| Template agents (fetch, scrape, redirect, etc.) | ✅ Automatic |
| User-created agents | ✅ Automatic via context.fetch |
When your agent handler receives the AgentExecutionContext, it includes a pre-configured fetch function with SSRF protection:
// Your custom agent - SSRF protection is automatic
export default async function(context: AgentExecutionContext) {
const { fetch, input } = context
// This fetch is SSRF-protected - blocks private IPs automatically
const response = await fetch(input.userProvidedUrl)
return { data: await response.json() }
}
Disabling SSRF Protection (Per-Agent)
In rare cases, you can disable SSRF protection for a specific agent:
# agent.yaml
name: internal-service-proxy
operation: code
handler: ./proxy.ts
security:
ssrf: false # ⚠️ Disables SSRF protection for this agent only
Only disable SSRF protection for agents that exclusively make requests to hardcoded, trusted URLs.
Never disable for agents that accept user-provided URLs.
For hardcoded API endpoints (like Twilio, Resend, OpenAI), you can use regular fetch() directly since the URLs are trusted.
Privacy Compliance
Conductor provides built-in location context for privacy law compliance:
Jurisdiction Detection
export default async function(context: AgentExecutionContext) {
const { location, input } = context
// Automatic jurisdiction detection
if (location?.isGDPR) {
// User is in EU/EEA/UK - opt-in consent required
if (!input.consents?.analytics) {
return { requiresConsent: true, jurisdiction: 'GDPR' }
}
}
if (location?.isCCPA) {
// User is in California - opt-out model
// Can process unless user has opted out
}
// Process with appropriate compliance
return processUserData(input)
}
Consent Helpers
export default async function(context: AgentExecutionContext) {
const { location } = context
// Check specific consent purposes
const purposes = ['analytics', 'marketing', 'personalization']
const requiredConsents = purposes.filter(
purpose => location?.requiresConsent(purpose as any)
)
return {
consentModel: location?.consentModel, // 'opt-in' | 'opt-out' | 'none'
requiredConsents,
jurisdiction: location?.jurisdiction
}
}
See Location Context for complete privacy compliance documentation.
Security Best Practices
1. Never Expose API Keys in Client Code
API keys should only be used server-side. For client applications, use:
- Short-lived JWT tokens
- OAuth 2.0 flows
- Session cookies
2. Use Scoped Permissions
Instead of giving services full access (*), scope their permissions:
# Bad: Full access
ensemble conductor keys generate --name "billing" --permissions "*"
# Good: Scoped to what's needed
ensemble conductor keys generate --name "billing" \
--permissions "ensemble:billing-*:execute,ensemble:invoice-*:execute"
3. Rotate Keys Regularly
# Rotate keys periodically
ensemble conductor keys rotate key_abc123
4. Use Unkey for Production
For production deployments, use Unkey for:
- Rate limiting
- Usage analytics
- Automatic rotation
- Key insights
5. Enable Auto-Permissions
For strict access control:
// conductor.config.ts
export default {
security: {
autoPermissions: true,
},
}
6. Disable Direct Agent Execution
If you don’t need to call agents directly:
// conductor.config.ts
export default {
security: {
allowDirectAgentExecution: false,
},
}
7. Use Edge Context for Bot Detection
Leverage edge context for traffic analysis:
export default async function(context: AgentExecutionContext) {
const { edge } = context
// Detect automated traffic
if (edge?.isFromCloudProvider()) {
// Requests from AWS, GCP, Azure often indicate bots
return { action: 'challenge', reason: edge.getCloudProvider() }
}
// Check for modern protocols
if (!edge?.isModernTLS()) {
// Outdated clients may be suspicious
return { action: 'warn', reason: 'legacy_tls' }
}
return { action: 'allow' }
}
API Reference
Execute Ensemble
POST /api/v1/execute/ensemble/{name}
Headers:
Authorization: Bearer <token> or X-API-Key: <key>
Content-Type: application/json
Request Body:
{
"input": {
// Ensemble input data
}
}
Response:
{
"success": true,
"output": {
// Ensemble output
},
"metadata": {
"executionId": "req_abc123",
"duration": 1234,
"timestamp": 1699900000000
}
}
Error Responses:
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Missing required permission
404 Not Found - Ensemble not found
500 Internal Server Error - Execution failed
Execute Agent
POST /api/v1/execute/agent/{name}
Headers:
Authorization: Bearer <token> or X-API-Key: <key>
Content-Type: application/json
Request Body:
{
"input": {
// Agent input data (required)
},
"config": {
// Optional config overrides
}
}
Response:
{
"success": true,
"data": {
// Agent output
},
"metadata": {
"executionId": "req_abc123",
"duration": 567,
"timestamp": 1699900000000
}
}
Error Responses:
400 Bad Request - Missing required input
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Direct agent execution disabled or missing permission
404 Not Found - Agent not found
500 Internal Server Error - Execution failed
Migrating from v0.3.x
If you’re upgrading from v0.3.x, note these breaking changes:
-
Auth Required by Default: All
/api/v1/* routes now require authentication. To restore open access (not recommended):
export default {
auth: {
requireAuth: false,
},
}
-
Docs Auth Default Changed: Documentation now defaults to
required auth. Shipped templates explicitly set public.
-
New Route Structure: Prefer the new URL-based routes:
- Old:
POST /api/v1/execute with {"ensemble": "name"}
- New:
POST /api/v1/execute/ensemble/{name}