Skip to main content

Overview

Conductor is secure by default. All API routes require authentication unless explicitly configured otherwise. This guide covers:
  1. Two Execution Paths - Triggers vs API routes
  2. API Authentication - Bearer tokens and API keys
  3. Permission System - Fine-grained access control
  4. API Key Management - CLI commands for key generation
  5. Configuration - Security settings in conductor.config.ts
  6. SSRF Protection - Built-in protection against server-side request forgery

Two Execution Paths

Conductor provides two ways to execute ensembles and agents: 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}
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:
  1. Create a KV namespace for API keys:
wrangler kv:namespace create "API_KEYS"
  1. Add to wrangler.toml:
[[kv_namespaces]]
binding = "API_KEYS"
id = "your-kv-namespace-id"
  1. Configure in your trigger:
trigger:
  - type: http
    path: /api/data
    auth:
      type: apiKey  # Uses API_KEYS KV namespace
  1. 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:
FeatureSimple apiKey (Default)Unkey (Advanced)
SetupJust KV bindingPlugin + API key
StorageCloudflare KVUnkey service
Rate limitingManualBuilt-in
Key rotationManualBuilt-in
Usage analyticsManualBuilt-in
CostFree (KV included)Unkey pricing
DependenciesNone@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

PermissionDescription
*Superuser - full access
ensemble:*All ensemble permissions
ensemble:*:executeExecute any ensemble
ensemble:invoice-pdf:executeExecute specific ensemble
ensemble:billing-*:executeExecute ensembles starting with “billing-”
agent:*:executeExecute any agent
agent:http:executeExecute 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:
requireExplicitapiExecutableResult
false (default)undefined✅ Allowed
falsetrue✅ Allowed
falsefalse❌ Denied
trueundefined❌ Denied
truetrue✅ Allowed
truefalse❌ 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:
RangeDescription
127.0.0.0/8Localhost
10.0.0.0/8Private Class A
172.16.0.0/12Private Class B
192.168.0.0/16Private Class C
169.254.0.0/16Link-local (AWS/GCP/Azure metadata service)
0.0.0.0/8Current network
::1IPv6 localhost
fc00::/7IPv6 unique local
fe80::/10IPv6 link-local
localhost, *.local, *.internalInternal 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 TypeCoverage
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)
}
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:
  1. Auth Required by Default: All /api/v1/* routes now require authentication. To restore open access (not recommended):
    export default {
      auth: {
        requireAuth: false,
      },
    }
    
  2. Docs Auth Default Changed: Documentation now defaults to required auth. Shipped templates explicitly set public.
  3. New Route Structure: Prefer the new URL-based routes:
    • Old: POST /api/v1/execute with {"ensemble": "name"}
    • New: POST /api/v1/execute/ensemble/{name}