Wardgate Configuration Reference¶
This document describes all configuration options for Wardgate.
Configuration Files¶
Wardgate uses two configuration files:
- config.yaml - Main configuration (endpoints, rules, notifications)
- .env - Credentials and secrets (never commit to version control)
Quick Start¶
# Create config files from examples
cp config.yaml.example config.yaml
cp .env.example .env
# Edit with your settings
vim config.yaml
vim .env
# Run Wardgate
./wardgate -config config.yaml
Presets (Easy Configuration)¶
Presets let you configure popular APIs with minimal setup. Instead of manually specifying upstream URLs, auth types, and rules, just use a preset name.
Presets are stored as YAML files in the presets/ directory. You must set presets_dir to use them:
presets_dir: ./presets
endpoints:
todoist:
preset: todoist
auth:
credential_env: WARDGATE_CRED_TODOIST_API_KEY
capabilities:
read_data: allow
create_tasks: allow
delete_tasks: deny
Included presets: todoist, github, cloudflare, google-calendar, postmark, sentry, plausible
Capability actions: allow, deny, ask (require human approval)
See the Presets Reference for all presets, capabilities, and examples.
Overriding Preset Defaults¶
You can override any preset value:
endpoints:
custom-todoist:
preset: todoist
upstream: https://my-proxy.example.com/todoist # Custom upstream
auth:
credential_env: MY_TODOIST_KEY
rules: # Custom rules replace capabilities entirely when no capabilities are set
- match: { method: GET }
action: allow
- match: { method: "*" }
action: deny
Combining Capabilities with Custom Rules¶
When both capabilities and rules are specified, your custom rules are evaluated first (first-match-wins), followed by the capability-expanded rules. This lets you use capabilities for broad defaults and add surgical overrides via rules:
endpoints:
mail:
preset: imap
upstream: imap://protonmail-bridge:143
auth:
type: plain
credential_env: WARDGATE_CRED_IMAP
capabilities:
list_folders: allow
rules:
# Allow reading only one specific folder (evaluated before capabilities)
- match: { method: GET, path: "/folders/Folders/Agent John*" }
action: allow
Evaluation order:
- User-defined
rules(highest priority, first-match-wins) - Rules expanded from
capabilities - Catch-all deny (automatically appended)
Custom Presets (User-Defined)¶
You can define your own presets for APIs not included with the source code. You are encouraged to share them with the community by adding them to the presets/ directory via a Pull Request.
Option 1: External Preset Files¶
Create YAML files in a presets/ directory:
# presets/helpscout.yaml
name: helpscout
description: "Help Scout customer support API"
upstream: https://api.helpscout.net/v2
auth_type: bearer
capabilities:
- name: read_conversations
description: "Read conversations and threads"
rules:
- match: { method: GET, path: "/conversations*" }
- name: reply_to_conversations
description: "Reply to customer conversations"
rules:
- match: { method: POST, path: "/conversations/*/reply" }
- name: manage_customers
description: "Create and update customer profiles"
rules:
- match: { method: POST, path: "/customers" }
- match: { method: PUT, path: "/customers/*" }
default_rules:
- match: { method: GET }
action: allow
- match: { method: "*" }
action: deny
Then reference in your config:
presets_dir: ./presets
endpoints:
helpscout:
preset: helpscout
auth:
credential_env: HELPSCOUT_TOKEN
capabilities:
read_conversations: allow
reply_to_conversations: ask
manage_customers: deny
Option 2: Inline Custom Presets¶
Define presets directly in your config file:
custom_presets:
my-internal-api:
description: "Company Internal API"
upstream: https://api.internal.company.com/v1
auth_type: bearer
capabilities:
- name: read_data
description: "Read resources"
rules:
- match: { method: GET }
- name: write_data
description: "Create/update resources"
rules:
- match: { method: POST }
- match: { method: PUT }
default_rules:
- match: { method: GET }
action: allow
- match: { method: "*" }
action: deny
endpoints:
internal:
preset: my-internal-api
auth:
credential_env: INTERNAL_API_KEY
capabilities:
read_data: allow
write_data: ask
Custom Preset File Format¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | Preset name (defaults to filename) |
description |
string | Yes | Human-readable description |
upstream |
string | Yes | Base URL of the API |
docs_url |
string | No | Link to API documentation (exposed in discovery) |
auth_type |
string | Yes | Auth type (bearer, basic, header, or plain) |
capabilities |
array | No | List of named capabilities |
default_rules |
array | No | Default rules when no capabilities specified |
Capability Definition¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Capability identifier |
description |
string | Yes | Human-readable description |
rules |
array | Yes | Rules to apply when capability is enabled |
Preset Priority¶
When multiple presets exist with the same name:
- Inline custom presets (highest priority) -
custom_presetsin config.yaml - External preset files - YAML files in
presets_dir
Full Configuration Example¶
# config.yaml
server:
listen: ":8080"
base_url: "https://wardgate.example.com"
agents:
- id: my-agent
key_env: WARDGATE_AGENT_KEY
notify:
timeout: "5m"
slack:
webhook_url: "https://hooks.slack.com/services/..."
endpoints:
todoist-api:
upstream: https://api.todoist.com/rest/v2
auth:
type: bearer
credential_env: WARDGATE_CRED_TODOIST_API_KEY
rules:
- match: { method: GET }
action: allow
rate_limit: { max: 100, window: "1m" }
- match: { method: POST, path: "/tasks" }
action: allow
- match: { method: DELETE }
action: ask
- match: { method: "*" }
action: deny
Configuration Sections¶
server¶
Server configuration.
| Field | Type | Default | Description |
|---|---|---|---|
listen |
string | :8080 |
Address and port to listen on |
base_url |
string | Base URL for links in notifications | |
admin_key_env |
string | Env var for admin key (enables Web UI at /ui/ and CLI) |
|
logging.max_entries |
int | 1000 |
Max log entries to keep in memory for dashboard |
logging.store_bodies |
bool | false |
Store request bodies in logs (privacy consideration) |
seal.key_env |
string | Env var holding 32-byte hex AES-256 key for sealed credentials | |
seal.cache_size |
int | 1000 |
Max entries in the decryption LRU cache |
seal.allowed_headers |
[]string | [Authorization, X-Api-Key, X-Auth-Token, Proxy-Authorization] |
Whitelist of headers agents can seal |
server:
listen: ":8080" # Listen on all interfaces, port 8080
base_url: "https://wardgate.example.com" # For links in notifications
admin_key_env: WARDGATE_ADMIN_KEY # Enables admin Web UI and CLI
grants_file: grants.json # Path to dynamic grants file (default: grants.json)
logging:
max_entries: 1000 # Keep last 1000 requests in memory
store_bodies: false # Don't store request bodies by default
Dynamic Grants¶
Wardgate supports dynamic grants -- runtime-added policy rules that override static rules. Grants can be permanent or time-limited. See Grants Documentation for full details.
Admin UI and CLI¶
When admin_key_env is set and the corresponding environment variable contains a key, Wardgate enables:
- Web UI at
/ui/- Dashboard for viewing and managing pending approvals - CLI commands -
wardgate approvals list|approve|deny|view|history|monitor
The admin key authenticates both the Web UI (via localStorage + Authorization header) and CLI commands.
Listen Address Examples¶
listen: ":8080" # All interfaces, port 8080
listen: "127.0.0.1:8080" # Localhost only
listen: "0.0.0.0:443" # All interfaces, HTTPS port
agents¶
List of agents allowed to use the gateway.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier for the agent |
key_env |
string | Yes | Environment variable containing the agent's API key |
agents:
- id: my-agent
key_env: WARDGATE_AGENT_KEY
- id: another-agent
key_env: WARDGATE_AGENT_2_KEY
Agents authenticate using the Authorization: Bearer <key> header.
Managing Agents via CLI¶
Instead of manually generating keys and editing files, use the CLI:
# Add a new agent (generates key, updates config.yaml and .env)
wardgate agent add my-agent
# Remove an agent
wardgate agent remove my-agent
agent add generates a random 32-byte key, appends the env var to .env, adds the agent to config.yaml, and prints the key for configuring wardgate-cli.
JWT Agent Authentication¶
Instead of managing static keys per agent, you can configure JWT-based authentication. This is useful when you have an orchestrator that spins up ephemeral sandboxed agents -- just sign a short-lived JWT with the agent ID and inject it into the sandbox.
| Field | Type | Required | Description |
|---|---|---|---|
secret |
string | No* | HMAC signing secret (inline, for dev) |
secret_env |
string | No* | Env var holding the HMAC signing secret |
issuer |
string | No | Expected iss claim (rejected if mismatch) |
audience |
string | No | Expected aud claim (rejected if mismatch) |
* One of secret or secret_env is required.
server:
listen: ":8080"
jwt:
secret_env: WARDGATE_JWT_SECRET
issuer: "my-orchestrator" # optional
audience: "wardgate" # optional
The JWT sub (subject) claim is used as the agent ID. Standard exp handles expiry. Supported signing algorithms: HS256, HS384, HS512.
Example JWT payload:
How it works:
- Static keys are checked first (fast map lookup)
- If the token doesn't match any static key and JWT is configured, it's validated as a JWT
- The agent ID from the sub claim is used for all downstream features (policy, rate limiting, audit, scoping)
- wardgate-cli sends Authorization: Bearer <key> where the key can be any string including a JWT -- no client changes needed
No token revocation: JWT is stateless by design. Use short-lived tokens with exp for ephemeral agents. Both static keys and JWT can be used simultaneously.
Managing Conclaves via CLI¶
# Add a new conclave (generates key, updates config.yaml and .env)
wardgate conclave add obsidian "Personal notes vault"
# Remove a conclave
wardgate conclave remove obsidian
conclave add generates a random key, appends the env var to .env, adds the conclave to config.yaml with a default deny-all rule, and prints a starter wardgate-exec config.
endpoints¶
Map of endpoint names to their configuration.
| Field | Type | Required | Description |
|---|---|---|---|
adapter |
string | No | Adapter type: http (default), imap, smtp, or ssh |
preset |
string | No | Preset name to use as base configuration (see Presets) |
description |
string | No | Human-readable description (shown in discovery API) |
agents |
array | No | Agent IDs allowed to access this endpoint (empty = all agents) |
upstream |
string | Yes* | URL of the upstream service |
allowed_upstreams |
array | No | Glob patterns for dynamic upstream targets (see Dynamic Upstreams) |
docs_url |
string | No | Link to API documentation (exposed in discovery, overrides preset) |
auth |
object | Yes | Authentication configuration |
rules |
array | No | Policy rules (default: deny all) |
filter |
object | No | Sensitive data filtering (see Sensitive Data Filtering) |
imap |
object | No | IMAP-specific settings (for adapter: imap) |
smtp |
object | No | SMTP-specific settings (for adapter: smtp) |
ssh |
object | No | SSH-specific settings (for adapter: ssh) |
* Either upstream or allowed_upstreams is required for HTTP endpoints.
endpoints:
todoist-api:
agents: [tessa] # Only agent "tessa" can access this endpoint
upstream: https://api.todoist.com/rest/v2
auth:
type: bearer
credential_env: WARDGATE_CRED_TODOIST_API_KEY
rules:
- match: { method: GET }
action: allow
When agents is omitted or empty, all authenticated agents can access the endpoint. When specified, only the listed agents are permitted; other agents receive a 403 Forbidden response, and the endpoint is hidden from their GET /endpoints discovery response.
Endpoints are accessed as: http://wardgate:8080/{endpoint-name}/{path}
endpoints.auth¶
Authentication configuration for the upstream service.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes* | Authentication type (bearer, basic, header, plain) |
credential_env |
string | Yes* | Environment variable containing the credential |
header |
string | No | Header name (required when type: header) |
prefix |
string | No | Value prefix (optional, used with type: header) |
sealed |
bool | No | Credentials come from agent's encrypted X-Wardgate-Sealed-* headers |
* Not required when sealed: true.
# Static credentials (Wardgate injects from vault)
auth:
type: bearer
credential_env: WARDGATE_CRED_TODOIST_API_KEY
# Basic auth (credential_env value is user:password, base64-encoded automatically)
auth:
type: basic
credential_env: WARDGATE_CRED_MY_API
# Custom header auth (any header name and optional prefix)
auth:
type: header
header: Authorization
prefix: "AccessKey "
credential_env: WARDGATE_CRED_BIRD_API_KEY
# Sealed credentials (agent provides encrypted values)
auth:
sealed: true
Currently supported types:
- bearer - Adds Authorization: Bearer <credential> header
- basic - Adds Authorization: Basic <base64(credential)> header. Credential format is user:password.
- header - Sets a custom header: <header>: <prefix><credential>. Requires header field; prefix is optional.
- plain - For IMAP: credential format is username:password
When sealed: true, the agent sends encrypted header values prefixed with X-Wardgate-Sealed-*. Wardgate decrypts and forwards them. See Sealed Credentials for full documentation.
endpoints.rules¶
Array of policy rules. See Policy Documentation for details.
| Field | Type | Required | Description |
|---|---|---|---|
match |
object | Yes | Conditions to match |
match.method |
string | No | HTTP method to match (GET, POST, *, etc.) |
match.path |
string | No | Path pattern to match |
match.command |
string | No | Exec: glob match on command path (e.g., /usr/bin/python*) |
match.args_pattern |
string | No | Exec: regex match on argument string |
match.cwd_pattern |
string | No | Exec: glob match on working directory |
action |
string | Yes | Action to take (allow, deny, ask) |
message |
string | No | Message to return (for deny) |
rate_limit |
object | No | Rate limiting configuration |
time_range |
object | No | Time-based restrictions |
rules:
- match:
method: GET
path: "/tasks*"
action: allow
rate_limit:
max: 100
window: "1m"
time_range:
hours: ["09:00-17:00"]
days: ["mon", "tue", "wed", "thu", "fri"]
endpoints.rules.rate_limit¶
Rate limiting configuration.
| Field | Type | Default | Description |
|---|---|---|---|
max |
integer | Maximum requests allowed in window | |
window |
string | 1m |
Time window (30s, 5m, 1h) |
endpoints.rules.time_range¶
Time-based restrictions.
| Field | Type | Description |
|---|---|---|
hours |
array | Time ranges in 24h format (["09:00-17:00"]) |
days |
array | Day abbreviations (["mon", "tue", "wed", "thu", "fri"]) |
notify¶
Notification configuration for the ask action.
| Field | Type | Description |
|---|---|---|
timeout |
string | How long to wait for approval (5m, 1h) |
webhook |
object | Generic webhook configuration |
slack |
object | Slack webhook configuration |
notify.webhook¶
Generic webhook notification.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | Webhook URL |
headers |
map | No | Additional headers to send |
webhook:
url: "https://your-service.example.com/notify"
headers:
Authorization: "Bearer your-token"
X-Custom-Header: "value"
Webhook payload:
{
"title": "Approval Required",
"body": "Agent my-agent wants to DELETE /tasks/123",
"request_id": "abc123",
"endpoint": "todoist-api",
"method": "DELETE",
"path": "/tasks/123",
"agent_id": "my-agent",
"dashboard_url": "https://wardgate.example.com/ui/"
}
Note: Webhooks are notification-only. Approvals must be done through the Web UI (requires admin key authentication) or CLI.
notify.slack¶
Slack webhook notification.
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url |
string | Yes | Slack incoming webhook URL |
To get a webhook URL: 1. Go to Slack App Directory 2. Create or select an app 3. Enable Incoming Webhooks 4. Create a webhook for your channel
Environment Variables¶
Credentials are stored in environment variables for security.
Naming Convention¶
# Agent keys
WARDGATE_AGENT_<NAME>_KEY=<agent-secret>
# Upstream credentials
WARDGATE_CRED_<NAME>=<credential>
Example .env File¶
# Admin key (for Web UI and CLI)
WARDGATE_ADMIN_KEY=your-secret-admin-key
# Agent authentication keys
WARDGATE_AGENT_GEORGE_KEY=sk-agent-xyz789
# Upstream API credentials
WARDGATE_CRED_TODOIST_API_KEY=0123456789abcdef
WARDGATE_CRED_GOOGLE_CALENDAR_KEY=AIzaSy...
WARDGATE_CRED_GITHUB_TOKEN=ghp_xxxxxxxxxxxx
Loading Environment Variables¶
# Automatic loading from .env
./wardgate -config config.yaml
# Specify different env file
./wardgate -config config.yaml -env /path/to/.env
# Or export manually
export WARDGATE_AGENT_KEY=sk-agent-abc123
./wardgate -config config.yaml
Command Line Options¶
./wardgate [options]
Options:
-config string
Path to config file (default "config.yaml")
-env string
Path to .env file (default ".env")
-version
Show version and exit
CLI Commands for Approvals¶
Wardgate includes CLI commands for managing approval requests:
# Set environment variables
export WARDGATE_URL=http://localhost:8080
export WARDGATE_ADMIN_KEY=your-secret-admin-key
# List pending approvals
wardgate approvals list
# View details of an approval (including full email content)
wardgate approvals view <id>
# Approve or deny a request
wardgate approvals approve <id>
wardgate approvals deny <id>
# View history of recent decisions
wardgate approvals history
# Monitor mode - live updates with interactive approve/deny
wardgate approvals monitor
Monitor Mode¶
The monitor command provides a live view of pending approvals with interactive commands:
a <id>orapprove <id>- Approve a requestd <id>ordeny <id>- Deny a requestv <id>orview <id>- View full request detailsrorrefresh- Refresh the listqorquit- Exit monitor mode
The list auto-refreshes every 3 seconds.
Multiple Endpoints Example¶
server:
listen: ":8080"
agents:
- id: assistant
key_env: WARDGATE_AGENT_KEY
endpoints:
# Todoist - task management
todoist-api:
upstream: https://api.todoist.com/rest/v2
auth:
type: bearer
credential_env: WARDGATE_CRED_TODOIST
rules:
- match: { method: GET }
action: allow
- match: { method: POST, path: "/tasks" }
action: allow
- match: { method: "*" }
action: deny
# Google Calendar - read only
google-calendar:
upstream: https://www.googleapis.com/calendar/v3
auth:
type: bearer
credential_env: WARDGATE_CRED_GOOGLE
rules:
- match: { method: GET }
action: allow
- match: { method: "*" }
action: deny
# GitHub - limited write
github-api:
upstream: https://api.github.com
auth:
type: bearer
credential_env: WARDGATE_CRED_GITHUB
rules:
- match: { method: GET }
action: allow
- match: { method: POST, path: "/repos/*/issues" }
action: allow
rate_limit: { max: 10, window: "1h" }
- match: { method: "*" }
action: ask
Validation¶
Wardgate validates configuration on startup:
- All endpoints must have
upstreamandauth - All
credential_envandkey_envmust exist in environment - All
actionvalues must be valid (allow,deny,ask) - Rate limit
windowmust be valid duration
Invalid configuration causes startup failure with descriptive error.
Conclaves (Remote Execution Environments)¶
The top-level conclaves: section defines isolated execution environments and their per-conclave policy rules.
conclaves:
obsidian:
description: "Obsidian vault (personal notes)"
key_env: WARDGATE_CONCLAVE_OBSIDIAN_KEY
agents: [tessa] # Only agent "tessa" can execute on this conclave
cwd: /data/vault
rules:
- match: { command: "cat" }
action: allow
- match: { command: "rg" }
action: allow
- match: { command: "tee" }
action: ask
- match: { command: "*" }
action: deny
Conclave Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | No | Human-readable description of the conclave |
key_env |
string | Yes | Environment variable holding the conclave's pre-shared key |
agents |
array | No | Agent IDs allowed to access this conclave (empty = all agents) |
cwd |
string | No | Default working directory for commands |
commands |
map | No | Named command templates (see below) |
rules |
array | No | Policy rules using exec match fields |
filter |
object | No | Output filter for sensitive data (see Output Filtering) |
Conclave Output Filtering¶
Conclave output (stdout/stderr) can be scanned for sensitive data before returning to the agent. This reuses the same filter engine as endpoint filtering.
conclaves:
obsidian:
key_env: WARDGATE_CONCLAVE_OBSIDIAN_KEY
cwd: /data/vault
filter:
enabled: true
patterns: [api_keys, passwords, private_keys]
action: block
commands:
search:
template: "find . -iname {query}"
args: [{ name: query }]
filter:
enabled: false # filenames only, skip filtering
read:
template: "python3 /usr/local/lib/wardgate-tools/file_read.py {file}"
args: [{ name: file }]
# inherits conclave filter
Two levels of configuration:
- Per-conclave
filter:- default for all commands and raw exec - Per-command
filter:on a command definition - overrides the conclave default
The filter fields are the same as endpoint filtering (enabled, patterns, custom_patterns, action, replacement). Supported actions for conclave output: block, redact, log (not ask - the command has already executed).
When action is block and sensitive data is found, the response returns 403 with a description of what was detected. When action is redact, matches are replaced in-place. When action is log, matches are logged but output is returned unchanged.
Command Templates¶
Define pre-made commands that agents invoke by name, supplying only arguments:
conclaves:
obsidian:
key_env: WARDGATE_CONCLAVE_OBSIDIAN_KEY
cwd: /data/vault
commands:
search:
description: "Search notes by filename"
template: "find . -iname {query}"
args:
- name: query
description: "Filename pattern"
grep:
description: "Search note contents"
template: "rg {pattern} | grep -v SECRET1 | grep -v SECRET2"
args:
- name: pattern
description: "Text pattern"
action: ask
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | No | Human-readable description |
template |
string | Yes | Command with {argname} placeholders |
args |
array | No | Ordered argument definitions |
args[].name |
string | Yes | Argument name (matches placeholder in template) |
args[].description |
string | No | Human-readable description |
args[].type |
string | No | path enables path validation (rejects absolute paths and traversal) |
args[].allowed_paths |
array | No | Glob patterns restricting valid paths (requires type: path) |
action |
string | No | allow (default), ask, or deny. Fallback when no rules match |
rules |
array | No | Per-arg policy rules (first match wins, default deny when present) |
rules[].match |
map | Yes | Arg name to glob pattern (all must match, AND logic) |
rules[].action |
string | Yes | allow, ask, or deny |
rules[].message |
string | No | Message to return (for deny) |
Path Validation¶
Args with type: path and allowed_paths are validated before template expansion:
- Absolute paths are rejected
- Path traversal (
../) is rejected - The value must match at least one
allowed_pathsglob pattern
This is a hard boundary - rejected paths return 403 immediately.
Command Rules¶
When rules is present on a command, they are evaluated in order (first match wins). If no rule matches, the request is denied (consistent with conclave-level rules). When rules is absent, the action field is used directly.
commands:
read:
template: "python3 /usr/local/lib/wardgate-tools/file_read.py {file}"
args:
- name: file
type: path
allowed_paths: ["notes/**", "config/**"]
rules:
- match: { file: "notes/**" }
action: allow
- match: { file: "config/**" }
action: ask
# unmatched paths -> default deny
Agents run commands via wardgate-cli run <conclave> <command> [args...]. Arguments are shell-escaped before substitution. See Conclaves for details.
When agents is omitted or empty, all authenticated agents can execute commands on the conclave. When specified, other agents receive 403 Forbidden and the conclave is hidden from their GET /conclaves discovery response.
Exec Match Fields¶
| Field | Type | Description |
|---|---|---|
command |
string | Glob match on command name (e.g., rg, python*, *) |
args_pattern |
string | Regex match on the joined argument string |
cwd_pattern |
string | Glob match on the working directory |
All match fields are optional and AND-ed together. Commands are resolved to absolute paths on the conclave by wardgate-exec.
See Conclaves for full documentation including pipeline support, deployment, and limitations.
IMAP Endpoints¶
For IMAP endpoints, Wardgate exposes a REST API that wraps the IMAP protocol:
endpoints:
imap-personal:
adapter: imap
upstream: imaps://imap.gmail.com:993
auth:
type: plain
credential_env: IMAP_CREDS # format: username:password
imap:
tls: true # Use TLS (default for imaps://) for ProtonBridge use false for StartTLS
insecure_skip_verify: true # Skip TLS cert verification for ProtonBridge
rules:
- match: { path: "/inbox*" }
action: allow
- match: { path: "/folders" }
action: allow
- match: { path: "/*" }
action: deny
IMAP REST API¶
| Endpoint | Method | Description |
|---|---|---|
/folders |
GET | List all mailbox folders |
/folders/{folder} |
GET | Fetch messages from folder |
/folders/{folder}?limit=N |
GET | Limit number of messages |
/folders/{folder}?since=YYYY-MM-DD |
GET | Messages since date |
/folders/{folder}?before=YYYY-MM-DD |
GET | Messages before date |
/folders/{folder}/messages/{uid} |
GET | Get full message by UID |
/folders/{folder}/messages/{uid}/mark-read |
POST | Mark message as read |
/folders/{folder}/messages/{uid}/move?to=X |
POST | Move message to folder |
Message operations are scoped to folders, so policy rules like /folders/inbox* will apply to both listing and reading messages from that folder.
Folder Names with Slashes¶
IMAP folder names can contain slashes (e.g., Folder/Orders). URL-encode them in requests:
| Folder Name | URL Request |
|---|---|
INBOX |
/folders/INBOX |
Folder/Orders |
/folders/Folder%2FOrders |
Work/Projects/Active |
/folders/Work%2FProjects%2FActive |
The %2F is the URL-encoded form of /.
Important: Policy rules use the decoded path, not the encoded form:
# Correct - use decoded folder name
- match:
path: "/folders/Folder/Orders*"
action: allow
# Wrong - don't use URL encoding in rules
- match:
path: "/folders/Folder%2FOrders*"
action: allow
IMAP Upstream URL¶
| Scheme | Port | TLS |
|---|---|---|
imaps:// |
993 | Yes |
imap:// |
143 | No |
endpoints.imap¶
IMAP-specific configuration.
| Field | Type | Default | Description |
|---|---|---|---|
tls |
bool | true | Use TLS connection |
max_conns |
int | 5 | Max connections per endpoint |
idle_timeout_secs |
int | 300 | Idle connection timeout |
SMTP Endpoints¶
For SMTP endpoints, Wardgate exposes a REST API for sending emails:
endpoints:
smtp-personal:
adapter: smtp
upstream: smtps://smtp.gmail.com:465 # Or smtp://smtp.gmail.com:587 for STARTTLS
auth:
type: plain
credential_env: SMTP_CREDS # format: username:password
smtp:
tls: true
from: "your-email@gmail.com"
known_recipients:
- "@company.com"
ask_new_recipients: true
blocked_keywords:
- "password"
- "secret"
rules:
- match: { path: "/send" }
action: allow
SMTP REST API¶
| Endpoint | Method | Description |
|---|---|---|
/send |
POST | Send an email |
Send Request Body¶
{
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": ["bcc@example.com"],
"reply_to": "reply@example.com",
"subject": "Email subject",
"body": "Plain text body",
"html_body": "<html>...</html>"
}
SMTP Upstream URL¶
| Scheme | Port | TLS |
|---|---|---|
smtps:// |
465 | Implicit TLS |
smtp:// |
587 | STARTTLS |
endpoints.smtp¶
SMTP-specific configuration.
| Field | Type | Default | Description |
|---|---|---|---|
tls |
bool | false | Use implicit TLS (port 465) |
starttls |
bool | true | Use STARTTLS upgrade (port 587) |
from |
string | Default from address | |
allowed_recipients |
array | Allowlist of recipients (block all others) | |
known_recipients |
array | Recipients that don't need approval | |
ask_new_recipients |
bool | false | Ask before sending to unknown recipients |
blocked_keywords |
array | Keywords to block in subject/body |
Recipient Patterns¶
Allowlist and known recipients support two patterns:
| Pattern | Example | Matches |
|---|---|---|
| Domain | @company.com |
Any email ending in @company.com |
| Exact | specific@example.com |
Only that exact address |
smtp:
allowed_recipients:
- "@company.com" # Allow any @company.com address
- "partner@external.com" # Allow this specific address
known_recipients:
- "@internal.com" # No approval needed for internal
Content Filtering¶
Block emails containing specific keywords in subject or body:
Keywords are case-insensitive. Any match will reject the email with HTTP 403.
SSH Endpoints¶
For SSH endpoints, Wardgate exposes a REST API that executes commands on a remote host via SSH. This is similar to conclaves but without requiring wardgate-exec on the target host - Wardgate connects directly via SSH.
REST API¶
| Method | Path | Description |
|---|---|---|
| POST | /exec |
Execute a command |
POST /exec
Response:
SSH Configuration¶
| Field | Type | Default | Description |
|---|---|---|---|
host |
string | (required) | SSH server hostname |
port |
int | 22 | SSH server port |
username |
string | (required) | SSH username |
known_host |
string | - | Inline known_hosts entry for host key verification |
known_hosts_file |
string | - | Path to known_hosts file |
insecure_skip_verify |
bool | false | Skip host key verification (not recommended) |
max_sessions |
int | 5 | Maximum concurrent SSH sessions |
timeout_secs |
int | 30 | Per-command timeout in seconds |
Authentication¶
The auth.credential_env environment variable must contain the PEM-encoded SSH private key. Use escaped newlines in .env files:
WARDGATE_SSH_KEY_PROD="-----BEGIN OPENSSH PRIVATE KEY-----\nb3Blbn...\n-----END OPENSSH PRIVATE KEY-----"
Example¶
endpoints:
prod-server:
adapter: ssh
description: "Production server access"
ssh:
host: prod.example.com
port: 22
username: deploy
known_host: "prod.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
auth:
type: ssh-key
credential_env: WARDGATE_SSH_KEY_PROD
capabilities:
exec_commands: allow
Using the preset:
endpoints:
prod-server:
preset: ssh
ssh:
host: prod.example.com
username: deploy
known_host: "prod.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
auth:
credential_env: WARDGATE_SSH_KEY_PROD
capabilities:
exec_commands: ask # Require approval for every command
Host Key Verification¶
Host key verification is required by default. You must provide one of:
known_host: inline known_hosts entry (e.g., fromssh-keyscan prod.example.com)known_hosts_file: path to a known_hosts fileinsecure_skip_verify: true: disable verification (logs a warning, not recommended for production)
Dynamic Upstreams¶
By default, each endpoint has a fixed upstream URL. Dynamic upstreams allow agents to choose the target URL per-request using the X-Wardgate-Upstream header, validated against an allowlist of glob patterns.
This is useful for endpoints that proxy to multiple services sharing the same credentials (e.g., different Google API hosts).
Configuration¶
endpoints:
google-apis:
# No static upstream needed (but can be set as fallback)
allowed_upstreams:
- "https://*.googleapis.com"
- "https://api.example.com/v1"
auth:
type: bearer
credential_env: WARDGATE_CRED_GOOGLE
rules:
- match: { method: GET }
action: allow
Usage¶
Agents set the target per-request:
GET /google-apis/storage/v1/b/my-bucket
X-Wardgate-Upstream: https://storage.googleapis.com
Authorization: Bearer <agent-key>
Pattern Syntax¶
Patterns use glob-style matching with scheme enforcement:
*matches exactly one hostname segment (between dots)**matches one or more hostname segments (across dots)
| Pattern | Matches | Does Not Match |
|---|---|---|
https://api.example.com |
https://api.example.com/any/path |
http://api.example.com (scheme mismatch) |
https://*.googleapis.com |
https://storage.googleapis.com |
https://googleapis.com (no subdomain), https://evil.com.googleapis.com (* is single segment) |
https://**.googleapis.com |
https://storage.googleapis.com, https://intended.com.googleapis.com |
https://googleapis.com (** requires at least one segment) |
https://api.example.com/v1 |
https://api.example.com/v1/users |
https://api.example.com/v1-admin (path segment boundary) |
Hostname matching is case-insensitive. Path matching enforces segment boundaries (/v1 matches /v1/foo but not /v1-admin).
Security¶
- URLs with userinfo (
http://user@host), query parameters, or fragments are rejected - Only
http://andhttps://schemes are allowed - The
X-Wardgate-Upstreamheader is stripped before forwarding to the upstream - Policy rules are evaluated before upstream resolution
Fallback¶
If both upstream and allowed_upstreams are set, the static upstream is used when no X-Wardgate-Upstream header is present. If only allowed_upstreams is set, the header is required.
Sensitive Data Filtering¶
Wardgate can automatically detect and filter sensitive data in API responses and email messages. This prevents agents from seeing OTP codes, verification links, API keys, and other security-sensitive information.
Configuration¶
Filtering is enabled by default for all endpoints. You can configure it per endpoint:
endpoints:
my-api:
upstream: https://api.example.com
auth:
credential_env: API_KEY
filter:
enabled: true # Enable/disable filtering (default: true)
patterns: # Built-in patterns to detect
- otp_codes
- verification_links
- api_keys
action: block # Action: block, redact, ask, log (default: block)
replacement: "[REDACTED]" # Replacement text for redact action
To disable filtering for a specific endpoint (e.g., an OTP inbox for account creation):
endpoints:
otp-inbox:
preset: imap
auth:
credential_env: IMAP_CREDS
filter:
enabled: false # Allow agent to see OTP codes in this mailbox
Global Defaults¶
Set default filter settings for all endpoints:
filter_defaults:
enabled: true
patterns:
- otp_codes
- verification_links
- api_keys
action: block
replacement: "[SENSITIVE DATA REDACTED]"
endpoints.filter¶
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable sensitive data filtering |
patterns |
array | [otp_codes, verification_links, api_keys] |
Built-in patterns to detect |
custom_patterns |
array | User-defined regex patterns | |
action |
string | block |
Action when detected: block, redact, ask, log |
replacement |
string | [SENSITIVE DATA REDACTED] |
Replacement text for redact action |
sse_mode |
string | filter |
SSE stream handling: filter or passthrough (see SSE Streaming) |
SSE Streaming¶
When an upstream returns Content-Type: text/event-stream (Server-Sent Events), Wardgate filters each SSE message individually as it streams through, rather than buffering the entire response. This allows real-time filtering of LLM streaming responses.
Behavior by mode:
| Mode | Description |
|---|---|
filter (default) |
Each SSE message is scanned. redact replaces sensitive data inline. block terminates the stream with an error event. |
passthrough |
SSE messages are forwarded without scanning (useful for trusted endpoints). |
How it works:
- SSE metadata lines (
id:,event:,retry:) pass through unchanged - Only
data:fields are scanned for sensitive content - The
[DONE]sentinel (used by OpenAI-compatible APIs) always passes through - On
block, anevent: erroris emitted and the stream terminates: - Error messages are generic and do not reveal which filter patterns matched (to prevent bypass crafting)
- SSE lines are limited to 1MB to protect against oversized payloads
Example:
endpoints:
llm-api:
upstream: https://api.openai.com/v1
auth:
type: bearer
credential_env: OPENAI_KEY
filter:
enabled: true
patterns: [api_keys]
action: redact
sse_mode: filter # Default - filter SSE streams per-message
How to Add an SSE Endpoint (OpenAI Example)¶
This walkthrough shows how to configure Wardgate as a streaming proxy for OpenAI's chat completions API, combining Dynamic Upstreams with SSE filtering. The same approach works for any OpenAI-compatible API (Anthropic, Groq, local models, etc.).
1. Wardgate Configuration¶
Create a config.yaml with a dynamic upstream endpoint:
server:
listen: ":4065"
agents:
- id: my-agent
key_env: WARDGATE_AGENT_KEY
endpoints:
openai:
description: "OpenAI API (dynamic upstream)"
allowed_upstreams:
- "https://api.openai.com"
timeout: "5m"
auth:
type: bearer
credential_env: OPENAI_API_KEY
filter:
enabled: true
patterns:
- api_keys
action: redact
sse_mode: filter
rules:
- match:
method: POST
action: allow
- match:
method: "*"
action: deny
message: "Only POST allowed"
Key points:
allowed_upstreamsinstead of a staticupstream-- the agent specifies the target via theX-Wardgate-Upstreamheader. This is useful when you want one endpoint definition to cover multiple path prefixes (e.g.,/v1,/v2).timeout: "5m"-- streaming responses can take a while; set a generous timeout.sse_mode: filter-- Wardgate scans each SSE chunk for sensitive data as it streams through, rather than buffering the full response.patterns: [api_keys]-- prevents the LLM from leaking API keys in its output (e.g., if it echoes a key from training data).
2. Environment Variables¶
Add the credentials to your .env file:
The agent authenticates to Wardgate with WARDGATE_AGENT_KEY. Wardgate injects OPENAI_API_KEY into the upstream request. The agent never sees the real OpenAI key.
3. Client Usage¶
The agent sends requests to Wardgate instead of directly to OpenAI, using the agent key for auth and X-Wardgate-Upstream to specify the target:
POST /openai/chat/completions
Host: localhost:4065
Authorization: Bearer my-agent-secret-key
X-Wardgate-Upstream: https://api.openai.com/v1
Content-Type: application/json
{"model": "gpt-4o-mini", "stream": true, "messages": [{"role": "user", "content": "Hello"}]}
Wardgate will:
1. Authenticate the agent via the Authorization header
2. Validate X-Wardgate-Upstream against allowed_upstreams
3. Evaluate the request against rules (POST is allowed)
4. Replace the agent's Bearer token with the real OPENAI_API_KEY
5. Forward the request to https://api.openai.com/v1/chat/completions
6. Stream the SSE response back, filtering each chunk for sensitive data
4. Example: Vercel AI SDK Client¶
Here is a TypeScript client using the Vercel AI SDK that streams a chat completion through Wardgate:
import { createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";
const openai = createOpenAI({
baseURL: "http://localhost:4065/openai",
apiKey: "my-agent-secret-key",
headers: {
"X-Wardgate-Upstream": "https://api.openai.com/v1",
},
});
const result = streamText({
model: openai("gpt-4o-mini"),
prompt: "Write a haiku about secure API gateways.",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
The AI SDK sends the apiKey as Authorization: Bearer ..., which Wardgate uses for agent authentication. The X-Wardgate-Upstream header tells Wardgate where to forward the request.
5. Using a Static Upstream Instead¶
If you only need to proxy to a single OpenAI base URL, you can use a static upstream instead of allowed_upstreams:
endpoints:
openai:
upstream: https://api.openai.com/v1
auth:
type: bearer
credential_env: OPENAI_API_KEY
filter:
enabled: true
patterns: [api_keys]
action: redact
sse_mode: filter
rules:
- match: { method: POST }
action: allow
- match: { method: "*" }
action: deny
With a static upstream, the agent does not need to send the X-Wardgate-Upstream header.
Built-in Patterns¶
| Pattern | Description | Examples |
|---|---|---|
otp_codes |
One-time passwords and verification codes | "Code: 123456", "Your OTP is 847291" |
verification_links |
Email verification and password reset URLs | https://example.com/verify/abc, ?token=xyz |
api_keys |
Common API key formats | sk-..., ghp_..., AKIA... |
ssn |
Social Security Numbers (US SSN) and Dutch BSN | SSN: 123-45-6789, BSN: 123456789 |
passport |
Passport numbers (US, NL, and other formats) | Passport: 123456789, paspoort: AB1234567 |
credit_cards |
Credit card numbers | 4111-1111-1111-1111 |
passwords |
Passwords in common formats | password: secret123 |
private_keys |
Private key headers | -----BEGIN PRIVATE KEY----- |
Actions¶
| Action | Description | Use Case |
|---|---|---|
block |
Return 403 error, don't return content | Default - highest security |
redact |
Replace sensitive data with placeholder | When agent needs partial content |
ask |
Require human approval | For SMTP: verify before sending |
log |
Log detection, allow passthrough | Monitoring only |
Custom Patterns¶
Define your own patterns using regex:
endpoints:
my-api:
filter:
custom_patterns:
- name: internal_id
pattern: "INTERNAL-[A-Z0-9]{8}"
description: "Internal tracking IDs"
- name: ssn
pattern: "\\d{3}-\\d{2}-\\d{4}"
description: "Social Security Numbers"
Per-Adapter Behavior¶
| Adapter | Filter Applies To | Default Action |
|---|---|---|
| HTTP | Response bodies (JSON, text, XML) and SSE streams (per-message) | block |
| IMAP | Message subject and body | block |
| SMTP | Outgoing email subject/body (triggers ask) |
ask |
| SSH | Command stdout/stderr | block |
| Conclave | Command stdout/stderr | block |
For SMTP, detecting sensitive data triggers the approval workflow rather than blocking, so humans can review before sending.
For conclaves, ask is not supported (the command has already executed). Use block, redact, or log instead. Per-command filter: on a command definition overrides the conclave default.
Example: Secure Email Access¶
endpoints:
# Personal email - block OTPs and verification links
imap-personal:
preset: imap
auth:
credential_env: IMAP_PERSONAL
filter:
enabled: true
patterns:
- otp_codes
- verification_links
action: block
# OTP inbox for automated account creation - allow OTPs
imap-otp:
preset: imap
auth:
credential_env: IMAP_OTP
filter:
enabled: false # Agent needs to read OTPs here
Security Recommendations¶
- Never commit .env files - Add to
.gitignore - Use strong agent keys - At least 32 random characters
- Separate credentials by endpoint - Don't reuse across services
- Restrict file permissions -
chmod 600 .env - Rotate credentials regularly - Especially after suspected exposure