Internal services & service meshes
Most DaloyJS security defaults — body limits, request timeouts, JWT algorithm allowlists, timingSafeEqual credential checks, prototype-pollution-safe parsers, fetchGuard() SSRF defaults, schema strictness, RFC 9457 problem+json with prod-mode redaction — apply just as much to a service running behind a service mesh as to one facing the public internet. A compromised neighbour, an SSRF in another pod, or a leaked internal token will exercise those guards identically.
A small subset of defaults, however, only make sense at a browser-facing boundary: HSTS, CSP, X-Frame-Options, the cross-origin write guard, and the session+state-changing-route CSRF boot guard. When TLS is terminated by the mesh, there is no browser to read those headers, and no Origin header to compare. Forcing them on at the app layer adds noise without adding safety.
For these deployments, use preset: "internal-service". It turns off the topology-dependent guards and keeps everything else on.
One line to switch posture
Per-knob options you pass alongside the preset still win. If you want headers back on a single internal service that proxies to a browser, set secureHeaders: {} explicitly — the preset will not overwrite an explicit value.
What the preset turns OFF
secureHeadersauto-install — HSTS, CSP, X-Frame-Options, COOP / CORP. No browser to read them.corsCrossOriginGuard— rejects state-changing requests carrying a cross-originOriginheader. Service-to-service callers do not sendOrigin.csrfboot guard — refuses to start whensession()is registered alongside a state-changing route withoutcsrf(). Internal callers authenticate with bearer tokens or mTLS, not cookies.- unconfigured
X-Forwarded-*guard — the first-request 500 whentrustProxy/behindProxyis unset. The mesh terminates TLS and the immediate peer inside the mesh is the caller.
What the preset KEEPS on (non-negotiable)
Everything that protects the service itself from malformed input, confused dependencies, compromised callers, or operational mistakes stays on. The preset does not weaken any of:
bodyLimitBytes(1 MiB default),requestTimeoutMs(30 s default)- JWT algorithm allowlist +
timingSafeEqualcredential comparison - Prototype-pollution-safe parsers +
isForbiddenObjectKey fetchGuard()SSRF defaults (still blocks169.254.169.254)- Weak session secret refuse-to-boot
cors({ origin: '*' })refuse-to-boot- Anonymous stateful plugin refuse-to-boot
- RFC 9457 problem+json with prod-mode redaction
stripServerHeaders(removesServerandX-Powered-By)- Schema
.strict()and response validation when enabled crashOnUnhandledRejection(still on by default in production)
The threat model behind the preset
"Behind a firewall" is a weaker guarantee than it used to be. Internal services are still reachable through SSRF from a compromised workload, leaked VPN access, accidentally-permissive ingress rules, dev tunnels, port-forwards, CI jobs, and other internal services gone bad. The Aikido / Supabase Secure-by-Default Developmentwrite-up puts the underlying risk plainly: "If you tell an AI to make something work, it might remove the very security checks that protect you."
The preset is the answer to that risk for service-to-service deployments: do not remove the guards — name the topology once, audit which guards stayed on, and keep everything else. That is closer to the config.force_ssl / SECURE_*settings shape used by Rails and Django than to a master "disable everything" switch.
Boot audit log
Whenever the preset is applied, the framework emits a one-time info log under event: "security.preset.applied" naming the preset, the guards that were disabled, the guards that stayed on, and the list of fields the caller overrode explicitly. Operators can grep for it in centralized logs without reading the app source.
Introspecting the live posture
app.getSecurityPosture() returns a frozen snapshot of the resolved security configuration — useful for an internal /__security route, CI audits, or a custom dashboard:
When NOT to use the preset
- The service is reachable from a browser, even indirectly (BFF pattern, admin UI, embedded widgets). Use the default posture and add
cors()per route. - The service is exposed directly to the public internet without a mesh / WAF / TLS terminator in front. Use the default posture.
- You only need to disable a single guard. Prefer the per-knob option (
secureHeaders: false,corsCrossOriginGuard: false,csrf: "off") so the rest of the posture stays explicit at the call site.
Related
- Secure-by-default — the full list of defaults the framework ships.
secureDefaultsenforcement — the wholesale escape hatch (refuses-to-boot in production without explicit acknowledgement). Prefer the topology preset where possible.fetchGuard()SSRF defaults — still active under the preset.