None of the three is secure by default in every respect, and none is insecure by nature. The documented defaults differ. Caddy leads on automatic HTTPS and spoof resistant forwarded headers. nginx has tight body and header limits. Apache defaults to a verbose Server header and a 1 GB body limit. Know yours and set what you need.
The table
All values are from each project's own documentation.
| Topic | nginx | Apache httpd | Caddy |
|---|---|---|---|
| Version in Server header | server_tokens on by default | ServerTokens Full by default | Not covered in the pages I read |
| Client forwarded headers | You choose with realip, trust list required | ProxyAddHeaders On, existing values are appended | Ignores incoming X-Forwarded values by default |
| Upstream TLS verification | proxy_ssl_verify off by default | Defined in the mod_ssl docs, not checked here | Verifies https upstreams, insecure skip is a named option |
| Body size limit | client_max_body_size 1m | LimitRequestBody 1 GB (unlimited in 2.4.53 and earlier) | request_body max_size, no default stated |
| TLS versions | TLSv1.2 and TLSv1.3 | Not checked here | Automatic HTTPS handles certificates |
| Certificates | You provide them | You provide them | Obtained and renewed automatically |
| Open proxy risk | None unless you build one with variable proxy_pass | ProxyRequests Off by default | Not applicable by default |
| TRACE | Not in pages read | TraceEnable on by default | Not in pages read |
Blank or unchecked cells are a reminder to read the docs for your version, not a statement that nothing exists.
Reading the differences
Banners. nginx and Apache both show the version by default. Trim them, but see server_tokens myth versus fact and the Apache directives post.
Forwarded headers. Caddy ignores client supplied X-Forwarded values unless the sender is a trusted proxy. Apache sends them to the backend and, when the request already had them, comma separates the values, so a backend reading the first entry reads client input. nginx's realip module rewrites the client address only for senders you list.
Upstream TLS. nginx's proxy_ssl_verify default off is the surprise here. See the upstream TLS post.
Limits. nginx rejects large bodies at 1 MB until you raise it. Apache's 1 GB default is only a ceiling. Set your own in both.
Certificates. Caddy's automation removes a common outage cause, described in the Caddy HTTPS post. With nginx and Apache you need renewal automation, see the ACME checklist.
What to set in every case
- Short Server header.
- One clear forwarded header policy, with trusted proxies listed exactly.
- Verified upstream TLS where traffic crosses a shared network.
- Body, header and timeout limits that match your app.
- A catch-all for unknown hosts.
- Security headers on every status code.
- A patch routine for the server and its modules.
Choosing
Choose by operations, not by security slogans. A team that struggles with certificate renewal gains a lot from Caddy. A team with deep nginx experience and config management may be safer staying where its reviews are strongest. The weakest setup is the one nobody reviews.
Where Safeguard fits
Safeguard does not replace any of the three. It inventories the software around them through asset discovery, SBOM and dependency scanning, and policy gates. Try the free scan at https://gold.safeguard.sh/scan.
Quick check
Which of the three ignores client X-Forwarded headers by default?
Caddy, per its reverse_proxy documentation, unless the sender is in trusted_proxies.
Sources
- nginx core module documentation
- nginx proxy module documentation
- Apache 2.4 core directives
- Apache mod_proxy
- Caddy reverse_proxy directive
- Caddy request_body directive
Last reviewed: 19 October 2026.