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
Hostheader 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 data —
tenantID | 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_versionandexpected_schema_hashfrom 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/:adminscopes (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=enterpriseUnder 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, andDUCTOR_AUTH_ALLOW_ANONYMOUSunset/false (it is honored only indevelopmentanyway).- 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 staticauth.api_keyssetting 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) withhash_chainon; keepredact_piion in production (it defaults on).
Tenancy
- Confirm callers are tenant-scoped (claims) or send a validated
X-Tenant-ID. - Reserve tenant
adminfor full same-tenant administration. Provision the separate deployment-issuedplatform-adminrole only for genuine cross-tenant control-plane operators.
Entitlements
- Run
DUCTOR_ENTITLEMENT_ENFORCEMENT=reportfirst, watch the grace-admit events, then move tostrict.
Hardening
- Set
DUCTOR_API_ALLOWED_HOSTSto your real external hostnames. - Provide a 32-byte
DUCTOR_CONNECTOR_ENCRYPTION_KEYfrom a secret manager. - Terminate TLS in front of the API; ensure SAML
acs_base_urlis HTTPS. - Consider
DUCTOR_SECURITY_PROFILE=enterpriseto forbid the anonymous posture.
For the condensed operational runbook of these same knobs, see Operations → Security & auth.
Key custody & crypto-shred (BYOK)
Per-tenant key-encryption-key custody — platform-managed vs BYOK modes, the binding lifecycle, the staged crypto-shred state machine, and pluggable KMS backends.
Operating the Clearing Layer
Running Ductor in production — the operational model, plus migrations, observability, security, keys, and day-2 runbooks.