Skip to content

Helix — Advanced Security Guide

Secure your AI agents, RAG pipelines, and integration workflows with enterprise-grade security. This guide covers authentication (JWT, OAuth2, API Key, BasicAuth), transport security (mTLS), secret management (Vault, AWS Secrets Manager), rate limiting, IP filtering, CORS, multi-tenancy, and production hardening.

All cryptographic operations use the Go standard library (crypto/*) and golang.org/x/crypto/bcrypt. No third-party JWT or OAuth libraries are required.

AI Agent Security

Beyond workflow-level auth, Helix provides AI-specific security features:

  • Human-in-the-loop approval — approval_policy: tagged requires human confirmation before executing sensitive tools (e.g., transfer_money, delete_account)
  • Guardrails — PII detection/redaction, content filtering, output validation, and token budget enforcement via pkg/agent/guardrails/
  • Session isolation — Per-session memory and state via Redis-backed session store
  • Input validation — tools: with mandatory schemas reject malformed input before execution

Table of Contents

  1. JWT Authentication (HS256 / RS256)
  2. Basic Authentication (bcrypt)
  3. OAuth2 Token Introspection (RFC 7662)
  4. TLS and mTLS Configuration
  5. API Key Authentication
  6. Rate Limiting (Token Bucket)
  7. IP Allowlisting
  8. Secret Resolution (Vault, AWS Secrets Manager, Env Vars)
  9. Security Interceptors -- Chaining Auth
  10. CORS Configuration
  11. Production Security Best Practices
  12. Complete Examples

1. JWT Authentication

The JWT processor (security.NewAuthProcessor with Type: "jwt") performs full signature verification and claims validation using only the Go standard library (crypto/hmac, crypto/rsa, crypto/sha256, crypto/x509). No third-party JWT libraries are used.

Source: pkg/security/security.go -- validateJWT function.

Configuration Types

// pkg/security/security.go

type AuthConfig struct {
    Type   string         // "apikey", "jwt", "basic", "oauth2"
    APIKey *APIKeyConfig
    JWT    *JWTConfig
    Basic  *BasicAuthConfig
    OAuth2 *OAuth2Config
}

type JWTConfig struct {
    Secret    string            // HMAC shared secret (for HS256)
    PublicKey string            // Path to RSA public key PEM file (for RS256)
    Issuer    string            // Expected "iss" claim
    Audience  string            // Expected "aud" claim
    Claims    map[string]string // Additional required claims (key -> expected value)
}

Supported Algorithms

Algorithm Key Material Verification Method
HS256 JWTConfig.Secret (shared secret) hmac.Equal (constant-time comparison)
RS256 JWTConfig.PublicKey (PEM file path) rsa.VerifyPKCS1v15 via crypto/x509

Any other alg value (including none) is rejected with an error.

Validation Steps

The validateJWT function performs the following checks in order:

  1. Bearer token extraction -- Reads the Authorization header, expects Bearer <token>.
  2. Structure validation -- Splits on . and verifies exactly 3 parts (header, payload, signature).
  3. Header parsing -- Base64url-decodes the header and extracts the alg field.
  4. Signature verification -- Depending on the algorithm:
  5. HS256: Computes HMAC-SHA256(secret, header.payload) and compares with hmac.Equal.
  6. RS256: Reads the PEM public key file, parses it via x509.ParsePKIXPublicKey, and verifies with rsa.VerifyPKCS1v15.
  7. Expiration check -- If the exp claim is present, rejects tokens where time.Now().Unix() > exp.
  8. Issuer validation -- If JWTConfig.Issuer is set, verifies the iss claim matches exactly.
  9. Audience validation -- If JWTConfig.Audience is set, verifies aud matches (supports both single-string and array formats per RFC 7519).
  10. Custom claims -- Iterates over JWTConfig.Claims and verifies each key-value pair exists in the payload.

Claims Propagation

After successful validation, all claims are propagated as exchange headers with the auth. prefix:

Header Source Claim
auth.subject sub
auth.issuer iss
auth.<key> Any other claim (e.g., auth.role, auth.exp)

This allows downstream processors to access identity information without re-parsing the token.

Go Examples

RS256 (asymmetric -- recommended for multi-service):

import "github.com/arvenza/helix/pkg/security"

authProc := security.NewAuthProcessor(&security.AuthConfig{
    Type: "jwt",
    JWT: &security.JWTConfig{
        PublicKey: "/etc/helix/keys/jwt-public.pem",
        Issuer:   "https://auth.example.com",
        Audience: "helix-api",
        Claims: map[string]string{
            "role": "admin",
        },
    },
})

// Use in a workflow pipeline:
builder.From("http://0.0.0.0:8080/api").
    Process(authProc).
    To("direct:business-logic")

HS256 (symmetric -- simpler for single-service):

authProc := security.NewAuthProcessor(&security.AuthConfig{
    Type: "jwt",
    JWT: &security.JWTConfig{
        Secret:   "${vault:secret/data/helix#jwt_secret}",
        Issuer:   "helix-internal",
        Audience: "my-service",
    },
})

Reading Authenticated Identity Downstream

func myProcessor(ctx context.Context, ex *exchange.Exchange) error {
    subject := ex.In().HeaderString("auth.subject")  // "sub" claim
    issuer  := ex.In().HeaderString("auth.issuer")    // "iss" claim
    role    := ex.In().HeaderString("auth.role")       // custom claim
    // Route based on identity...
    return nil
}

Key Security Properties

  • Constant-time comparison for HS256 signatures (hmac.Equal).
  • Algorithm enforcement: only HS256 and RS256 accepted; none is rejected.
  • Expiration is enforced when present; tokens without exp are allowed (configure a required exp claim in Claims to mandate it).
  • Audience validation supports both single-string and array formats per RFC 7519.

2. Basic Authentication

HTTP Basic authentication is verified against bcrypt password hashes using golang.org/x/crypto/bcrypt. Passwords are never stored in plaintext.

Source: pkg/security/security.go -- validateBasic function.

Configuration

type BasicAuthConfig struct {
    Users map[string]string // username -> bcrypt password hash
}

Validation Steps

  1. Reads the Authorization header, expects Basic <base64>.
  2. Base64-decodes the credentials and splits on : to extract username and password.
  3. Looks up the username in BasicAuthConfig.Users.
  4. Verifies the password against the stored bcrypt hash using bcrypt.CompareHashAndPassword.

Unknown users return a generic "unknown user" error. Wrong passwords return "invalid credentials". Both prevent user enumeration.

Generating bcrypt Hashes

import "golang.org/x/crypto/bcrypt"

// Generate a hash (do this offline or at init time, not per-request):
hash, err := bcrypt.GenerateFromPassword([]byte("s3cretP@ss"), bcrypt.DefaultCost)
// hash is something like "$2a$10$..."
# Using htpasswd (Apache utils):
htpasswd -nbBC 12 "" 'my-password' | cut -d: -f2

Go Example

authProc := security.NewAuthProcessor(&security.AuthConfig{
    Type: "basic",
    Basic: &security.BasicAuthConfig{
        Users: map[string]string{
            "alice": "$2a$12$LJ3m4O.../hashed...",
            "bob":   "$2a$12$xyz9.../hashed...",
        },
    },
})

YAML with Vault-Resolved Hashes

security:
  auth:
    type: basic
    basic:
      users:
        alice: "${vault:secret/data/auth#alice_bcrypt_hash}"
        bob: "${vault:secret/data/auth#bob_bcrypt_hash}"

Security Properties

  • bcrypt comparison is inherently constant-time.
  • Always use cost >= 10 (bcrypt.DefaultCost = 10) for production. Cost 12 is recommended for sensitive endpoints.

3. OAuth2 Token Introspection

The OAuth2 processor validates bearer tokens via RFC 7662 Token Introspection. It sends the token to the configured introspection endpoint and checks the response for active status and required scopes.

Source: pkg/security/security.go -- validateOAuth2 function.

Configuration

type OAuth2Config struct {
    TokenURL     string   // RFC 7662 introspection endpoint
    ClientID     string   // Client credentials for introspection
    ClientSecret string
    Scopes       []string // Required scopes (AND semantics)
}

Introspection Flow

  1. Extracts the Bearer token from the Authorization header.
  2. Sends a POST request to TokenURL with:
  3. Body: token=<bearer_token> (form-encoded)
  4. Content-Type: application/x-www-form-urlencoded
  5. Accept: application/json
  6. HTTP Basic auth: ClientID:ClientSecret
  7. 10-second timeout (hardened against slow introspection servers)
  8. Parses the introspection response JSON.
  9. Rejects if active is false.
  10. If Scopes is configured, verifies every required scope appears in the space-delimited scope field (AND semantics, not OR).

Claims Propagation

Header Source Field
auth.subject sub
auth.clientId client_id
auth.scope scope (space-separated)

Go Example

authProc := security.NewAuthProcessor(&security.AuthConfig{
    Type: "oauth2",
    OAuth2: &security.OAuth2Config{
        TokenURL:     "https://auth.example.com/oauth2/introspect",
        ClientID:     "${env:OAUTH_CLIENT_ID}",
        ClientSecret: "${vault:secret/data/helix#oauth_client_secret}",
        Scopes:       []string{"read:data", "write:data"},
    },
})

Security Properties

  • Introspection requests have a 10-second timeout to prevent hanging.
  • Client credentials are sent via HTTP Basic auth on the introspection request (not as form parameters), per RFC 7662 recommendation.
  • Scope validation requires all configured scopes (AND semantics).

4. TLS and mTLS Configuration

Helix provides two TLS configuration types, both using only the standard library crypto/tls:

  • TLSConfig (pkg/security/security.go) -- high-level, string-based configuration for YAML/config files.
  • NetworkTLSConfig (pkg/security/tls.go) -- programmatic, used by component implementations with URI parameter parsing.

4.1 High-Level TLS Configuration (TLSConfig)

// pkg/security/security.go

type TLSConfig struct {
    Enabled    bool
    CertFile   string // Server/client certificate
    KeyFile    string // Private key
    CAFile     string // CA certificate (for peer verification)
    SkipVerify bool
    MinVersion string // "1.2" (default) or "1.3"
    ClientAuth string // "none" (default), "request", or "require"
}

The Build() method converts this to *tls.Config:

  • Defaults to TLS 1.2 minimum.
  • When CAFile is set, the CA is loaded into both RootCAs and ClientCAs.
  • ClientAuth maps to:
  • "none" -> tls.NoClientCert
  • "request" -> tls.RequestClientCert
  • "require" -> tls.RequireAndVerifyClientCert
tlsCfg := &security.TLSConfig{
    Enabled:    true,
    CertFile:   "/etc/helix/tls/server.crt",
    KeyFile:    "/etc/helix/tls/server.key",
    CAFile:     "/etc/helix/tls/ca.crt",
    MinVersion: "1.3",
    ClientAuth: "require",
}

goTLS, err := tlsCfg.Build()

4.2 Network Component TLS (NetworkTLSConfig)

// pkg/security/tls.go

type NetworkTLSConfig struct {
    Enabled            bool
    CertFile           string
    KeyFile            string
    CAFile             string
    InsecureSkipVerify bool
    MinVersion         uint16 // tls.VersionTLS12 or tls.VersionTLS13
    ServerName         string // SNI hostname
}

Three builder functions are provided:

Function Purpose mTLS Behavior
NewTLSConfig(cfg) Base TLS config No auto-mTLS
NewClientTLSConfig(cfg) Client-side (removes ClientCAs) N/A
NewServerTLSConfig(cfg) Server-side Auto-enables RequireAndVerifyClientCert if CAFile is set

4.3 URI Parameter Parsing

Components extract TLS config from endpoint URIs using ParseTLSParams:

kafka://broker:9093?tls=true&tlsCert=/certs/client.pem&tlsKey=/certs/key.pem&tlsCA=/certs/ca.pem&tlsMinVersion=1.3

Recognized URI parameters:

Parameter Description Default
tls Enable TLS (true, 1, yes, on) false
tlsCert Client/server certificate file --
tlsKey Private key file --
tlsCA CA certificate file --
tlsInsecure Skip certificate verification false
tlsMinVersion "1.2" or "1.3" "1.2"
tlsServerName SNI server name override --

4.4 mTLS (Mutual TLS)

For mutual TLS, both sides present certificates and verify each other:

Server side -- requires client certificates:

serverTLS, err := security.NewServerTLSConfig(security.NetworkTLSConfig{
    Enabled:    true,
    CertFile:   "/certs/server.crt",
    KeyFile:    "/certs/server.key",
    CAFile:     "/certs/client-ca.crt",  // Triggers RequireAndVerifyClientCert
    MinVersion: tls.VersionTLS13,
})

Client side -- presents client certificate:

clientTLS, err := security.NewClientTLSConfig(security.NetworkTLSConfig{
    Enabled:    true,
    CertFile:   "/certs/client.crt",
    KeyFile:    "/certs/client.key",
    CAFile:     "/certs/server-ca.crt",
    ServerName: "api.internal.example.com",
})

4.5 TLS for Vault Connections

The VaultClient (pkg/config/vault_client.go) supports TLS options:

vc := config.NewVaultClient(
    "https://vault.example.com:8200",
    "s.mytoken",
    config.WithTLSSkipVerify(false),   // default: verify
    config.WithTimeout(30*time.Second),
    config.WithNamespace("prod"),
)

Minimum TLS Version

Helix enforces TLS 1.2 as the minimum by default. For environments that support it, TLS 1.3 is recommended and eliminates the need to configure cipher suites (Go's TLS 1.3 implementation negotiates automatically).


5. API Key Authentication

API key validation uses crypto/subtle.ConstantTimeCompare to prevent timing attacks that could reveal partial key matches.

Source: pkg/security/security.go -- validateAPIKey function.

Configuration

type APIKeyConfig struct {
    Header string   // Header name; defaults to "X-API-Key"
    Keys   []string // List of allowed API keys
}

Validation

  1. Reads the configured header (default X-API-Key) from the exchange.
  2. Compares against each allowed key using subtle.ConstantTimeCompare.
  3. Returns "unauthorized: invalid API key" if no key matches.

Go Example

authProc := security.NewAuthProcessor(&security.AuthConfig{
    Type: "apikey",
    APIKey: &security.APIKeyConfig{
        Header: "X-API-Key",
        Keys: []string{
            "${vault:secret/data/helix#api_key_1}",
            "${vault:secret/data/helix#api_key_2}",
        },
    },
})

Custom Header

// Use a service-specific header name:
cfg := &security.APIKeyConfig{
    Header: "X-Service-Token",
    Keys:   []string{"${env:SERVICE_API_KEY}"},
}

Key Rotation

Multiple keys are supported simultaneously. To rotate:

  1. Add the new key to the Keys list alongside the old key.
  2. Update clients to use the new key.
  3. Remove the old key from the list.

6. Rate Limiting

The RateLimiter (pkg/security/security.go) implements a token-bucket algorithm with a background refill goroutine.

How It Works

  1. A channel of capacity rate is pre-filled with tokens on creation.
  2. A background goroutine adds one token every period / rate interval.
  3. Allow() performs a non-blocking receive -- returns true if a token was available, false otherwise.
  4. Wait(ctx) blocks until a token is available or the context is cancelled.
  5. Stop() shuts down the refill goroutine (safe to call multiple times via sync.Once).

Configuration

// 100 requests per second
rl := security.NewRateLimiter(100, time.Second)
defer rl.Stop()

Using as a Processor

rl := security.NewRateLimiter(50, time.Second)

builder.From("http://0.0.0.0:8080/api").
    Process(rl.Processor()).   // Rate limit first
    Process(authProc).          // Then authenticate
    To("direct:handler")

When no token is available within the context deadline, the exchange fails with a "rate limited: context deadline exceeded" error.

Programmatic Usage

// Non-blocking check
if rl.Allow() {
    // proceed
} else {
    // reject with 429
}

// Blocking wait (respects context cancellation)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := rl.Wait(ctx); err != nil {
    // rate limited or context cancelled
}

Per-Workflow Rate Limiting

Create separate rate limiters for different workflows or tiers:

publicRL  := security.NewRateLimiter(50, time.Second)   // 50 req/s
premiumRL := security.NewRateLimiter(500, time.Second)   // 500 req/s

builder.From("http://0.0.0.0:8080/public").
    Process(publicRL.Processor()).
    To("direct:handler")

builder.From("http://0.0.0.0:8080/premium").
    Process(premiumRL.Processor()).
    To("direct:handler")

Lifecycle

Always call Stop() during shutdown to clean up the background goroutine. The Stop() method is idempotent.


7. IP Allowlisting

The IPAllowlist (pkg/security/security.go) restricts access to a set of approved IP addresses.

IP Extraction Order

The processor extracts the client IP in this order:

  1. X-Forwarded-For header -- Uses the first comma-separated entry (the original client IP).
  2. RemoteAddr header -- Fallback when X-Forwarded-For is absent.
  3. Port stripping -- Removes port numbers (e.g., 192.168.1.1:4321 becomes 192.168.1.1).

X-Forwarded-For always takes precedence over RemoteAddr.

Go Example

allowlist := security.NewIPAllowlist([]string{
    "10.0.0.0",
    "10.0.0.1",
    "192.168.1.100",
    "::1",  // IPv6 loopback
})

builder.From("http://0.0.0.0:9090/admin").
    Process(allowlist.Processor()).   // IP check first
    Process(authProc).                // Then authenticate
    To("direct:admin-handler")

Important: Proxy Considerations

When running behind a reverse proxy or load balancer:

  • Ensure the proxy sets X-Forwarded-For correctly.
  • Ensure only trusted proxies can modify this header (untrusted clients can spoof it).
  • Consider using CIDR ranges at the network layer (firewall rules) in addition to application-level IP allowlisting.

8. Secret Resolution

Helix provides a multi-provider secret resolution system that replaces ${provider:reference} placeholders in configuration values at startup. All implementations use only the Go standard library (no external SDKs).

8.1 Providers Overview

Provider Prefix Reference Format Implementation
HashiCorp Vault vault: path#field HTTP REST API
AWS Secrets Manager aws-sm: name or name#field REST API + SigV4
Environment Variables env: VARIABLE_NAME os.Getenv
File file: /path/to/secret os.ReadFile

8.2 Composite Secret Resolver (pkg/config/vault.go)

The CompositeSecretResolver delegates to provider-specific resolvers based on a prefix:

resolver := config.NewDefaultCompositeResolver()
// Pre-registered: "vault:", "aws-sm:", "env:"

err := config.ResolveSecrets(ctx, resolver, configMap)

Example config with placeholders:

helix:
  components:
    kafka:
      brokers: "kafka.example.com:9093"
      password: "${vault:secret/data/kafka#password}"
    redis:
      password: "${aws-sm:redis-credentials#password}"
    http:
      api-key: "${env:EXTERNAL_API_KEY}"

The ResolveSecrets function walks arbitrary map[string]any structures recursively (maps, slices, strings) and replaces all ${provider:ref} patterns.

8.3 Vault Resolver

The VaultResolver supports:

  • KV v1 and KV v2 secret engines (auto-detected via mount metadata at /v1/sys/mounts/).
  • Token authentication (from config or VAULT_TOKEN env var).
  • AppRole authentication -- Automatic login via POST /v1/auth/approle/login when Token is empty but AppRoleID/AppSecretID are set.
  • Namespace support for Vault Enterprise (sets X-Vault-Namespace header).
  • Caching with configurable TTL (default 5 minutes with NewDefaultCompositeResolver).
vaultResolver := config.NewVaultResolver(
    "https://vault.example.com:8200",  // or VAULT_ADDR env var
    "",                                  // or VAULT_TOKEN env var
    "prod",                             // namespace (optional)
    5*time.Minute,                      // cache TTL
)

// With AppRole authentication:
vaultResolver.AppRoleID = "role-id-here"
vaultResolver.AppSecretID = "secret-id-here"

// Reference format: path#field
secret, err := vaultResolver.Resolve(ctx, "secret/data/myapp#database_password")

8.4 AWS Secrets Manager Resolver

Uses AWS SigV4 request signing implemented in pure Go -- no AWS SDK dependency. The signing implementation includes canonical request construction, HMAC-SHA256 signing key derivation, and authorization header formatting.

awsResolver := config.NewAWSSecretsResolver(
    "",  // region (falls back to AWS_REGION -> AWS_DEFAULT_REGION -> us-east-1)
    "",  // access key (falls back to AWS_ACCESS_KEY_ID)
    "",  // secret key (falls back to AWS_SECRET_ACCESS_KEY)
    5*time.Minute,
)

// Full secret string:
secret, err := awsResolver.Resolve(ctx, "my-database-creds")

// Specific JSON field:
password, err := awsResolver.Resolve(ctx, "my-database-creds#password")

8.5 Security-Level Secret Resolution (pkg/security)

The security.SecretResolver is a simpler runtime resolver with a prefix:key syntax:

r := security.NewSecretResolver()

// Built-in resolvers:
val, _ := r.Resolve("env:DATABASE_URL")          // os.Getenv
val, _ := r.Resolve("file:/run/secrets/api_key")  // os.ReadFile + TrimSpace

// Register a custom resolver:
r.RegisterResolver("vault", func(key string) (string, error) {
    secrets, err := vaultClient.ReadSecret(ctx, key)
    if err != nil {
        return "", err
    }
    return secrets["value"], nil
})

8.6 VaultClient (Standalone)

For direct Vault access, config.VaultClient provides a lightweight HTTP client with functional options:

vc := config.NewVaultClient(
    "https://vault.example.com:8200",
    os.Getenv("VAULT_TOKEN"),
    config.WithNamespace("production"),
    config.WithTimeout(10*time.Second),
    config.WithTLSSkipVerify(false),
)

// Read a single secret (auto-detects KV v1/v2):
secrets, err := vc.ReadSecret(ctx, "secret/data/myapp")
dbPass := secrets["db_password"]

// Resolve all ${vault:path/key} placeholders in a Config struct:
cfg, _ := config.Load("helix.yaml")
err = vc.ResolveSecrets(ctx, cfg)

The ResolveSecrets method uses reflection to walk all exported string fields in the Config struct (including nested structs, pointers, and maps) and replaces ${vault:path/key} patterns where the last / separates the Vault path from the field name.

8.7 Caching Behavior

Both VaultResolver and AWSSecretsResolver use in-memory caches protected by sync.RWMutex:

  • Cache key: the full reference string (e.g., path#field).
  • Cache entry: resolved value + expiration time.
  • TTL of 0 disables caching entirely.
  • Cache is checked before making any network request.
  • No cache eviction beyond TTL expiry (entries are lazy-expired on access).

9. Security Interceptors -- Chaining Auth

Helix uses the engine.Interceptor interface for cross-cutting security concerns that apply to every exchange on every workflow (or globally).

Interceptor Interface

// pkg/engine/interfaces.go

type Interceptor interface {
    Before(ctx context.Context, exchange *exchange.Exchange) error
    After(ctx context.Context, exchange *exchange.Exchange) error
}

Before runs before the processor pipeline; returning an error from Before short-circuits the pipeline and the exchange fails. After runs after processing completes (even on error).

9.1 Tenant Interceptor

The TenantInterceptor (pkg/engine/tenant.go) extracts a tenant identifier from message headers and enforces an allowlist:

type TenantInterceptorConfig struct {
    HeaderName     string   // default "X-Tenant-ID"
    Required       bool     // reject exchanges without a tenant header
    AllowedTenants []string // empty = allow all
}

tenantInterceptor := engine.NewTenantInterceptor(engine.TenantInterceptorConfig{
    HeaderName:     "X-Tenant-ID",
    Required:       true,
    AllowedTenants: []string{"acme", "globex", "initech"},
})

Behavior:

  1. Reads the configured header from the exchange message.
  2. If Required is true and the header is missing, rejects the exchange.
  3. If AllowedTenants is non-empty, checks the tenant against the allowlist.
  4. On success, sets the helix.tenant property on the exchange for downstream use (routing decisions, metrics labels, database scoping).

YAML configuration:

helix:
  tenant:
    enabled: true
    header_name: "X-Tenant-ID"
    required: true
    allowed_tenants:
      - acme
      - globex
      - initech

9.2 Audit Interceptor

The AuditInterceptor (pkg/engine/audit.go) records exchange lifecycle events for compliance and debugging:

store := engine.NewInMemoryAuditStore(10000)   // ring buffer
// or: store := engine.NewSlogAuditStore(logger, 10000)  // structured log output

audit := engine.NewAuditInterceptor(store, 256)  // 256 bytes body preview

Each AuditEntry records:

Field Description
Timestamp When the event was recorded
ExchangeID Unique exchange identifier
WorkflowID Workflow that processed the exchange
FromURI Source endpoint URI
Operation "completed" or "failed"
Duration Processing time
Error Error message (if failed)
Headers Message headers snapshot
BodyPreview First N bytes of the message body

YAML configuration:

helix:
  audit:
    enabled: true
    store: "log"       # "memory" (ring buffer) or "log" (slog)
    max_entries: 10000
    body_preview: 256

9.3 Chaining Multiple Security Interceptors

Interceptors run in registration order. Register them on the HelixContext:

hc := engine.NewContext("my-app")

// Order matters: each interceptor runs in registration order.
// 1. Audit everything first (captures timing for all subsequent interceptors)
hc.AddInterceptor(auditInterceptor)

// 2. Tenant isolation
hc.AddInterceptor(tenantInterceptor)

// 3. Distributed tracing
hc.AddInterceptor(tracingInterceptor)

9.4 Interceptors vs Processors: When to Use Which

Mechanism Scope Use Case
Interceptor (hc.AddInterceptor) Global (all workflows) Audit logging, tenant isolation, tracing
Auth Processor (security.NewAuthProcessor) Per-workflow Different auth per endpoint
Rate Limiter (rl.Processor()) Per-workflow Workflow-specific rate limits
IP Allowlist (allowlist.Processor()) Per-workflow Endpoint-level IP restrictions

9.5 Combined Security Pipeline Example

hc := engine.NewContext("my-app")

// Global interceptors (all workflows):
hc.AddInterceptor(auditInterceptor)
hc.AddInterceptor(tenantInterceptor)

// Per-workflow: public API (rate limited + JWT)
publicRL := security.NewRateLimiter(100, time.Second)
jwtAuth := security.NewAuthProcessor(&security.AuthConfig{
    Type: "jwt",
    JWT:  jwtCfg,
})

builder.From("http://0.0.0.0:8080/api").
    Process(publicRL.Processor()).
    Process(jwtAuth).
    To("direct:api-handler")

// Per-workflow: admin API (IP restricted + API key)
adminIPs := security.NewIPAllowlist([]string{"10.0.0.1", "10.0.0.2"})
adminAuth := security.NewAuthProcessor(&security.AuthConfig{
    Type:   "apikey",
    APIKey: &security.APIKeyConfig{Keys: []string{"${vault:secret/data/helix#admin_key}"}},
})

builder.From("http://0.0.0.0:8080/admin").
    Process(adminIPs.Processor()).
    Process(adminAuth).
    To("direct:admin-handler")

// Per-workflow: webhook (API key only, no rate limit)
webhookAuth := security.NewAuthProcessor(&security.AuthConfig{
    Type:   "apikey",
    APIKey: &security.APIKeyConfig{
        Header: "X-Webhook-Secret",
        Keys:   []string{"${env:WEBHOOK_SECRET}"},
    },
})

builder.From("http://0.0.0.0:8080/webhook").
    Process(webhookAuth).
    To("direct:webhook-handler")

10. CORS Configuration

CORS is configured on REST definitions via the DSL builder or YAML. The CORSDefinition type lives in pkg/workflow/rest_definition.go.

Data Model

// pkg/workflow/rest_definition.go

type CORSDefinition struct {
    AllowedOrigins []string
    AllowedMethods []string
    AllowedHeaders []string
    MaxAge         int  // seconds
}

type SecurityDefinition struct {
    Type   string  // apiKey, bearer, basic, oauth2
    Name   string  // header/parameter name
    In     string  // header, query
}

Go Fluent DSL

import "github.com/arvenza/helix/pkg/dsl"

restDef := dsl.Rest("/api/v1").
    EnableCORS(
        []string{"https://app.example.com", "https://admin.example.com"},
        []string{"GET", "POST", "PUT", "DELETE"},
        []string{"Authorization", "Content-Type", "X-Tenant-ID"},
        3600,  // Max-Age in seconds
    ).
    Get("/users").To("direct:list-users").
    Post("/users").To("direct:create-user")

YAML Configuration

rest:
  - path: "/api/v1"
    cors:
      allowed-origins:
        - "https://app.example.com"
        - "https://admin.example.com"
      allowed-methods:
        - GET
        - POST
        - PUT
        - DELETE
      allowed-headers:
        - Authorization
        - Content-Type
        - X-Tenant-ID
      max-age: 3600
    security:
      - type: bearer
        name: Authorization
        in: header
    verbs:
      - method: GET
        path: "/users"
        target: "direct://list-users"
        description: "List all users"
      - method: POST
        path: "/users"
        target: "direct://create-user"
        description: "Create a new user"

# Handler workflows for the REST endpoints
workflows:
  - id: list-users
    trigger:
      uri: "direct://list-users"
    steps:
      - transform:
          constant: '[]'
      - set-header:
          name: Content-Type
          constant: "application/json"

  - id: create-user
    trigger:
      uri: "direct://create-user"
    steps:
      - set-header:
          name: Content-Type
          constant: "application/json"
      - transform:
          constant: '{"status":"created"}'

AG-UI and A2UI Automatic CORS

The AG-UI (pkg/component/agui/) and A2UI (pkg/component/a2ui/) server components set CORS headers automatically for frontend access. No additional configuration is needed for those endpoints.


11. Production Security Best Practices

11.1 Secret Management

  • Never hardcode secrets in Go source or YAML. Use ${vault:...}, ${aws-sm:...}, or ${env:...} placeholders exclusively.
  • Use AppRole authentication for Vault instead of long-lived tokens. AppRole supports secret ID rotation and TTL-limited tokens.
  • Enable caching (CacheTTL) to reduce Vault/AWS API calls while maintaining freshness. 5 minutes is a good default.
  • Use file-based secrets for Kubernetes: mount Secrets as files and use file:/var/run/secrets/helix/db-password.
  • Rotate secrets by updating them in Vault/AWS SM. Reduce cache TTL for faster propagation during rotation windows.

11.2 Transport Security

  • Always enable TLS in production. Use MinVersion: "1.3" when all clients support it.
  • Use mTLS for service-to-service communication. NewServerTLSConfig automatically sets RequireAndVerifyClientCert when a CA file is provided.
  • Never set SkipVerify: true or tlsInsecure=true in production.
  • Rotate TLS certificates before expiry. Go reads certificate files at config build time, so restart or reload is required after rotation.

11.3 Authentication

  • Prefer RS256 over HS256 for JWT when multiple services validate tokens. HS256 requires sharing the secret with every verifier, increasing the attack surface.
  • Always validate exp, iss, and aud claims. To mandate exp, add it to the Claims map.
  • Use bcrypt cost >= 10 for Basic Auth. Cost 12 is recommended for internet-facing services.
  • Set required scopes for OAuth2 introspection. Never accept tokens with no scope validation.
  • API key comparison is constant-time -- no additional measures needed against timing attacks.

11.4 Rate Limiting

  • Apply rate limiting before authentication to prevent auth-related DoS. bcrypt hashing and OAuth2 introspection are expensive operations.
  • Use per-workflow rate limiters with different thresholds for public vs. authenticated vs. admin endpoints.
  • Always call rl.Stop() during shutdown to clean up background goroutines.

11.5 IP Filtering

  • Use IP allowlisting for admin endpoints (management API, health checks with sensitive data, audit endpoints).
  • Trust X-Forwarded-For only behind known proxies. Untrusted clients can spoof this header.
  • Combine IP allowlisting with authentication for defense-in-depth.

11.6 Multi-Tenancy

  • Set Required: true to ensure every exchange has a tenant identifier.
  • Use AllowedTenants to enforce a strict allowlist.
  • The helix.tenant exchange property can be used in downstream routing, metrics labels, and database query scoping.

11.7 Audit and Compliance

  • Enable the audit interceptor in production for compliance logging.
  • Limit body_preview to avoid logging sensitive data (passwords, PII, credit card numbers). 128-512 bytes is typically sufficient for debugging.
  • Use the "log" store backend to forward audit logs to a centralized SIEM.

11.8 Graceful Shutdown

Configure drain_timeout to allow in-flight requests to complete before the process exits:

helix:
  shutdown:
    drain_timeout: "30s"

11.9 CORS

  • Never use * for AllowedOrigins in production. Enumerate specific, trusted origins.
  • Limit AllowedMethods to the HTTP methods your API actually supports.
  • Set a reasonable MaxAge (e.g., 3600 seconds) to reduce preflight requests.

11.10 Tool-Level Authentication

When agents call external services via type: a2a, type: mcp, or type: connector tools, you can attach an auth: block to each tool. This secures outbound calls without affecting the agent's own inbound authentication.

Four auth types are supported. All ${env:...} placeholders are resolved at load time via Helix's secret resolution system (environment variables, Vault, AWS Secrets Manager).

Bearer Token

tools:
  - name: remote-agent
    type: a2a
    a2a_server:
      url: "http://agent.internal:9000"
    auth:
      type: bearer
      token: "${env:A2A_TOKEN}"

The token is sent as Authorization: Bearer <token> on every request to the remote server.

API Key

tools:
  - name: vendor-tools
    type: mcp
    mcp_server:
      url: "https://api.vendor.com/mcp"
    auth:
      type: api-key
      header: "X-API-Key"              # custom header name
      key: "${env:VENDOR_API_KEY}"

The key is sent in the specified header on every request. Defaults to X-API-Key if header is omitted.

OAuth2 Client Credentials

tools:
  - name: corp-agent
    type: a2a
    a2a_server:
      url: "http://agent.corp.internal:9000"
    auth:
      type: oauth2
      token_url: "https://auth.corp.internal/token"
      client_id: "${env:OAUTH_CLIENT_ID}"
      client_secret: "${env:OAUTH_CLIENT_SECRET}"
      scopes: ["agent:invoke", "tools:read"]

Helix performs a client credentials grant to token_url before calling the remote service, caches the access token, and refreshes it on expiry.

Mutual TLS (mTLS)

tools:
  - name: secure-tools
    type: mcp
    mcp_server:
      url: "https://mcp.internal:3100"
    auth:
      type: mtls
      cert_file: "/certs/client.crt"
      key_file: "/certs/client.key"
      ca_file: "/certs/ca.crt"

The client presents its certificate during the TLS handshake. The ca_file is used to verify the server's certificate.

Combining Tool Auth with Workflow Auth

Tool-level auth is independent of workflow-level auth. An agent endpoint can require JWT for inbound requests while its tools use OAuth2 for outbound calls:

agents:
  - id: secure-agent
    type: react
    trigger: http-server://0.0.0.0:8080/api/agent
    auth:                                    # inbound: callers must present JWT
      type: jwt
      jwt:
        secret: "${env:JWT_SECRET}"
    model:
      provider: openai
      model: gpt-4o
    tools:
      - name: remote-service
        type: a2a
        a2a_server:
          url: "http://service.internal:9000"
        auth:                                # outbound: agent uses OAuth2
          type: oauth2
          token_url: "https://auth.internal/token"
          client_id: "${env:CLIENT_ID}"
          client_secret: "${env:CLIENT_SECRET}"

11.11 Agent-Level Security

When running AI agents in production, apply additional safeguards beyond transport and route security:

  • Approval policies: Require human-in-the-loop confirmation before agents execute sensitive tools (e.g., database writes, payment calls). Configure per-tool approval via the agent's tools[].approval: required setting.
  • PII guardrails: Use a processor or interceptor to scan agent inputs and outputs for personally identifiable information before it reaches the LLM or leaves the system. Combine with audit.body_preview: 0 to avoid logging PII.
  • Token budgets: Set max_tokens and max_turns on agent definitions to cap LLM spend per request. Monitor helix_agent_tokens_total to detect runaway loops.

11.12 Security Processor Ordering

The recommended order for processors in a workflow pipeline is:

  1. Rate Limiting -- Protect against DoS before any expensive operations.
  2. IP Allowlisting -- Reject unauthorized IPs early.
  3. Authentication (JWT/Basic/OAuth2/API Key) -- Validate identity.
  4. Authorization (custom processor) -- Check roles/permissions.
  5. Business Logic -- Process the request.

12. Complete Examples

12.1 Full Secured REST API

package main

import (
    "context"
    "time"

    "github.com/arvenza/helix/pkg/config"
    "github.com/arvenza/helix/pkg/dsl"
    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/security"
)

func main() {
    ctx := context.Background()

    // --- Secret Resolution at Startup ---
    resolver := config.NewDefaultCompositeResolver()
    configMap := map[string]any{
        "jwt_secret":     "${vault:secret/data/auth#jwt_secret}",
        "admin_api_key":  "${aws-sm:helix-admin#api_key}",
        "webhook_secret": "${env:WEBHOOK_SECRET}",
    }
    if err := config.ResolveSecrets(ctx, resolver, configMap); err != nil {
        panic(err)
    }

    // --- Global Interceptors ---
    hc := engine.NewContext("my-app")

    // Tenant isolation
    hc.AddInterceptor(engine.NewTenantInterceptor(engine.TenantInterceptorConfig{
        HeaderName:     "X-Tenant-ID",
        Required:       true,
        AllowedTenants: []string{"acme", "globex"},
    }))

    // Audit logging
    auditStore := engine.NewInMemoryAuditStore(50000)
    hc.AddInterceptor(engine.NewAuditInterceptor(auditStore, 256))

    // --- Per-Workflow Security ---
    builder := dsl.NewRoute()

    // Public API: rate limited + JWT
    publicRL := security.NewRateLimiter(100, time.Second)
    defer publicRL.Stop()

    jwtAuth := security.NewAuthProcessor(&security.AuthConfig{
        Type: "jwt",
        JWT: &security.JWTConfig{
            PublicKey: "/etc/helix/keys/jwt-public.pem",
            Issuer:    "https://auth.example.com",
            Audience:  "api.example.com",
        },
    })

    builder.From("http://0.0.0.0:8080/api/v1").
        Process(publicRL.Processor()).
        Process(jwtAuth).
        To("direct:api-handler")

    // Admin API: IP restricted + API key
    adminIPs := security.NewIPAllowlist([]string{"10.0.0.1", "10.0.0.2"})
    adminAuth := security.NewAuthProcessor(&security.AuthConfig{
        Type: "apikey",
        APIKey: &security.APIKeyConfig{
            Header: "X-Admin-Key",
            Keys:   []string{configMap["admin_api_key"].(string)},
        },
    })

    builder.From("http://0.0.0.0:8080/admin").
        Process(adminIPs.Processor()).
        Process(adminAuth).
        To("direct:admin-handler")

    // Webhook: API key via custom header
    webhookAuth := security.NewAuthProcessor(&security.AuthConfig{
        Type: "apikey",
        APIKey: &security.APIKeyConfig{
            Header: "X-Webhook-Secret",
            Keys:   []string{configMap["webhook_secret"].(string)},
        },
    })

    builder.From("http://0.0.0.0:8080/webhook").
        Process(webhookAuth).
        To("direct:webhook-handler")

    // Start engine...
    hc.Start(ctx)
}

12.2 mTLS Between Services

package main

import (
    "crypto/tls"

    "github.com/arvenza/helix/pkg/security"
)

func setupMTLS() (*tls.Config, *tls.Config) {
    // Server: requires client certificates signed by the CA
    serverTLS, err := security.NewServerTLSConfig(security.NetworkTLSConfig{
        Enabled:    true,
        CertFile:   "/certs/server.crt",
        KeyFile:    "/certs/server.key",
        CAFile:     "/certs/ca.crt",  // Triggers RequireAndVerifyClientCert
        MinVersion: tls.VersionTLS13,
    })
    if err != nil {
        panic(err)
    }

    // Client: presents client certificate for mTLS
    clientTLS, err := security.NewClientTLSConfig(security.NetworkTLSConfig{
        Enabled:    true,
        CertFile:   "/certs/client.crt",
        KeyFile:    "/certs/client.key",
        CAFile:     "/certs/ca.crt",
        ServerName: "api.internal.example.com",
    })
    if err != nil {
        panic(err)
    }

    return serverTLS, clientTLS
}

12.3 OAuth2-Protected Agent Endpoint

agentAuth := security.NewAuthProcessor(&security.AuthConfig{
    Type: "oauth2",
    OAuth2: &security.OAuth2Config{
        TokenURL:     "https://auth.example.com/oauth2/introspect",
        ClientID:     "${env:OAUTH_CLIENT_ID}",
        ClientSecret: "${vault:secret/data/helix#oauth_secret}",
        Scopes:       []string{"agent:invoke", "agent:read"},
    },
})

builder.From("http://0.0.0.0:8080/agent/invoke").
    Process(agentAuth).
    To("agent:my-agent")

12.4 Vault with AppRole Authentication

vault := config.NewVaultResolver(
    "https://vault.example.com:8200",
    "",  // No static token
    "production",
    5*time.Minute,
)
vault.AppRoleID = os.Getenv("VAULT_ROLE_ID")
vault.AppSecretID = os.Getenv("VAULT_SECRET_ID")

// The resolver will automatically perform AppRole login
// when Resolve() is first called.
secret, err := vault.Resolve(ctx, "secret/data/myapp#database_password")

12.5 Full Production YAML

helix:
  name: "production-service"

  shutdown:
    drain_timeout: "30s"

  logging:
    level: "info"
    format: "json"
    components:
      security: "debug"

  tenant:
    enabled: true
    header_name: "X-Tenant-ID"
    required: true
    allowed_tenants:
      - acme
      - globex

  audit:
    enabled: true
    store: "log"
    max_entries: 50000
    body_preview: 256

  management:
    enabled: true
    port: 9090

  health:
    enabled: true
    port: 8081

  components:
    kafka:
      brokers: "${env:KAFKA_BROKERS}"
      password: "${vault:secret/data/kafka#password}"
    redis:
      addr: "${env:REDIS_ADDR}"
      password: "${aws-sm:redis-credentials#password}"
    postgres:
      dsn: "postgres://${vault:secret/data/pg#user}:${vault:secret/data/pg#pass}@${env:PG_HOST}:5432/myapp?sslmode=verify-full"

workflows:
  - from:
      uri: "http://0.0.0.0:8443/api?tls=true&tlsCert=/certs/server.crt&tlsKey=/certs/server.key&tlsCA=/certs/ca.crt&tlsMinVersion=1.3"
    steps:
      - target: "direct:api-handler"

rest:
  - path: "/api/v1"
    cors:
      allowed-origins:
        - "https://app.example.com"
      allowed-methods:
        - GET
        - POST
        - PUT
        - DELETE
      allowed-headers:
        - Authorization
        - Content-Type
        - X-Tenant-ID
      max-age: 3600
    security:
      - type: bearer
        name: Authorization
        in: header
    verbs:
      - method: GET
        path: "/resources"
        target: "direct://list-resources"
        description: "List resources"
      - method: POST
        path: "/resources"
        target: "direct://create-resource"
        description: "Create a resource"

12.6 Custom Role-Based Access Control Processor

package main

import (
    "context"
    "fmt"

    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/exchange"
    "github.com/arvenza/helix/pkg/security"
)

// RequireRole creates a processor that checks the auth.role header
// (set by JWT validation) against a required role.
func RequireRole(role string) engine.Processor {
    return engine.ProcessorFunc(func(ctx context.Context, ex *exchange.Exchange) error {
        actualRole := ex.In().HeaderString("auth.role")
        if actualRole != role {
            return fmt.Errorf("forbidden: required role %q, got %q", role, actualRole)
        }
        return nil
    })
}

// Usage: JWT auth sets auth.role, then RequireRole checks it.
func buildAdminRoute() {
    jwtAuth := security.NewAuthProcessor(&security.AuthConfig{
        Type: "jwt",
        JWT: &security.JWTConfig{
            PublicKey: "/keys/public.pem",
            Issuer:    "https://auth.example.com",
        },
    })

    // Pipeline: rate limit -> JWT -> role check -> handler
    rl := security.NewRateLimiter(50, 1)

    // In route builder:
    // builder.From("http://0.0.0.0:8080/admin").
    //     Process(rl.Processor()).
    //     Process(jwtAuth).
    //     Process(RequireRole("admin")).
    //     To("direct:admin-handler")

    _ = rl
    _ = jwtAuth
}

MCP Server Security

MCP servers in Helix include a full HTTP security middleware stack:

Configuration

mcps:
  - id: my-tools
    addr: 0.0.0.0:3100
    api_keys: [mcp-secret-key]       # Bearer or X-API-Key auth
    trusted_proxies: ["127.0.0.1"]   # IPs that skip auth (gateway)
    rate_limit: 60                    # Requests/minute
    cors_origins: ["http://localhost:3000"]
    max_body_bytes: 2097152           # 2MB
    allowed_tools: []                 # Whitelist (empty = all)
    blocked_tools: [admin-tool]      # Blacklist (always blocked)

Security Middleware Stack

Applied in order for every HTTP request to the MCP server:

  1. CORS — validates Origin header, returns 403 for unlisted origins
  2. Trusted proxy bypass — requests from trusted_proxies skip auth
  3. IP allowlist — restricts client IPs (if configured)
  4. API key auth — constant-time Bearer/X-API-Key validation, returns 401
  5. Rate limiting — token-bucket per minute, returns 429
  6. Body size limit — rejects oversized payloads, returns 413
  7. Tool filtering — blocked_tools checked at tools/call time, returns MCP isError

Tool Access Control

  • allowed_tools: [tool-a] — only tool-a can be called; others get "tool X is not allowed"
  • blocked_tools: [tool-b] — tool-b always returns isError: true with "tool X is blocked"
  • If a tool is in both lists, blocked wins
  • Empty lists = no filtering (all exposed tools available)

Multi-MCP Security Isolation

Each MCP server has independent security:

mcps:
  - id: public     # Port 3100, key: public-key
    api_keys: [public-key]
  - id: admin      # Port 3101, key: admin-key
    api_keys: [admin-key]

Using public-key on port 3101 → 401 Rejected (cross-key isolation).


Runtime Governance Layer (built-in)

Helix ships its own deterministic, app-layer governance stack under pkg/agent/governance/ that runs inside the agent dispatcher — no external gateway required. It complements (and is independent of) the SynaptX integration below.

Configure under agent.governance: in YAML:

agents:
  - id: finance-agent
    type: react
    model: { provider: openai, name: gpt-4o-mini }
    tools: [transfer, get_balance]
    governance:
      audit_chain: true                    # hash-chained tamper-evident log
      rings:
        assignment: standard               # 4-tier: admin / standard / restricted / sandboxed
        permissions:
          standard: [get_balance, transfer]
          admin: ["*"]
      policy:
        - action: transfer
          effect: review                   # surface to /api/approvals
          starlang_when: |
            ctx["args"]["amount"] > 10000
        - action: get_balance
          effect: allow
          max_calls: 100
          window: 1m
        - action: "*"
          effect: allow

Subsystems (all optional, all composable):

Subsystem What it does
Hash-chained audit log sha256(prev_hash \|\| ts \|\| agent \|\| action \|\| decision) per entry; Verify() walks the chain
Scoped kill switches Global / Agent / Capability scopes, flipped at runtime via POST /api/kill-switch
Privilege rings 4-tier (Admin/Standard/Restricted/Sandboxed) with prefix-wildcard permissions; default-deny
Policy engine First-match-wins rules with allow / deny / review / rate_limit effects; optional Starlang predicates with ctx, agent_id, tool_name, tenant, action
Policy conflict resolver deny_overrides / allow_overrides / priority_first_match / most_specific_wins for stacked global+tenant+agent engines
MCP security scanner Detects typosquatting (Levenshtein-1 + Unicode confusables), tool poisoning, hidden instructions, rug-pull
Prompt-defense classifier Regex catalogue for instruction override / prompt exfiltration / credential leak / approval bypass

Evaluation order per tool call: kill switches → privilege rings → policy engine → audit. First non-allow decision wins; the dispatcher feeds a synthetic Tool call denied by governance: <reason> tool_result back to the LLM and emits a governance_block AgentEvent on the unified stream.

Runtime control (role-gated):

Endpoint Method Role Purpose
/api/audit GET viewer List recent audit entries
/api/kill-switch GET viewer Current kill-switch activations
/api/kill-switch POST admin Activate ({"scope":"agent:finance-agent","reason":"investigation","actor":"secops"})
/api/kill-switch/reset POST admin Clear all switches
/api/agents/{id}/pause POST operator Soft pause one agent
/api/agents/{id}/resume POST operator Resume
/api/agents/{id}/budget/reset POST operator Reset budget counters
/api/approvals GET viewer Pending review approvals
/api/approvals/{id} POST operator Resolve ({"approved":true,"feedback":"..."})

See the dedicated Governance Guide for the full Go API, conflict resolver, MCP scanner, and prompt-defense classifier with worked examples and the operational playbook.


SynaptX Security Integration

When Helix runs behind SynaptX gateway, two security layers work together:

Prompt Injection Guard

SynaptX detects 16 prompt injection patterns (per-workflow prompt_guard = true): - "Ignore previous instructions" — blocked (403) - DAN/jailbreak mode — blocked (403) - System prompt exfiltration — blocked (403) - Chat template delimiters — blocked (403) - Encoding evasion, token smuggling — warned (header)

PII Detection

SynaptX detects 10 PII types (per-workflow pii_mode = "block"): - SSN (123-45-6789) — blocked (403) - Credit card (4111-1111-1111-1111) — blocked (403) - AWS access key (AKIA...) — blocked (403) - Email, phone, IBAN, passport — configurable

Modes: off (default), warn (header), redact (replace), block (403)

Virtual Keys

Per-agent API keys with budget, rate limit, and model ACL:

curl -X POST http://localhost:7081/virtual-keys \
  -d '{"name":"code-agent","max_budget":50,"max_rpm":60,"allowed_models":["gpt-4o-mini"]}'

Governance Policies

ABAC/RBAC policies via admin API:

curl -X POST http://localhost:7081/governance/policies \
  -d '{"id":"block-admin","effect":"deny","subjects":{"agent_ids":["admin-tool"]},"resources":{"paths":["/ai/**"]}}'

Source File Reference

File Contents
pkg/security/security.go AuthConfig, JWTConfig, BasicAuthConfig, OAuth2Config, APIKeyConfig, TLSConfig, NewAuthProcessor, SecretResolver, RateLimiter, IPAllowlist
pkg/security/tls.go NetworkTLSConfig, NewTLSConfig, NewClientTLSConfig, NewServerTLSConfig, ParseTLSParams
pkg/config/vault.go VaultResolver, AWSSecretsResolver, CompositeSecretResolver, EnvResolver, ResolveSecrets
pkg/config/vault_client.go VaultClient, VaultOption, ReadSecret, ResolveSecrets (struct reflection)
pkg/config/config.go Config, TenantConfig, AuditConfig, ShutdownConfig, LoggingConfig
pkg/engine/tenant.go TenantInterceptor, TenantInterceptorConfig
pkg/engine/audit.go Workflow-level AuditInterceptor, AuditEntry, InMemoryAuditStore, SlogAuditStore (best-effort observation)
pkg/governance/audit_chain.go Hash-chained tamper-evident AuditLogger, Verify, LoadAuditChain
pkg/governance/engine_audit_bridge.go AuditLogger.AsEngineAuditStore() — exposes the chain via the engine.AuditStore + engine.Verifier interfaces
pkg/governance/kill_switch.go KillSwitchRegistry with Global / Agent / Capability scopes
pkg/governance/kill_switch_backend.go KillSwitchBackend interface + RedisKillSwitchBackend — cluster propagation via Redis pub/sub
pkg/governance/rings.go RingEnforcer — 4-tier privilege rings with prefix wildcards
pkg/governance/policy.go PolicyEngine, PolicyRule, Starlang predicates, fixed-window rate limit
pkg/governance/rate_limit_backend.go RateLimitBackend interface + LocalRateLimitBackend / RedisRateLimitBackend
pkg/governance/conflict.go PolicyConflictResolver — deny_overrides / allow_overrides / priority_first_match / most_specific_wins
pkg/governance/mcp_scanner.go McpSecurityScanner — typosquatting (Levenshtein-1 + confusables), poisoning, hidden instructions, rug-pull
pkg/governance/prompt_defense.go PromptDefenseEvaluator with four built-in threat classes
pkg/governance/stack.go Stack.Check — orchestrates kill-switch → rings → policy → audit
pkg/governance/interceptor.go NewInterceptor(stack, cfg) — adapts Stack into an engine.Interceptor for workflow-level gating
pkg/component/governance/ governance://stack-name?action=X URI component for non-agent workflow steps
pkg/engine/interfaces.go Interceptor, Processor, ProcessorFunc interfaces
pkg/workflow/rest_definition.go CORSDefinition, SecurityDefinition
pkg/dsl/rest.go RestBuilder.EnableCORS, RestBuilder.Security
pkg/component/mcp/server.go securityMiddleware, validateRequestAuth, extractClientIP, rateBucket
pkg/component/mcp/tools.go isInvocableRoute, sanitizeToolName, tool filtering helpers
pkg/component/websocket/websocket.go ServerComponent, ServerConsumer (RFC 6455 WS server with response)