Operations

Security & auth

OIDC, DB-backed API keys, allowed hosts, RBAC, and connector credential encryption.

Ductor's security surface spans authentication (who you are), authorization (what you may do), request hardening (which requests are even accepted), and secret protection (connector credentials at rest). This page covers the operational knobs for each.

Looking for the full model?

This page is the condensed operator runbook. For the exhaustive reference — every credential type, the claim-mapper registry, the RBAC role/scope catalog, the entitlement plane, and the cross-tenant boundary — see the Auth & Security section: Authentication, API keys, Authorization, Entitlements, Tenancy isolation, and Hardening.

Request Allowed hosts+ rate limits AuthenticationOIDC / API key Tenant isolation AuthorizationRBAC roles + scopes Entitlementenforcement Handler

Authentication

Two mechanisms, controlled by auth.* and api.*:

OIDC JWT (primary)

Validate bearer tokens against your IdP:

export DUCTOR_AUTH_ENABLED=true
export DUCTOR_API_OIDC_ISSUER="https://your-idp.example.com/"
export DUCTOR_API_OIDC_AUDIENCE="ductor-api"

The tenant is derived from the token's claims. Clients send Authorization: Bearer <jwt>.

DB-backed API keys (service-to-service)

Enable with auth.api_key_enabled=true. Keys are created per tenant via POST /api/v2/api-keys, stored bcrypt-hashed, and returned in plaintext once. See Issue & use API keys. Clients send X-API-Key: <secret>.

Never ship anonymous access

auth.allow_anonymous=true and auth.enabled=false are development conveniences. In production set auth.enabled=true and leave anonymous off.

Authorization (RBAC)

API keys and principals carry roles (viewer / operator / admin / service, plus module roles) and per-key scopes (resource:action, e.g. pool:write). Sensitive operations require specific roles and scopes — for example CreateApiKey requires the admin role and tenant:write scope.

authz.allow_when_unconfigured controls the fail posture when no authorizer is wired: it defaults to true (fail-open) but should be set to false in security-sensitive deployments so unconfigured authorization denies rather than allows.

Entitlement enforcement

Entitlements gate which capabilities a tenant may use (connectors, strategies, MCP tools, workflow surfaces). The runtime posture is set by entitlement.enforcement (DUCTOR_ENTITLEMENT_ENFORCEMENT):

ModeBehavior
offEntitlements are not evaluated.
report (default)Evaluate and emit grace-admit evidence, but do not deny traffic — fail-open, but loud and observable.
strictDeny when a required entitlement is missing or the source is stale.

report is the migration-safe default: flip to strict only after tenant entitlement snapshots are provisioned and monitored. In every mode an explicit deny always denies — a snapshot may not carry a conflicting allow and deny for the same item, and a deny is never overridden by a grace admit. entitlement.grace_unprovisioned lets a strict-mode bootstrap admit a genuinely un-onboarded tenant (loudly) without fail-opening a present-but-incomplete snapshot. See Entitlements.

Agent & MCP tool security

Agent and MCP tools are exposed through server-owned descriptors: the tool surface a caller sees is defined by the server, not supplied by the client, and defaults to least privilege. This keeps an agent (or a compromised prompt) from inventing tool authority it was never granted. See Agent tool security.

Tenancy isolation

Tenant context comes from the authenticated principal (JWT claims), not from request parameters. An optional X-Tenant-ID header may select a tenant but must match the authenticated tenant — a mismatch is rejected with 403. This is what prevents a caller from acting across tenant boundaries. (TenantService/ CreateTenant and health endpoints are the documented exemptions.)

Request hardening

ControlConfigPurpose
Allowed hostsapi.allowed_hostsReject requests whose Host isn't allow-listed
Body size capapi.max_request_body_size (4 MiB)Bound request bodies
Global concurrencyapi.max_concurrent_requestsCap in-flight requests
Per-tenant concurrencyapi.max_concurrent_requests_per_tenantFairness across tenants
Rate limitsratelimit.global_rps, ratelimit.tenant_rpsThrottle; ratelimit.fail_mode sets Redis-outage behavior
CORScors.allowed_originsRestrict browser origins
Egressegress.mode (shadow/enforce/disabled) + allowlistsGuard outbound connector calls

allowed_hosts default is permissive

api.allowed_hosts defaults to loopback plus 0.0.0.0 for local development. Restrict it to your real hostnames in production — the Dokploy stack, for example, lists api.ductor.io and docs.ductor.io.

egress.mode defaults to shadow

egress.mode ships as shadow — outbound connector calls that would be blocked are logged but still allowed, so you can see the impact before enforcing. Move it to enforce in production; security_profile=enterprise requires enforce. Defaults already block private networks and the cloud metadata IP; extend the allow list with egress.allowed_domains and egress.allowed_ip_nets.

Connector credential encryption

Connector credentials are encrypted at rest with XChaCha20-Poly1305 (AEAD) using a base64-encoded 32-byte master key:

export DUCTOR_CONNECTOR_ENCRYPTION_KEY="$(openssl rand -base64 32)"
  • Inject the key from a secret manager — never bake it into an image or commit it.
  • Rotate by setting a new active key/connector.encryption_key_id and keeping prior keys in the decrypt-only connector.rotation_keys keyring so existing ciphertext stays readable.
  • Losing the key means losing access to every stored connection; leaking it exposes them.

The same master key also protects payload bytes when the AEAD payload codec is enabled, and BYOK/KEK unwrapping (key_provider.*) can source it from an external KMS or Vault. Rotation is a process-level concern with its own procedure — see Key management. This is distinct from per-tenant BYOK custody, covered in Key custody.

Transport security

  • Public TLStls.cert_file / tls.key_file, or terminate TLS at your ingress (Traefik on Dokploy, an Ingress on Kubernetes).
  • Internal / executor planesserver.internal_* and server.exec_* support mutual TLS (*_require_mtls, cert/key/client-CA files) plus an HMAC shared secret for executor RPC.

Enterprise posture

Set security_profile=enterprise to force fail-closed controls across subsystems. The Helm chart's production: true gate additionally requires this profile alongside external secrets, loopback-bound metrics, and a non-default service account — see Kubernetes (Helm).

Identity federation (SCIM / SAML)

For enterprise SSO and provisioning, enable identity.saml.* (SAML SP, ACS URL, session TTL) and identity.scim.* (SCIM 2.0 at /scim/v2). Mint a SCIM token with ductor scim-token mint, and manage a tenant's SAML IdP with ductor saml-idp set/get/disable.

Audit

Enable tamper-evident audit logging with audit.sink (stderr/stdout/file) and audit.hash_chain=true (SHA-256 chaining, on by default when a sink is set). Use audit.redact_pii=true to redact sensitive fields.