resources.yaml: what interfaces share
resources.yaml defines, once and by a name, everything interfaces connect to that isn't a
source or a destination of their own: the databases their code queries, SFTP servers, HTTP APIs,
directories people sign in with, and certificates. Interfaces and their code use the name:
const row = await db("patient_db").one("SELECT mrn FROM patients WHERE id = $1", [id]);
const patient = await api("empi").get(`/patients/${mrn}`);
Why names: no address, login or password is ever written in an interface or its code. The
code is the same in every environment; resources.yaml fills in each environment's host with
${NAME} and its password with ${secret:NAME} (environments). The engine
logs in on the code's behalf, so the code never sees a credential. And because every connection
is named, ductus doctor can check each one from the server, ductus impact can say which
interfaces use it, and its certificate is watched for expiry.
When not: an interface's own source and destinations (an MLLP receiver, a folder) are in its interface.yaml, not here, unless several share them: an SFTP server and an HTTP API are always resources, so their login is in one place.
Every entry of every kind also takes application, vendor and owner (who to ask, best an
email address: its certificate's expiry alerts reach them with to: owner), for ductus impact
and alerts, and custom:, your own documentation fields.
Databases
databases are the databases interfaces' code
queries with db("<name>"): lookups, inserts, a nightly extract. Each is
postgres or
sqlserver.
# resources.yaml
databases:
patient_db:
postgres:
host: ${PATIENT_DB_HOST}
database: patients
user: ductus_lookup
password: ${secret:PATIENT_DB_PASSWORD}
tls: verify-full
max_connections: 5
application: Patient index
owner: dba@hospital.org
billing_db:
sqlserver:
host: ${BILLING_DB_HOST}
database: Billing
user: ductus
password: ${secret:BILLING_DB_PASSWORD}
application: Billing
vendor: Cerner
- The login: give it only what the queries need, often read-only on a few tables or views. A wrong password makes messages wait and retry rather than quarantine: fixing it fixes them all (nuance).
- Connections: each entry has a pool, shared by every interface's calls, of up to
max_connections(default 5), opened as needed. A call that finds them all busy waits up to 10 s, then fails as a temporary error (the message waits and is tried again). Raise it when calls queue while the database itself is quick, within what its DBA allows for every engine; don't raise it to cure slow queries. A busy interface withisolation: dedicatedcan have connections of its own (dedicated_pools). - Postgres TLS works as for the engine's own database:
tls: verify-fullchecks the server's certificate against this server's trusted CAs (ca:names a private one); withouttls, it'sprefer, which checks nothing (PostgreSQL's TLS). - SQL Server is always TLS, verified:
encrypt: mandatoryby default,strict(TDS 8) for servers set to force it; there's no unencrypted mode.ca:for the domain's CA where this server doesn't trust it;name_in_certificatewhen connecting by an alias or a listener's name (nuance). A named instance isinstance: PROD: its port comes from the SQL Server Browser (UDP 1434, which the firewall must let through). - SQL Server Windows logins (
auth: windows), where the DBA allows only those: withoutuser, the engine logs in as the Windows service's own account (a gMSA is best: no password anywhere); withuserandpassword, as that domain account. From Linux that is always NTLM, which some servers refuse (nuance, from Linux).
# resources.yaml
databases:
billing_db:
sqlserver:
host: ${BILLING_DB_HOST}
instance: PROD
database: Billing
auth: windows # as the service's account: on Windows, best a gMSA
What code can do with one, and what to watch for: one result set per call on SQL Server, a
query that writes is a write a crash can repeat, write names out (db("patient_db"), not
db(name), so ductus impact can see it)
(databases and APIs, running twice).
SQL Server, worth knowing before the first connection:
encrypt: strictis TDS 8: TLS first, before anything else is said (nuance). SQL Server 2022 on Linux refuses it whatever it's given; 2025 accepts it (nuance).- Connecting by an alias, a listener or an address the certificate doesn't list:
name_in_certificatechecks it for the right name (nuance). - Its certificate is read through PRELOGIN, as the connection meets it, and watched when verified (nuance); verifying it costs nothing measurable (measured).
- Windows logins (
auth: windows) from the service's own account are the computer's account on the network: give that a login (nuance). Doctor and the start log say whether Kerberos or NTLM was used (nuance). A refusal is 18452 (Windows said no) or 18456 (no login on SQL Server's side) (nuance), and Extended Protection set to Required refuses NTLM (nuance).
SFTP servers
sftp entries are the servers sftp sources read from
and sftp destinations write to (server: <name>).
# resources.yaml
sftp:
quest:
host: ${QUEST_SFTP_HOST}
user: hospital
private_key: ${secret:QUEST_SFTP_KEY}
host_key: "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"
application: Quest Lab Results
vendor: Quest
- Log in with a key where the server takes one:
ssh-keygen -t ed25519 -f questmakes a pair; the private half goes in a secret (ductus secret set QUEST_SFTP_KEY --env prod <quest), the public half (quest.pub) to the server's owner. Apasswordworks too. host_keyis the server's key fingerprint, so a server pretending to be this one is refused before any password or key reaches it. Leave it out the first time: the connection fails, and the error gives the fingerprint the server presented. Check it with the server's administrator, then paste it in. When their key changes (a rebuilt server), connections fail until you update it: on purpose, since that's what an impostor looks like too.
HTTP APIs
apis are the HTTP APIs the code calls with
api("<name>"), and that http destinations send through (api: <name>): a base URL, the
headers every request carries (the token), a timeout, and TLS.
# resources.yaml
apis:
empi:
url: ${EMPI_URL}
headers: { Authorization: "Bearer ${secret:EMPI_TOKEN}" }
timeout: 10s
application: EMPI
owner: empi-team@hospital.org
- Paths in code and destinations go under the URL:
api("empi").get("/patients/1")calls${EMPI_URL}/patients/1. - A header the code or a destination sets itself replaces the API's of the same name (nuance).
- 5xx, 408 and 429 answers are retried by default (nuance).
ca:for an API with a private CA's certificate,certificate:for one that asks for a client certificate (mutual TLS): bothcertificates:entries (TLS).
Directories
ldap entries are the directories people sign in to
the web UI with (Active Directory, OpenLDAP): the server, its TLS and the service account that
looks people up. auth.ldap in ductus.yaml names the one to use and maps its groups to roles:
People: users, tokens, the directory.
Certificates
certificates are the certificates the engine
presents (on its listeners, the API, and as a client certificate) and the CAs it trusts or
checks clients against, each by a name: TLS and certificates.