From 50563607c26dd11ba9d37a69a07dedc33e480e02 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 6 Jul 2026 21:53:15 -0600 Subject: [PATCH 1/2] Add deployment security & compliance docs (SECURITY.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the guidance that recurs in security questionnaires and compliance reviews, closing the gaps from the Symphony + WAF security review (#13): 1. Private key handling — inline vs by-path sources, in-memory Zeroizing, on-disk file permission/ownership baseline, and live file-watch rotation. 2. Config-file integrity — the hot-reloaded config is a traffic-control surface; recommend restrictive perms + file-integrity monitoring. 3. MD5/JA3 rationale — MD5 appears only because JA3 is defined over it; it is a non-cryptographic identity label, not an integrity/auth use. Includes a ready questionnaire answer. JA4 (truncated SHA-256) noted as the successor. 4. TLS parameters attestation (plan-for-later) — current rustls 0.23 + ring provider / TLS 1.2+1.3 state, PCI DSS 12.3.3 reporting need, and the aws-lc-rs FIPS provider-swap path (incl. the ring/md-5 call sites to move). 5. DDoS shared-responsibility statement — L3/L4 volumetric is upstream's; Symphony provides connection-level controls. Includes a questionnaire answer. Items 1-3 are documentation; 4-5 are design notes to keep options open. Linked from README. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 11 ++++ SECURITY.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index 1a3c33b..2a0f3b6 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,17 @@ Protection config is per-listener and not currently hot-swappable via `updateCon --- +## Security & compliance + +For deployment hardening and security-questionnaire guidance — private key file +handling and rotation, protecting the (hot-reloaded) config file with restrictive +permissions and file-integrity monitoring, why an MD5 dependency appears (JA3 +fingerprinting, not a cryptographic use), the current TLS parameters and FIPS/PCI +attestation roadmap, and the shared-responsibility split for DDoS — see +[SECURITY.md](./SECURITY.md). + +--- + ## Metrics & monitoring ```typescript diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..497228e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,174 @@ +# Symphony — deployment security & compliance notes + +This document gives operators and reviewers the guidance that keeps coming up in +security questionnaires and compliance reviews: how Symphony handles private keys, +how to protect its configuration, why it links MD5, and what its TLS story is today +versus what is planned. It is deployment guidance, not a control catalog — Symphony +is an L4/TLS termination proxy, and the items below describe how to run it safely and +how to answer common questions about it. + +--- + +## 1. Private key handling + +Symphony never generates, stores, or manages key material of its own. Every private +key is supplied by the operator, either: + +- **inline** as a PEM string/`Buffer` in `CertConfig` (`certChain` / `privateKey`), or +- **by path** in the standalone `symphony-server` config + (`certChainFile` / `privateKeyFile`), resolved by the server before handing inline + PEM to the native layer. + +**In memory.** Once parsed, a private key lives only inside the rustls `ServerConfig` +for the routes that use it. The PEM buffer Symphony holds while building that config is +wrapped in `Zeroizing` (RustCrypto) and is zeroed on drop, so the raw PEM is not left +lingering in freed heap after the `ServerConfig` is built. Symphony does not log key +material and does not write keys back to disk. + +**On disk (operator responsibility).** Key files are only as protected as the operator +makes them. Recommended baseline: + +| Path | Owner | Mode | Notes | +|---|---|---|---| +| Private key files (`*-key.pem`) | the Symphony service user | `0600` | Never group/world readable. | +| Cert chain files (`*.pem`) | the Symphony service user | `0644` | Public material; readable is fine. | +| Directory holding keys | the Symphony service user | `0700` | Prevents directory listing / traversal by other users. | + +- Run `symphony-server` as a dedicated **non-root** service user and grant that user + ownership of the key directory. Only privileged ports (`:80`/`:443`) need elevated + capability — prefer `CAP_NET_BIND_SERVICE` (e.g. systemd `AmbientCapabilities`) over + running the whole process as root. +- Keep keys off shared/backed-up volumes unless those backups are themselves encrypted + and access-controlled. + +**Rotation.** `symphony-server` watches the cert/key files referenced by its config and +hot-reloads an in-place or renamed rotation live — no config write, no restart, no +dropped connections (see the cert-rotation notes in `CLAUDE.md`). To rotate a key: +replace the key and matching chain on disk (atomic rename preferred so a reader never +sees a half-written pair), and Symphony picks them up on the next debounced reconcile. +During the window between a new chain and its new key, the previous cert continues to +serve for that SNI (per-route cert failures are isolated and the last-good route is +carried forward), so a mismatched mid-rotation pair degrades one route briefly rather +than aborting the port set. Kubernetes projected-volume (`..data` symlink swap) +rotation is **not** yet covered by the file watcher. + +> KMS/HSM-backed keys are intentionally out of scope today. Symphony consumes PEM only; +> if a hardware-custody requirement appears, it would be a new key-source integration, +> not a change to the handling above. + +--- + +## 2. Configuration file integrity + +The standalone `symphony-server` **watches its config file and hot-reloads on change**. +That is a deliberate operational feature (live route/cert updates), but it also means +that **anyone who can write the config file can redirect or intercept traffic** — change +an upstream, swap a cert, or drop a route — without restarting the process. The config +file is a traffic-control surface and should be protected accordingly. + +Recommended controls: + +- **Restrictive permissions.** Config file `0600` (or `0640` with a trusted operator + group), owned by the Symphony service user; containing directory `0700`. No + world-writable paths anywhere in the resolved config or cert/key paths. +- **File-integrity monitoring (FIM).** Put the config file — and the cert/key directory — + under a FIM tool (AIDE, Tripwire, `auditd` watches, or the platform's equivalent) so + an unexpected modification raises an alert. Because reloads are silent and + connection-preserving, FIM is the primary detection for unauthorized config change. +- **Provenance.** Prefer generating the config from a controlled source (host-manager, + config management, or an image build) rather than hand-editing in place, so the + authoritative copy is versioned and any on-host drift is visible. +- **`status.json`.** `symphony-server` writes `status.json` (pid, version, bound ports) + for supervisors. Treat it as attacker-influenceable state-out, not trusted input — + it should not be writable by anyone who shouldn't already control the service. + +--- + +## 3. MD5 in JA3 — why the dependency scanner flag is a false positive + +Symphony computes a **JA3** TLS client fingerprint in the ClientHello peek path +(`src/sni.rs`), and JA3 is *defined* by its spec as the **MD5** of a canonicalized +ClientHello string. That is the only reason an MD5 implementation appears in the +dependency tree. + +**This is not a cryptographic use of MD5.** JA3 uses MD5 purely as a fixed-length +*label* to identify and correlate a client's TLS stack. It is not used for integrity, +authentication, key derivation, signatures, or confidentiality — nothing about +Symphony's security depends on MD5 being collision- or preimage-resistant. An attacker +who could forge an MD5 collision would gain nothing except the ability to make two +different ClientHellos share a fingerprint, which is already possible by other means and +is exactly why JA3 is treated as a coarse signal, not an authorization primitive. + +Because of that coarseness (Chrome and other Chromium browsers randomize ClientHello +extension order, so one client emits many JA3 hashes), Symphony also computes **JA4**, +the randomization-resistant successor, which uses a **truncated SHA-256** — again as an +identity label, not a security primitive. Both fingerprints feed optional blocklists +(`ja3Blocklist` / `ja4Blocklist`); neither is a trust boundary. + +**Questionnaire answer:** *Symphony links an MD5 implementation solely because the JA3 +TLS-fingerprint standard is defined over MD5. MD5 is used as a non-cryptographic +identity/labeling hash for client fingerprinting and blocklist matching. It is not used +for any integrity, authentication, signing, or confidentiality function, so the standard +MD5-weakness findings do not apply.* + +--- + +## 4. TLS parameter attestation (planned) + +**Today.** Symphony terminates TLS with **rustls 0.23** using the **`ring`** +CryptoProvider (`tls12` feature enabled), which negotiates **TLS 1.2 and TLS 1.3** with +rustls's default cipher suites and key-exchange groups. Symphony does **not** currently +expose configuration for minimum protocol version, cipher suites, or key-exchange +groups, and does **not** report the negotiated parameters through its metrics API. The +effective policy is "rustls defaults," which are modern and safe but are attested today +only by pointing at the rustls/`ring` versions in the lockfile. + +**Why this matters for compliance.** **PCI DSS 12.3.3** requires customers to formally +review, at least annually, the cryptographic cipher suites and protocols in use and +confirm none are deprecated. Meeting that cleanly means Symphony should eventually be +able to *report* its negotiated/configured TLS parameters as machine-readable evidence, +rather than the customer inferring them from dependency versions. + +**Planned direction (design note, keep options open):** + +- Add a way to **report** the active TLS configuration (min version, offered suites, + key-exchange groups) — e.g. via the metrics/status surface — so it can be attached to + an annual review as evidence. +- Consider making **minimum TLS version** (and optionally a suite policy) configurable + per listener, so operators can raise the floor to TLS 1.3 where their clients allow. +- **FIPS 140-3:** rustls supports a FIPS-validated path via the **`aws-lc-rs`** + CryptoProvider. Keeping a provider swap cheap now (avoid hard-coding `ring`-only + assumptions in new code) preserves the option for government/regulated customers + later. Note the swap is not entirely free: Symphony currently calls into `ring` + explicitly in two non-negotiation spots — the TLS session `Ticketer` and the cert + dedup SHA-256 in `src/tls.rs` — plus JA3's `md-5`; those call sites would need to move + to the chosen provider's primitives as part of any FIPS build. + +None of item 4 is implemented yet; it is recorded here so the design stays open and so +reviewers get a consistent answer about the current state versus the roadmap. + +--- + +## 5. DDoS — shared responsibility + +Symphony is **not** a volumetric DDoS mitigation layer, and deployments should not treat +it as one. + +- **Out of scope: L3/L4 volumetric attacks** (SYN floods, UDP/amplification floods, raw + bandwidth exhaustion). Absorbing these is the responsibility of the **upstream network + provider / cloud edge** (scrubbing centers, anycast, provider-level rate limiting) + that sits in front of Symphony. Symphony runs on a host and inherits that host's + network capacity; it cannot defend against traffic that saturates the link before it + arrives. +- **In scope: connection-level controls.** Once connections reach it, Symphony provides + per-IP connection rate limiting and burst caps, per-IP concurrency limits, CIDR + allow/blocklists, TLS-handshake timeouts (to shed slow-handshake abuse), `requireSni` + enforcement, and JA3/JA4 fingerprint blocking. These blunt application-adjacent and + connection-exhaustion abuse and keep one noisy source from starving others — they are + **not** a substitute for upstream volumetric protection. + +**Questionnaire answer:** *Volumetric (L3/L4) DDoS protection is a shared responsibility: +absorbing bandwidth/packet floods is handled by the upstream network/cloud provider in +front of Symphony. Symphony contributes connection-level controls — per-IP rate and +concurrency limits, CIDR filtering, handshake timeouts, SNI enforcement, and fingerprint +blocklists — to mitigate connection-exhaustion and abuse for traffic that reaches it.* From e0c708f064b242a5e302c77dcbbc261f1b40406b Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 9 Jul 2026 13:56:22 -0600 Subject: [PATCH 2/2] docs: correct SECURITY.md references (CLAUDE.md, ja4Blocklist status) (#22) --- SECURITY.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 497228e..25afc47 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -43,7 +43,8 @@ makes them. Recommended baseline: **Rotation.** `symphony-server` watches the cert/key files referenced by its config and hot-reloads an in-place or renamed rotation live — no config write, no restart, no -dropped connections (see the cert-rotation notes in `CLAUDE.md`). To rotate a key: +dropped connections (see the config/cert file watcher in `ts/server.ts` and the +per-route cert-failure isolation in `router.rs::build_route_table`). To rotate a key: replace the key and matching chain on disk (atomic rename preferred so a reader never sees a half-written pair), and Symphony picks them up on the next debounced reconcile. During the window between a new chain and its new key, the previous cert continues to @@ -100,10 +101,12 @@ different ClientHellos share a fingerprint, which is already possible by other m is exactly why JA3 is treated as a coarse signal, not an authorization primitive. Because of that coarseness (Chrome and other Chromium browsers randomize ClientHello -extension order, so one client emits many JA3 hashes), Symphony also computes **JA4**, -the randomization-resistant successor, which uses a **truncated SHA-256** — again as an -identity label, not a security primitive. Both fingerprints feed optional blocklists -(`ja3Blocklist` / `ja4Blocklist`); neither is a trust boundary. +extension order, so one client emits many JA3 hashes), **JA4** — the +randomization-resistant successor, which uses a **truncated SHA-256** — is planned as a +future identity label, not a security primitive. Today, JA3 is the only fingerprint +Symphony computes, and it feeds a single optional blocklist (`ja3Blocklist`); a +`ja4Blocklist` would follow once JA4 support ships. Neither is, nor is intended to be, a +trust boundary. **Questionnaire answer:** *Symphony links an MD5 implementation solely because the JA3 TLS-fingerprint standard is defined over MD5. MD5 is used as a non-cryptographic @@ -163,8 +166,9 @@ it as one. - **In scope: connection-level controls.** Once connections reach it, Symphony provides per-IP connection rate limiting and burst caps, per-IP concurrency limits, CIDR allow/blocklists, TLS-handshake timeouts (to shed slow-handshake abuse), `requireSni` - enforcement, and JA3/JA4 fingerprint blocking. These blunt application-adjacent and - connection-exhaustion abuse and keep one noisy source from starving others — they are + enforcement, and JA3 fingerprint blocking (JA4 planned). These blunt + application-adjacent and connection-exhaustion abuse and keep one noisy source from + starving others — they are **not** a substitute for upstream volumetric protection. **Questionnaire answer:** *Volumetric (L3/L4) DDoS protection is a shared responsibility: