Network & TLS
TLS cipher configuration, HSTS, Cache-Control policy, and why accurate client-IP recording depends on how nginx is deployed.
RedScribe terminates TLS at the nginx container in front of the app, and that’s also where a handful of other network-level protections live. This page covers what’s locked down at that layer: which TLS versions and ciphers are allowed, how HSTS and caching headers are set, and how nginx is deployed so that client IPs in login lockout and the audit log are trustworthy rather than spoofable.
TLS cipher suite
Section titled “TLS cipher suite”RedScribe’s nginx configuration restricts TLS to a strong-only setup, following the Mozilla “Intermediate” baseline (ssl-config.mozilla.org; worth rechecking against that generator occasionally rather than treating this list as permanently fixed). Concretely, that means:
- TLS 1.2 and 1.3 only. No TLS 1.0 or 1.1.
- Every allowed TLS 1.2 cipher uses forward secrecy (ECDHE or DHE key exchange) and authenticated encryption (GCM or ChaCha20-Poly1305). There’s no RC4, 3DES, export ciphers, NULL ciphers, static RSA/DH key exchange, or CBC mode anywhere in the list.
- TLS 1.3’s three built-in ciphersuites are all authenticated encryption by spec and aren’t separately configurable, so there’s no weak TLS 1.3 suite to worry about excluding.
- The server, not the client, picks which cipher to use when negotiating a connection.
- Key exchange prefers X25519 (fastest and strong) with secp384r1 as a fallback for older clients. This doesn’t yet include the newer post-quantum hybrid curve, since that needs a newer OpenSSL than the current nginx base image ships with; the base image will move to it once one’s available.
- Session tickets are turned off deliberately, not just left at a default. A single nginx instance has no way to rotate ticket-signing keys safely, and a static or long-lived one would quietly undermine the forward secrecy the cipher list above is there for. A non-ticket session cache still avoids a full handshake on reconnect, so this doesn’t cost you much.
- OCSP stapling is on, so the server proves its certificate hasn’t been revoked without the client needing a separate round trip. It’s best-effort: silently skipped if the certificate authority’s responder isn’t reachable (for example, in a fully air-gapped deployment), and it never blocks a handshake if it fails.
- The nginx version number is hidden from the
Serverresponse header and from nginx’s own error pages, so an attacker fingerprinting for a version-specific nginx CVE learns only that it’s nginx, not which version.
To check what actually negotiates against your own deployment:
openssl s_client -connect <your-domain>:443 -tls1_3 -brief </dev/nullRedScribe tells browsers to always use HTTPS for your domain, never plain HTTP, for a full year at a time, including subdomains:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadOne year is the standard minimum for eligibility on browser HSTS preload lists. This header is emitted once, by nginx, which is the single source of truth for it. It applies to every response, static assets included.
No caching of dynamic content
Section titled “No caching of dynamic content”Every dynamic response, findings, engagement detail, decrypted content,
exported reports, is sent with Cache-Control: no-store, so a browser,
intermediate proxy, or back-forward cache never retains it. Static assets
(CSS, JS, and similar files, all served with content-hashed filenames) are
deliberately exempt from this, since caching those aggressively is both
safe and the entire point of hashing their names.
Why nginx uses host networking
Section titled “Why nginx uses host networking”RedScribe’s nginx container runs with direct access to the host’s network, rather than the more typical published-port mapping. That choice exists specifically so nginx sees the real client IP for every request. See the full rationale in Docker Compose deep dive.
In short: a published-port mapping is relayed through a helper process that substitutes its own address as the connection source. Without host networking, nginx would see a Docker-internal address instead of the real client IP on every request, which would silently break both login-lockout IP tracking and audit log IP recording at the source, no matter how carefully the application code itself handled IPs.
The client-IP trust boundary
Section titled “The client-IP trust boundary”RedScribe trusts the X-Forwarded-For header for client IP, but only
because nginx is configured to always overwrite it with its own view of
the real TCP connection, never to append to whatever the client already
sent:
proxy_set_header X-Forwarded-For $remote_addr;This matters because RedScribe sits behind exactly one reverse proxy hop
(nginx itself), with nothing further upstream. If nginx instead appended
to the incoming header, an external client could set their own fake
X-Forwarded-For value ahead of the real one and have it trusted, which
would let them bypass IP-based login lockout and plant a fabricated
address in the audit log. Overwriting the header removes that option
entirely: whatever nginx sees as the real TCP peer is what gets recorded,
full stop.
Tailscale Funnel changes the trust boundary slightly
Section titled “Tailscale Funnel changes the trust boundary slightly”If you expose RedScribe beyond your tailnet with Tailscale Funnel, Funnel forwards traffic over loopback and adds its own preamble carrying the real client IP. nginx accepts that preamble, but only on a second, more specific listener bound solely to loopback, and only trusts it as the source IP for connections that land on that exact listener. Nothing reaching nginx from the public internet can use this path: only a process already running on the same host could ever connect to that listener in the first place, which is the same trust level host-level access already implies everywhere else in this stack.