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.
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):
| Mode | Behavior |
|---|---|
off | Entitlements are not evaluated. |
report (default) | Evaluate and emit grace-admit evidence, but do not deny traffic — fail-open, but loud and observable. |
strict | Deny 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
| Control | Config | Purpose |
|---|---|---|
| Allowed hosts | api.allowed_hosts | Reject requests whose Host isn't allow-listed |
| Body size cap | api.max_request_body_size (4 MiB) | Bound request bodies |
| Global concurrency | api.max_concurrent_requests | Cap in-flight requests |
| Per-tenant concurrency | api.max_concurrent_requests_per_tenant | Fairness across tenants |
| Rate limits | ratelimit.global_rps, ratelimit.tenant_rps | Throttle; ratelimit.fail_mode sets Redis-outage behavior |
| CORS | cors.allowed_origins | Restrict browser origins |
| Egress | egress.mode (shadow/enforce/disabled) + allowlists | Guard 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_idand keeping prior keys in the decrypt-onlyconnector.rotation_keyskeyring 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 TLS —
tls.cert_file/tls.key_file, or terminate TLS at your ingress (Traefik on Dokploy, an Ingress on Kubernetes). - Internal / executor planes —
server.internal_*andserver.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.