> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ensemble.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Security & Authentication

> Secure your Conductor APIs with authentication, permissions, and API key management

## 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:

### Path 1: HTTP Triggers (Recommended for Public APIs)

Triggers are defined in ensemble YAML and go through the full routing system:

```yaml theme={null}
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:

```bash theme={null}
# 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)

```bash theme={null}
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:

```yaml theme={null}
# 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:

```bash theme={null}
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:

```bash theme={null}
wrangler kv:namespace create "API_KEYS"
```

2. Add to `wrangler.toml`:

```toml theme={null}
[[kv_namespaces]]
binding = "API_KEYS"
id = "your-kv-namespace-id"
```

3. Configure in your trigger:

```yaml theme={null}
trigger:
  - type: http
    path: /api/data
    auth:
      type: apiKey  # Uses API_KEYS KV namespace
```

4. Generate keys with the CLI:

```bash theme={null}
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](https://unkey.dev):

<Note>
  **Unkey requires installing a plugin** and an Unkey account. For most use cases, the simple `apiKey` method above is sufficient and recommended.
</Note>

```typescript theme={null}
// 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:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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

```bash theme={null}
# 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

```bash theme={null}
ensemble conductor keys list

# Output as JSON
ensemble conductor keys list --json
```

### Revoke a Key

```bash theme={null}
ensemble conductor keys revoke key_abc123
```

### Get Key Info

```bash theme={null}
ensemble conductor keys info key_abc123
```

### Rotate a Key

Generate a new key while keeping the same metadata:

```bash theme={null}
ensemble conductor keys rotate key_abc123
```

## Configuration

### Security Configuration

```typescript theme={null}
// 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:**

```yaml theme={null}
# 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:**

```typescript theme={null}
// conductor.config.ts
export default {
  api: {
    execution: {
      ensembles: { requireExplicit: true },  // Require opt-in
      agents: { requireExplicit: true },
    },
  },
}
```

```yaml theme={null}
# 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
```

<Note>
  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.
</Note>

### 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:

```yaml theme={null}
# 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:

```yaml theme={null}
# 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:

```typescript theme={null}
// 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:

```typescript theme={null}
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,
})
```

<Warning>
  **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.
</Warning>

### 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:

```typescript theme={null}
// 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:

```yaml theme={null}
# agent.yaml
name: internal-service-proxy
operation: code
handler: ./proxy.ts
security:
  ssrf: false  # ⚠️ Disables SSRF protection for this agent only
```

<Warning>
  Only disable SSRF protection for agents that exclusively make requests to hardcoded, trusted URLs.
  Never disable for agents that accept user-provided URLs.
</Warning>

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](/conductor/core-concepts/location-context) for privacy law compliance:

### Jurisdiction Detection

```typescript theme={null}
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

```typescript theme={null}
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](/conductor/core-concepts/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:

```bash theme={null}
# 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

```bash theme={null}
# 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:

```typescript theme={null}
// conductor.config.ts
export default {
  security: {
    autoPermissions: true,
  },
}
```

### 6. Disable Direct Agent Execution

If you don't need to call agents directly:

```typescript theme={null}
// conductor.config.ts
export default {
  security: {
    allowDirectAgentExecution: false,
  },
}
```

### 7. Use Edge Context for Bot Detection

Leverage [edge context](/conductor/core-concepts/edge-context) for traffic analysis:

```typescript theme={null}
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:**

```json theme={null}
{
  "input": {
    // Ensemble input data
  }
}
```

**Response:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "input": {
    // Agent input data (required)
  },
  "config": {
    // Optional config overrides
  }
}
```

**Response:**

```json theme={null}
{
  "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):
   ```typescript theme={null}
   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}`
