diff --git a/assets/docs/MessageFoundry-Adoption-Guide.pdf b/assets/docs/MessageFoundry-Adoption-Guide.pdf index c446d7a..f8bde51 100644 Binary files a/assets/docs/MessageFoundry-Adoption-Guide.pdf and b/assets/docs/MessageFoundry-Adoption-Guide.pdf differ diff --git a/assets/docs/MessageFoundry-Architecture.pdf b/assets/docs/MessageFoundry-Architecture.pdf index 3a5b41e..f0a9e6f 100644 Binary files a/assets/docs/MessageFoundry-Architecture.pdf and b/assets/docs/MessageFoundry-Architecture.pdf differ diff --git a/assets/docs/MessageFoundry-Configuration.pdf b/assets/docs/MessageFoundry-Configuration.pdf index e22eead..7f91927 100644 Binary files a/assets/docs/MessageFoundry-Configuration.pdf and b/assets/docs/MessageFoundry-Configuration.pdf differ diff --git a/assets/docs/MessageFoundry-Deployment-Guide.pdf b/assets/docs/MessageFoundry-Deployment-Guide.pdf index a1f2c79..475aacc 100644 Binary files a/assets/docs/MessageFoundry-Deployment-Guide.pdf and b/assets/docs/MessageFoundry-Deployment-Guide.pdf differ diff --git a/assets/docs/MessageFoundry-HA-Reference-Architecture.pdf b/assets/docs/MessageFoundry-HA-Reference-Architecture.pdf index da088c2..982b192 100644 Binary files a/assets/docs/MessageFoundry-HA-Reference-Architecture.pdf and b/assets/docs/MessageFoundry-HA-Reference-Architecture.pdf differ diff --git a/assets/docs/MessageFoundry-Install-Guide.pdf b/assets/docs/MessageFoundry-Install-Guide.pdf index 3d86b7d..ec0f27a 100644 Binary files a/assets/docs/MessageFoundry-Install-Guide.pdf and b/assets/docs/MessageFoundry-Install-Guide.pdf differ diff --git a/assets/docs/MessageFoundry-Remote-Console-Customer-Guide.pdf b/assets/docs/MessageFoundry-Remote-Console-Customer-Guide.pdf index 100ad26..3d7686b 100644 Binary files a/assets/docs/MessageFoundry-Remote-Console-Customer-Guide.pdf and b/assets/docs/MessageFoundry-Remote-Console-Customer-Guide.pdf differ diff --git a/assets/docs/MessageFoundry-Security-Overview.pdf b/assets/docs/MessageFoundry-Security-Overview.pdf index ca0132f..917f3ef 100644 Binary files a/assets/docs/MessageFoundry-Security-Overview.pdf and b/assets/docs/MessageFoundry-Security-Overview.pdf differ diff --git a/assets/docs/MessageFoundry-Users-and-Security.pdf b/assets/docs/MessageFoundry-Users-and-Security.pdf index c63d44a..a154502 100644 Binary files a/assets/docs/MessageFoundry-Users-and-Security.pdf and b/assets/docs/MessageFoundry-Users-and-Security.pdf differ diff --git a/assets/docs/MessageFoundry-Windows-Service.pdf b/assets/docs/MessageFoundry-Windows-Service.pdf index 15cc151..83679d3 100644 Binary files a/assets/docs/MessageFoundry-Windows-Service.pdf and b/assets/docs/MessageFoundry-Windows-Service.pdf differ diff --git a/assets/docs/_md/Adoption-Guide.md b/assets/docs/_md/Adoption-Guide.md index 9510c1d..cf23cad 100644 --- a/assets/docs/_md/Adoption-Guide.md +++ b/assets/docs/_md/Adoption-Guide.md @@ -58,8 +58,9 @@ operate it over a localhost HTTP API. See [ARCHITECTURE.md](ARCHITECTURE.md) for and who are comfortable validating a pre-1.0 tool against their own traffic before trusting it. A single engine node on a trusted network is the simplest pilot; **native TLS** (API + MLLP) and an opt-in **active-passive failover** cluster on a shared server-DB store (PostgreSQL or SQL Server) are both built when you need them (see -§2/§6/§14). What is genuinely *not* there yet is MFA, off-box log shipping, and a de-identification -framework — track those items (§2) and pilot the parts that are ready. (Horizontal *active-active* +§2/§6/§14), as are **native TOTP MFA** for local accounts and **off-box log/audit forwarding** to a +syslog/SIEM collector. What is genuinely *not* there yet is a de-identification +framework — track that item (§2) and pilot the parts that are ready. (Horizontal *active-active* scale-out was dropped and is not a planned milestone; active-passive HA is the supported HA model.) --- @@ -102,8 +103,7 @@ use the table below alongside them when planning. **The early-adopter bargain, stated plainly:** you get a durable engine with native TLS, real auth, opt-in active-passive failover, and a real validation toolchain, in exchange for validating capacity on -your own hardware and supplying the operational pieces that aren't built yet (MFA, off-box logs, -de-identification). If that trade is acceptable, the rest of this guide is your playbook. +your own hardware and supplying the one operational piece that isn't built yet (de-identification). If that trade is acceptable, the rest of this guide is your playbook. --- @@ -207,11 +207,13 @@ host. | `harness` | PySide6 | Running the standalone test-harness GUI | | `sftp` | paramiko | SFTP connectors | | `sqlserver` | `aioodbc` **+ OS-level Microsoft ODBC Driver 18** | The SQL Server *store* backend (`backend=sqlserver`, production) and the DATABASE connector family. | -| `dev` | pytest/ruff/mypy/httpx | Development & CI | +| `dev` | pytest (+ asyncio/timeout/rerunfailures plugins), ruff, mypy | Development & CI | -> ⚠️ There is **no friendly preflight** for the `postgres` extra: if you set `backend=postgres` but -> forgot `pip install 'messagefoundry[postgres]'`, you get a raw `ImportError` at startup instead of a -> clear message. Install the extra with the backend. +> ⚠️ `messagefoundry check` cannot catch a missing `postgres` extra — it validates a *declared* +> backend without dialing the database. If you set `backend=postgres` but forgot +> `pip install 'messagefoundry[postgres]'`, the engine fails at startup with a clear error naming the +> extra and the exact pip command (the `sqlserver` backend behaves the same way). Install the extra +> with the backend. ### Start your own config repo (`messagefoundry init`) @@ -370,9 +372,10 @@ Guidance for a clean first flow: ## 6. Security & PHI hardening before real data Full references: **[SECURITY.md](SECURITY.md)**, **[PHI.md](PHI.md)**, and **[DEPLOYMENT.md](DEPLOYMENT.md)** -(network exposure). MEFOR ships real auth, RBAC, audit, opt-in at-rest encryption, and **native TLS** -(API + MLLP, with a fail-closed off-loopback bind guard); the remaining transport gaps are **MFA** and -**off-box log shipping**. Complete this checklist **before any real PHI flows**: +(network exposure). MEFOR ships real auth, RBAC, audit, opt-in at-rest encryption, **native TLS** +(API + MLLP, with a fail-closed off-loopback bind guard), **native TOTP MFA** for local accounts, and +**off-box log/audit forwarding**; the remaining transport gap is **TLS for the raw TCP and X12 +connectors**, which are plaintext-only. Complete this checklist **before any real PHI flows**: - [ ] **API off-loopback requires native TLS.** The API binds `127.0.0.1` by default. To reach it from another host, configure **in-process TLS** (`[api].tls_cert_file` + `[api].tls_key_file`, @@ -637,7 +640,11 @@ as a **separate, later** step so you retain a fallback. **Verify-it-runs (after every start/restart):** `GET /health` → `{"status":"ok"}`, send a synthetic message, and confirm the **"wiring started"** banner in `service.out.log`. -**Monitoring surfaces (poll the API + parse logs — there is no Prometheus exporter):** +**Monitoring surfaces (scrape `/metrics` with Prometheus; poll the JSON API and watch the logs for the rest):** + +- `/metrics` — **Prometheus text exposition**: per-connection counters, queue-depth / in-pipeline / + oldest-pending gauges, latency histograms, and host CPU/memory. Gated by `monitoring:read` exactly + like `/stats`, so point your scraper at it with a service token. - `/stats` — outbox counts by status. - `/status` — DB size vs disk free, journal mode, counts. **Scrape db-size-vs-disk-free.** - `/status/integrity-check` — on-demand store integrity. @@ -654,8 +661,10 @@ message, and confirm the **"wiring started"** banner in `service.out.log`. **Log management:** logs land under `\logs` via NSSM. Configure rotation, keep the level at `INFO` or above (DEBUG can leak PHI — §6), treat `service.out/err.log` as **potential-PHI artifacts** -(ACL them; don't ship them off-box — off-box logging is deferred), and include them in your retention -policy. +(ACL them; rather than shipping the raw files, use the built off-box evidence copy — the +`[logging].forward_*` syslog/SIEM stream, to which PHI redaction applies exactly as it does to stdout — +and set `forward_protocol = "tls"`, since the transport default is UDP), and include them in your +retention policy. **Graceful drain for maintenance:** stopping the service (Ctrl+C / NSSM stop) triggers the ASGI lifespan to call `engine.stop()` for a clean drain. Always **drain → stop → back up → change → restart diff --git a/assets/docs/_md/Architecture.md b/assets/docs/_md/Architecture.md index 55aabc6..d935dbb 100644 --- a/assets/docs/_md/Architecture.md +++ b/assets/docs/_md/Architecture.md @@ -61,7 +61,11 @@ WebSocket API (FastAPI/uvicorn). The same API serves three deployments without c process (or a browser) that reaches the engine only over the HTTP API, never by importing it. - **Local daemon** — engine runs as a Windows service / Linux daemon; the web console attaches over the API. See [SERVICE.md](SERVICE.md) for the Windows service setup (NSSM). -- **Remote** — same API over the network (Phase 2+, with auth/TLS). +- **Remote** — the same API over the network. Supported today, **off by default**: set + `[security].local_access_only = false` and name the bind address in `[security].listen_address`, + with TLS either **in-process** (`[api].tls_cert_file`) or **terminated upstream** at a declared + trusted proxy (`[api].tls_terminated_upstream` + `[api].trusted_proxies`). Auth is always required, + and an off-loopback plaintext bind is refused at startup. We deliberately did **not** start with two separate processes + hand-rolled IPC. The logical boundary (library API) comes first; physical split is a deployment choice. @@ -87,7 +91,7 @@ flowchart TB subgraph ENGINE["Engine — headless asyncio service (no GUI imports)"] PIPE["pipeline/ — RegistryRunner
listener · router · transform · delivery workers"]:::engine - TRANS["transports/ — connector registry
MLLP · File · X12 · DICOM C-STORE SCP (TCP/HTTP/DB planned)"]:::engine + TRANS["transports/ — connector registry
MLLP · TCP · HTTP · File · SFTP/FTP · REST · SOAP · Database · X12 · FHIR · DICOM · Email · …"]:::engine PARSE["parsing/ — pure HL7/X12/DICOM library
python-hl7 · hl7apy · X12 codec · DICOM codec · base64 binary codec"]:::engine STORE[("store/ — staged queue
SQLite WAL · SQL Server · AES-256-GCM")]:::engine CFG["config/ — code-first wiring
Connections · Routers · Handlers · environments/"]:::engine @@ -360,12 +364,12 @@ hashed resolution lives in the committed **`uv.lock`** / **`requirements.lock`** **Optional extras** -- `harness` → `PySide6` (LGPL — chosen so the OSS test harness GUI is distributable; not PyQt) + `httpx` + `truststore` (was `[console]` before the desktop console was retired — BACKLOG #103) +- `harness` → `PySide6` only (LGPL — chosen so the OSS test harness GUI is distributable; not PyQt); the harness is the sole PySide6 consumer (was `[console]` before the desktop console was retired — BACKLOG #103; its HTTP client, `httpx` + `truststore`, is now a base runtime dependency rather than part of this extra) - `sqlserver` → `aioodbc` (production SQL Server store; also needs the OS-level Microsoft ODBC Driver 18 for SQL Server, which is not pip-installable; lazy-imported so SQLite-only installs skip it) - `dicom` → `pydicom` + `pynetdicom` (DICOM codec — headers/SR only, no numpy — and the C-STORE SCP inbound connector; ADR 0025; lazy-imported so non-DICOM installs skip it) -- `dev` → `pytest`, `pytest-asyncio`, `httpx` (ASGI test client for the API), `ruff`, `mypy` +- `dev` → `pytest` (with the `asyncio`, `timeout` and `rerunfailures` plugins), `ruff`, `mypy` (`httpx`, the ASGI test client, is now a base dependency and is no longer declared here) **Build / tooling** — `hatchling` (build backend), Ruff (format + lint, no Black), mypy (strict), pytest. diff --git a/assets/docs/_md/Configuration.md b/assets/docs/_md/Configuration.md index 1bf1228..c186b81 100644 --- a/assets/docs/_md/Configuration.md +++ b/assets/docs/_md/Configuration.md @@ -160,7 +160,7 @@ The one row that *does* vary: | `tls_ciphers` | str | _unset_ | optional OpenSSL cipher string (default = the interpreter's secure defaults). | | `tls_client_ca_file` | str | _unset_ | CA bundle to **require + verify client certs** (opt-in mTLS, e.g. the console). Requires `tls_cert_file`. | | `tls_client_ca_pin` | str | _unset_ | optional lowercase-hex SHA-256 pin over the corresponding CA anchor PEM (`tls_client_ca_file`); a mismatch refuses at load + reload (ASVS 6.7.1); unset = no pin (dormant). | -| `tls_client_cert_identities` | map str→str | `{}` | **(#200, ADR 0002):** mTLS client-cert → MessageFoundry principal map. Meaningful only with in-process mTLS (`tls_client_ca_file` set, so uvicorn `CERT_REQUIRED`-verifies the peer): a **verified** peer cert's subject CN / SAN is resolved to an existing username through this **allow-list**, and that principal's RBAC authorizes the request — a service-to-service identity that carries no bearer token. Keys are the qualified cert name `CN:` or `SAN::` (e.g. `SAN:DNS:svc.internal`); values are existing usernames. **Deny-by-default:** an unmapped verified cert — or a spoofed CN not listed here — resolves to no identity and is denied. A structured map, so **TOML-only** (no env-string form). Empty (the default) disables cert-identity. **Honest caveat:** stock uvicorn does not surface the peer cert to the ASGI scope, so the resolver is **inert** until a TLS-extension-capable server/shim populates it. | +| `tls_client_cert_identities` | map str→str | `{}` | **(#200, ADR 0002):** mTLS client-cert → MessageFoundry principal map. Meaningful only with in-process mTLS (`tls_client_ca_file` set, so uvicorn `CERT_REQUIRED`-verifies the peer): a **verified** peer cert's subject CN / SAN is resolved to an existing username through this **allow-list**, and that principal's RBAC authorizes the request — a service-to-service identity that carries no bearer token. Keys are the qualified cert name `CN:` or `SAN::` (e.g. `SAN:DNS:svc.internal`); values are existing usernames. **Deny-by-default:** an unmapped verified cert — or a spoofed CN not listed here — resolves to no identity and is denied. A structured map, so **TOML-only** (no env-string form). Empty (the default) disables cert-identity. **LIVE, not inert ([ADR 0083](adr/0083-mtls-client-certificate-identity.md)).** Stock uvicorn does not surface the peer cert to the ASGI scope, so `serve` swaps in a scope-populating uvicorn HTTP-protocol subclass (`messagefoundry/api/tls_client_cert.py`) **whenever `tls_client_ca_file` and this map are both set** — that shim is what lets a `CERT_REQUIRED`-verified peer cert reach the resolver. Both conditions are required: set the map without the CA and nothing is verified, so nothing resolves. | | `tls_client_cert_files` | list[str] | `[]` | **(ASVS 6.4.5):** PEM paths of **inbound service callers'** client certs you hold a copy of. Folded into the [`[cert_monitor]`](#cert_monitor) scan, so a caller's cert expiry is caught **even while that caller has stopped connecting** — the handshake-time check can only see a cert still being presented. These are certs the engine *verifies*, not ones it *presents*, so the served-cert scan cannot see them. Public certificates only (never a key); empty = off. | | `trusted_proxies` | list[str] | `[]` | **`[BUILT]` (WP-15):** reverse-proxy IP(s) whose `X-Forwarded-For`/`-Proto` are trusted (uvicorn `forwarded_allow_ips`), so the audit/rate-limit source IP is the **real client**, not the proxy. **Empty = trust nothing** (the direct TCP peer is used). Set ONLY to the proxy's address(es), or XFF spoofing returns — every host inside an entry may declare its own source address, so a broad range (e.g. `10.0.0.0/8` on a LAN numbered out of 10/8) makes every workstation a trusted spoofer. `"*"` and unparseable entries are **refused at load** (uvicorn would silently treat the latter as a never-matching literal, collapsing every client to the proxy). | | `tls_terminated_upstream` | bool | `false` | **`[BUILT]` (WP-15):** declare that a reverse proxy / load balancer terminates TLS in front of the engine. Lets a non-loopback bind satisfy the TLS gate **without** in-process TLS — but only when `trusted_proxies` is set (else refused at load). | diff --git a/assets/docs/_md/Deployment-Guide.md b/assets/docs/_md/Deployment-Guide.md index 6925e47..bef438c 100644 --- a/assets/docs/_md/Deployment-Guide.md +++ b/assets/docs/_md/Deployment-Guide.md @@ -18,8 +18,17 @@ the engine needs, see [`ANTIVIRUS-FIREWALL.md`](ANTIVIRUS-FIREWALL.md). MessageFoundry runs **on-premises** and binds **loopback (`127.0.0.1`) by default** — the engine API (`[security].local_access_only = true`), and every inbound listener via `[inbound].bind_host`. In that -posture there is **no off-host network exposure**: nothing PHI-bearing crosses a wire. Everything below -is about what changes when you deliberately bind a channel to a routable address. +posture **no MessageFoundry listener is reachable off-host**. + +**That is a claim about listeners only — it is not a claim that nothing PHI-bearing crosses a wire.** +Outbound delivery is **unaffected by the bind posture**: a loopback-bound engine still dials every +configured destination (MLLP, REST/SOAP, FHIR, DICOMweb, SFTP/FTPS, SMTP, Direct, a customer database), +still performs `db_lookup` / `fhir_lookup` reads, and still forwards syslog/SIEM logs and webhook +alerts — all off-box, and those delivery hops carry PHI. The outbound controls below (egress +allow-lists, the cleartext-hop rules, per-connection TLS) apply to a loopback deployment too. + +Everything else below is about what changes when you deliberately bind a *channel* to a routable +address. **Fail-closed rule (ADR 0002 §0):** a non-loopback **API** bind is *refused at startup* unless TLS is configured (or an upstream TLS terminator is trusted), and every inbound **listen** type — MLLP, HTTP, diff --git a/assets/docs/_md/HA-Reference-Architecture.md b/assets/docs/_md/HA-Reference-Architecture.md index 95fc8c5..a6f78b2 100644 --- a/assets/docs/_md/HA-Reference-Architecture.md +++ b/assets/docs/_md/HA-Reference-Architecture.md @@ -86,7 +86,7 @@ Four points belong in the design rather than the runbook: - **Synchronous commit belongs inside one site.** The staged pipeline makes several durable commits per message, and within an ordered lane they run serially — so every millisecond added to the commit round trip is paid several times over, per message. A synchronous replica across a WAN link multiplies the link's round trip into each of those commits. Keep synchronous replicas local, keep the remote replica asynchronous, and accept a small, *measured* recovery-point objective there instead. - **If the database tier is a Windows failover cluster, quorum comes back — theirs, not ours.** An availability-group design wants an odd vote count with a witness outside both data centres, and zero votes at the recovery site so a flaky link can never cost the primary site its quorum. The engine has no dependency on any of it. -- **Cross-subnet listeners need DNS-side care.** The store's connection string is a fixed keyword list, so the engine cannot emit the multi-subnet-failover hint an availability-group client normally would. Configure the listener to register only the active subnet's address with a short record TTL — the documented workaround — and drill the cross-site connect path. An opt-in setting for the hint is on the backlog, not shipped. +- **Cross-subnet listeners need DNS-side care.** The store connection takes one Always On-aware ODBC keyword, and it has shipped: `[store].multi_subnet_failover` emits `MultiSubnetFailover=Yes`, so the driver races the subnets instead of serially waiting each one out. It is **opt-in and defaults to off**, it has not yet been exercised against a live cross-subnet availability group, and it covers the *store* connection only — the separate database-lookup connector builds its own connection string and does not emit it. So set the keyword *and* configure the listener to register only the active subnet's address with a short record TTL, then drill the cross-site connect path. - **Backups and restores are yours.** On a server database the engine's DR backup covers **configuration only**; restoring the database itself is delegated to the DBA. Promotion at the recovery site is fail-closed on that: it requires an explicit per-activation attestation that the database was restored, plus a live provenance check on the restored store, and refuses otherwise. ## Third tier — the DR standby diff --git a/assets/docs/_md/Install-Guide.md b/assets/docs/_md/Install-Guide.md index 196e85f..1b9ea83 100644 --- a/assets/docs/_md/Install-Guide.md +++ b/assets/docs/_md/Install-Guide.md @@ -63,7 +63,8 @@ pip install "messagefoundry==0.3.2" # pin the exact version (core runti Add extras only for what a host actually runs — `messagefoundry[postgres]` (PostgreSQL store), `messagefoundry[sqlserver]` (SQL Server store + the DATABASE connectors, needs OS-level ODBC Driver 18), -`messagefoundry[harness]` (the PySide6 test harness GUI), `messagefoundry[sftp]` (SFTP connectors), +`messagefoundry[harness]` (PySide6 only — the harness itself is the separate `messagefoundry-harness` +distribution), `messagefoundry[sftp]` (SFTP connectors), `messagefoundry[fhir]` (FHIR codec + FHIR outbound), `messagefoundry[dicom]` (DICOM C-STORE SCP + codec). Key properties this install model gives you: @@ -265,7 +266,9 @@ explicitly — a default-on console on an exposed bind quietly degrades to the J and it additionally requires TLS, plus `[security].web_console_public_address` behind a declared TLS-terminating proxy (see [REMOTE-CONSOLE.md](REMOTE-CONSOLE.md)). The former PySide6 desktop console was retired in favour of this browser console (BACKLOG #103); PySide6 now backs only the standalone -test harness (`pip install "messagefoundry[harness]"`). +test harness, which ships as its own lockstep distribution — `pip install messagefoundry-harness`, +then `python -m harness`. (The engine wheel does not contain `harness/`; the `messagefoundry[harness]` +extra supplies only PySide6, and the harness distribution pulls it in for you.) --- diff --git a/assets/docs/_md/Remote-Console-Customer-Guide.md b/assets/docs/_md/Remote-Console-Customer-Guide.md index 0406a58..e29bc81 100644 --- a/assets/docs/_md/Remote-Console-Customer-Guide.md +++ b/assets/docs/_md/Remote-Console-Customer-Guide.md @@ -19,9 +19,11 @@ No code changes are required; everything here is configuration. ## What you're setting up -The MessageFoundry **engine** runs on a server (usually as a Windows service). The **console** is a -separate desktop application that monitors and operates the engine. By default the engine only -accepts connections from the **same machine** (`127.0.0.1`), so the console must run on the server. +The MessageFoundry **engine** runs on a server (usually as a Windows service). Its operator UI is the +**web console** — a browser application the engine serves same-origin at `/ui`, on by default at a +loopback bind, shipping as a separate distribution (`pip install messagefoundry-webconsole`). There is +no desktop application to install. By default the engine only accepts connections from the **same +machine** (`127.0.0.1`), so the console is reachable only from the server until you expose it. To use the console from another PC, you: diff --git a/assets/docs/_md/Security-Overview.md b/assets/docs/_md/Security-Overview.md index 2c35ecc..cf73f60 100644 --- a/assets/docs/_md/Security-Overview.md +++ b/assets/docs/_md/Security-Overview.md @@ -466,9 +466,13 @@ guarantees. The table below summarizes the posture per interface. | **Database egress** | Connection-string injection, MITM | Parameterized queries, ODBC injection guard, fail-closed TLS verification | | **Audit / log forwarding** | On-host tampering, PHI leakage in transit | Hash-chained tamper-evidence (HMAC-keyed once a store key is set; unkeyed SHA-256 on a keyless store), off-box forwarding for an independent copy that also covers a deleted tail, native TLS transport, redaction applied to the forwarded stream | -An **inbound web-service listener** — a partner calling *into* MessageFoundry over HTTP — is -deliberately **not built today**; it is a distinct surface that will need its own -authentication and TLS design before it ships. +An **inbound web-service listener** — a partner `POST`ing into MessageFoundry over HTTP — **is +built** ([ADR 0023](adr/0023-inbound-http-listener.md)): a connector-owned HTTP/1.1 receiver with +per-connection TLS, opt-in mutual TLS, peer-IP allow-listing, connection/body/header/slow-loris caps, +and the same off-loopback cleartext refusal every other listener carries. What is still deferred is +narrower and worth stating plainly: **there is no on-socket partner authentication on the `POST` +itself** — front it with a proxy or mutual TLS, or keep it on a trusted segment — and the synchronous +SOAP-envelope reply is not implemented. ## Verification posture — self-assessed, not certified @@ -485,9 +489,11 @@ conducted **by the project, on itself**. It is **not** a certification, an accre passed audit, or a third-party penetration test, and it should not be read as one. **No independent security assessment, code review, or DAST engagement has been performed.** ASVS at Level 3 *recommends* — but does not require — an independent review; ours is scheduled -rather than done. A dated risk acceptance covers that gap while the project is pre-1.0 and -loopback-only, and an **independent engagement is a documented prerequisite before any -off-loopback or production PHI exposure**. We publish no numeric score here, because a score +rather than done. A dated risk acceptance covers that gap while the project is pre-1.0. Because +MessageFoundry is **self-hosted**, the decision to deploy it beyond loopback — and the assessment +that justifies that decision — rest with the **implementing organization**: we state what has and +has not been independently verified rather than gating your deployment on it, and we would expect an +adopter to weigh an independent engagement before a production PHI exposure. We publish no numeric score here, because a score without an assessor's name behind it invites exactly the misreading we are trying to avoid; the current assessment and the risk register are available to adopters, evaluators, and security reviewers under NDA through the private contact route. diff --git a/assets/docs/_md/Users-and-Security.md b/assets/docs/_md/Users-and-Security.md index 32c73e9..a0a4d1b 100644 --- a/assets/docs/_md/Users-and-Security.md +++ b/assets/docs/_md/Users-and-Security.md @@ -334,7 +334,7 @@ Automated security scanning runs in continuous integration: **Every release publishes a verifiable supply chain**, not a questionnaire answer: a **CycloneDX SBOM** of the engine, an **OpenVEX** document carrying our per-CVE exploitability assessments, **Sigstore signatures** over the wheel, sdist, SBOM and VEX, **SLSA build provenance** binding each artifact to its source commit, and PyPI-side PEP 740 attestations. Feed the SBOM and VEX to your own scanner and you triage real risk rather than unreachable CVEs. Verification commands are in the [supply-chain guide](https://github.com/MEFORORG/MessageFoundry/blob/main/docs/SUPPLY-CHAIN.md). -A private vulnerability-disclosure policy is published with the project. A full-history secret scan to complement push-protection secret scanning is a tracked CI addition. +A private vulnerability-disclosure policy is published with the project. A **full-history secret scan** (`gitleaks`, over the complete git history rather than just the tip) runs as a **blocking** CI gate alongside GitHub-native push protection, and the same scanner is available as a pre-commit hook so a credential is caught before it is ever committed. ### Dependency lockfile diff --git a/assets/docs/_md/Windows-Service.md b/assets/docs/_md/Windows-Service.md index 7619273..67843c1 100644 --- a/assets/docs/_md/Windows-Service.md +++ b/assets/docs/_md/Windows-Service.md @@ -366,9 +366,12 @@ The engine logs to stdout/stderr with a stdlib `logging` setup (one timestamped stream — see [`messagefoundry/logging_setup.py`](../messagefoundry/logging_setup.py)), with a CR/LF log-injection filter and a `safe_exc()` PHI-redaction chokepoint on the exception path (WP-6c — see [PHI.md §7](PHI.md#7-logging--phi-redaction)). NSSM captures -those streams to the files above and rotates them at ~10 MB. Structured (JSON) logging -+ off-box (syslog/SIEM) forwarding are planned (bundled with off-box exposure); until -then **avoid raising the level to `DEBUG` in production**, since verbose output may +those streams to the files above and rotates them at ~10 MB. For structured stdout set +`[logging].format = "json"` (one JSON object per line); to ship a copy off-box to a +syslog/SIEM collector set `[logging].forward_host`, and `forward_protocol = "tls"` for a +native RFC 5425 encrypted hop, which also needs `forward_tls_ca_file` (a PEM trust anchor +for the collector). PHI redaction and control-char scrubbing apply to both sinks. Still +**avoid raising the level to `DEBUG` in production**, since verbose output may include message content. **Restrict the log directory's ACL** so the captured stdout/stderr (operational data,