Auth & Security

Hardening

Allowed-hosts (DNS-rebinding guard), connector credential encryption and key management, session-secret rules, and a locked-down production checklist.

Authentication and authorization decide who and what. Hardening covers the rest of the surface: which requests are even accepted, and how secrets are protected at rest. This page is the production-posture reference.

Allowed hosts (DNS-rebinding guard)

Ductor can validate the request Host header against an allowlist, which defends against DNS-rebinding attacks (a malicious page resolving your API's hostname to 127.0.0.1 to reach a locally-bound instance).

export DUCTOR_API_ALLOWED_HOSTS="api.example.com,api.internal.example.com"
  • The Host header is stripped of any port before matching.
  • A request whose host is not on the list is rejected with HTTP 421 Misdirected Request.
  • The default list is loopback-only (localhost, 127.0.0.1, ::1, 0.0.0.0) — convenient for local development.

The default is permissive for local dev — pin it in production

The middleware treats an empty allowlist as "allow all hosts" (no-op passthrough). The default covers only loopback names, which is fine for a laptop but means a production deployment must set api.allowed_hosts to its real external hostnames. Leaving it unset/empty behind a proxy disables the guard.

Connector credential encryption

Third-party connector credentials are the most sensitive data Ductor stores, and they are encrypted at rest with authenticated encryption.

  • Cipher. XChaCha20-Poly1305 (AEAD). Every credential blob is sealed with tenant-bound associated datatenantID | providerKey — so decryption fails loudly on any mismatch. That is what blocks a cross-tenant credential swap.

  • Master key. Supplied as a base64-encoded 32-byte key:

    export DUCTOR_CONNECTOR_ENCRYPTION_KEY="$(openssl rand -base64 32)"
  • Key separation. Newer envelopes HKDF-derive the working key from the master key using a purpose string, so blobs written for different purposes are cryptographically separated even under the same master key.

  • Per-tenant KEKs. Where per-tenant key-encryption-keys are configured, an additional envelope format binds each tenant's data to its own key, cached and invalidated across pods via a Redis eviction channel. The custody control plane behind these KEKs — BYOK modes, rotation, and the staged crypto-shred — is documented in Key custody (BYOK).

Key rotation and crypto-shred

  • Rotation and disable of a tenant KEK are local-pod immediate and fan out to every pod best-effort (bounded by the cache TTL on a missed broadcast).
  • Crypto-shred (destroy) is stronger: it overwrites the live wrapped ciphertext so the key cannot be restored through any normal database path, and its cross-pod broadcast is fail-closed — an unconfirmed broadcast is returned as an error, so a shred never reports success it cannot guarantee.

Rotate the master key deliberately

Losing DUCTOR_CONNECTOR_ENCRYPTION_KEY makes every stored credential undecryptable. Treat it like any other root secret: inject it from a secret manager, never commit it, and plan rotation using the documented key-id / rotation-keys mechanism so old ciphertext stays decryptable during cutover.

Session signing secrets

The HS256 secrets that sign SAML browser sessions (and other in-process session JWTs) must be at least 32 bytes. Secrets are generated as 32 bytes of cryptographic randomness and stored AEAD-encrypted at rest; the verifier rejects anything shorter. SAML session tokens additionally pin the signing method (HS256) at verification time, which blocks algorithm-confusion attacks.

Agent & MCP tool security

When Ductor exposes agent or MCP tools, the authorization for those tools is derived from server-owned descriptors — never from labels or schemas a caller supplies. Each descriptor carries the tool's input/output schema hashes, its read/write/destructive class, risk labels, the required scope, and the source identity. A caller cannot talk its way into a higher-privilege tool by asserting a friendlier label or a different schema.

  • Every invocation echoes tool_version and expected_schema_hash from the server's manifest, and authorization compares that hash with both the immutable grant and the current descriptor.
  • Drift, mismatch, missing, or duplicate descriptors are denied and produce a redacted decision receipt — a startup descriptor collision is a release blocker, not something to paper over with caller metadata.
  • The default policy exposes only server-classified read, non-mutating tools. Write, destructive, and unknown tools are not callable without an explicit policy mode and its mutation safeguards.
  • Exposure is scoped by the agent_tool_exposure:read / :write / :run / :admin scopes (see Authorization).

Never infer tool risk from caller input

Treat a schema mismatch as drift — refresh the tool session rather than retrying with a guessed hash — and never restore availability by accepting caller-supplied labels or name-based risk inference. The full model is in AI → Agent tool security.

Tamper-evident audit trail

Authorization decisions and credential-lifecycle events can be routed to a hash-chained audit sink (configured on the Authorization page). Each event is a link in a chain: its record hash is SHA-256(seq ‖ prev_hash ‖ payload), and the first event references a fixed genesis. A verifier walks the chain in seq order, recomputing each hash and checking that prev_hash matches the prior link — any seq gap, reordering, or payload edit breaks the chain and is detectable after the fact.

The audit event schema is frozen

Because the payload is hashed into the chain, the authz.AuditEvent field set is frozen — adding or removing a field would change how historical payloads serialize and break verification of the existing chain. Audit evidence is append-only in both senses: you cannot rewrite a past event, and you cannot reshape the record.

Enterprise security profile

Setting a named profile folds a stricter posture over your static config:

export DUCTOR_SECURITY_PROFILE=enterprise

Under the enterprise profile, an anonymous / no-op authorizer is not permitted — a deployment that would otherwise resolve to the anonymous posture fails to construct a real authorizer and does not start. It is the belt-and-braces switch that makes "accidentally running without authorization" impossible in production.

Locked-down production checklist

Work top to bottom before exposing an instance:

Production auth & security checklist

Authentication

  • DUCTOR_AUTH_ENABLED=true, and DUCTOR_AUTH_ALLOW_ANONYMOUS unset/false (it is honored only in development anyway).
  • A real identity source is wired: OIDC (api.oidc_issuer + api.oidc_audience) and/or DB-backed API keys (auth.api_key_enabled) and/or SAML. The legacy static auth.api_keys setting is not supported for request authentication.

Authorization

  • Plan the cutover to authz.allow_when_unconfigured=false.
  • Configure an audit sink (audit.sink = file/stderr/stdout) with hash_chain on; keep redact_pii on in production (it defaults on).

Tenancy

  • Confirm callers are tenant-scoped (claims) or send a validated X-Tenant-ID.
  • Reserve tenant admin for full same-tenant administration. Provision the separate deployment-issued platform-admin role only for genuine cross-tenant control-plane operators.

Entitlements

  • Run DUCTOR_ENTITLEMENT_ENFORCEMENT=report first, watch the grace-admit events, then move to strict.

Hardening

  • Set DUCTOR_API_ALLOWED_HOSTS to your real external hostnames.
  • Provide a 32-byte DUCTOR_CONNECTOR_ENCRYPTION_KEY from a secret manager.
  • Terminate TLS in front of the API; ensure SAML acs_base_url is HTTPS.
  • Consider DUCTOR_SECURITY_PROFILE=enterprise to forbid the anonymous posture.

For the condensed operational runbook of these same knobs, see Operations → Security & auth.