NGINX sits in front of a large share of web applications and APIs as web server, reverse proxy and TLS terminator, and a few unsafe defaults or one misplaced directive can expose files, leak versions, or send traffic to backends without verifying who they are. This guide sets out a hardening baseline for open source NGINX and notes where NGINX Plus and Kubernetes ingress controllers differ. It covers who can change the configuration, how NGINX authenticates clients and upstreams, the secrets it holds, logging and detection, and a directive-level baseline taken from the official nginx.org documentation. It is for platform engineers, web operations teams and application security staff, and it builds on the Proxies and Forward vs Reverse Proxy Security Guide rather than repeating it.
Key takeaways
- The configuration is the attack surface. NGINX has no admin console in the open source build: whoever can write to the configuration directory and run a reload controls every site it serves.
- Upstream TLS is not verified by default.
proxy_ssl_verifydefaults tooff, so a reverse proxy that re-encrypts to backends accepts any certificate unless you turn verification on. - Modern defaults help, explicit settings help more. Since 1.27.3 the default
ssl_protocolsis TLSv1.2 and TLSv1.3, but older builds and copied snippets still enable TLSv1 and TLSv1.1. - Private keys and status endpoints need protecting. TLS keys, htpasswd files, upstream client certificates,
stub_statusand the NGINX Plus API are high-value targets and must be restricted. - Ingress controllers are a different risk class. CVE-2025-1974 in the community ingress-nginx controller (CVSS 9.8) showed that a controller holding cluster-wide Secret access turns a configuration injection into cluster compromise, and that project's maintenance ended in March 2026.
How identity and access work in NGINX
NGINX makes three kinds of trust decision: who may change it, which clients may reach a resource, and which upstream servers it will trust. It has no user database of its own. Access decisions come from modules in the configuration.
| Mechanism | Module and directives | Use it for | Notes from the documentation |
|---|---|---|---|
| Address rules | allow, deny (access module) | Admin paths, status pages, internal APIs | Evaluated in order; end with deny all |
| HTTP Basic authentication | auth_basic, auth_basic_user_file | Small internal tools only | Supports crypt, apr1 and {SSHA}; the docs say not to use PLAIN or unsalted SHA. Only over HTTPS |
| External authorisation | auth_request, auth_request_set | Delegating to an SSO or OAuth proxy | Subrequest 2xx allows, 401 or 403 denies; not built by default (--with-http_auth_request_module) |
| Client certificates | ssl_verify_client, ssl_client_certificate | Service-to-service and device access | See the mTLS Guide |
| JWT and OIDC | auth_jwt, auth_oidc | Token validation at the edge | NGINX Plus modules; the satisfy directive lists them alongside the open source access modules |
| Method restriction | limit_except | Read-only endpoints | limit_except GET also permits HEAD |
| Combining rules | satisfy all | any | Address plus password, or either | Default all. any lets a request pass if one module allows it, a common cause of accidental exposure |
Use internal on locations that should only be reached by internal redirects, such as error pages or files served through X-Accel-Redirect: the core module documentation says external requests to them return 404.
Admin roles and privileged access
In open source NGINX, administration means file system and process control. The master process normally starts as root to bind ports 80 and 443 and read keys, then worker processes run as the unprivileged account named in the user directive. Privilege therefore sits with whoever can edit nginx.conf and its included files, write to the document root, replace the binary or modules, or run nginx -s reload.
- Separate config writers from operators: Keep configuration in version control and deploy it through a pipeline that runs
nginx -tbefore a reload. Interactive root access to production web servers should be the exception, logged through your privileged access tooling. See the Privileged Access Workstations Guide. - File ownership: Configuration, keys and htpasswd files owned by root and not writable by the worker user; document roots not writable by the worker user unless an upload path genuinely needs it, and then served with no script execution.
- Dynamic modules:
load_moduleloads shared objects into the server. Restrict the modules directory to root and only load modules you have reviewed. - NGINX Plus API: The commercial
apimodule withwrite=onaccepts POST, PATCH and DELETE to add or remove upstream servers and change key-value zones. That is a live control plane. The documentation's example binds it to127.0.0.1withallow 127.0.0.1; deny all;; keep it on loopback or a management interface and put authentication in front of it. - Management tools and agents: Central management agents and dashboards that push configuration hold the same power as root on every instance. Treat their credentials and service accounts as privileged.
Integrations, API tokens and service accounts
NGINX holds and passes machine credentials that are easy to overlook in an access review.
| Credential | Where it lives | Control |
|---|---|---|
| TLS private keys | ssl_certificate_key files | The nginx.org HTTPS guide says keys need restricted access but must be readable by the master process. Root-owned, mode 600 or 400, automated renewal. See the ACME and Automated Certificate Management Guide |
| Upstream client certificate | proxy_ssl_certificate, proxy_ssl_certificate_key | Unique per proxy tier, short-lived, rotated with the backend trust store |
| Static API keys injected into upstream requests | proxy_set_header Authorization ... in configuration | Avoid secrets in config files; if unavoidable, keep them in an included file with restricted permissions, sourced from a secrets manager at deploy time. See the Secrets Management Guide |
| htpasswd entries | auth_basic_user_file | Salted hashes only; move to SSO through auth_request where users are people |
| Ingress controller service account | Kubernetes RBAC for the controller pod | Limit Secret read to namespaces it serves where the controller supports it. See the Kubernetes RBAC Guide |
| Session ticket keys | ssl_session_ticket_keys files | Rotate regularly or disable tickets; a stolen ticket key undermines forward secrecy for resumed sessions |
When NGINX authenticates users through auth_request and passes identity to backends with auth_request_set and proxy_set_header, the backend must accept those headers only from the proxy. Strip any client-supplied header with the same name, and lock the backend to the proxy's address or to mTLS, otherwise a client can forge identity by calling the backend directly.
Kubernetes ingress
The NVD record for CVE-2025-1974 says an unauthenticated attacker with access to the pod network could run arbitrary code in the ingress-nginx controller and read the Secrets it can access, which by default means all Secrets cluster-wide. The Kubernetes project released fixes in ingress-nginx 1.12.1 and 1.11.5 on 24 March 2025, and later announced that best-effort maintenance of the community Ingress NGINX controller would end in March 2026, after which there are no releases, bug fixes or security fixes. Confirm which controller you run (the community project is distinct from other NGINX-based controllers), plan migration to a maintained controller or the Gateway API, restrict network access to admission webhooks, and narrow the controller's Secret access. See the Kubernetes Security Guide and the Kubernetes Admission Control Guide.
Logging and detection
- Structured access logs: The predefined
combinedformat logs client address, user, time, request line, status, bytes, referrer and user agent. Add$request_time,$upstream_addr,$upstream_status,$ssl_protocol,$ssl_cipherand a request ID, and uselog_format ... escape=json(since 1.11.8) so fields parse reliably in the SIEM. - Ship off the host:
access_log syslog:server=...or a log agent sends logs off-box, so an attacker with server access cannot quietly edit them. - Real client address: Behind a load balancer, use the realip module with
set_real_ip_fromlimited to the load balancer ranges; trustingX-Forwarded-Forfrom anywhere lets clients spoof their address in logs and access rules. - Error log: Keep
error_logatwarnorerrorin production and collect it: upstream TLS failures,limit_reqrejections and permission errors appear there. - Rate limit telemetry:
limit_req_dry_run on(since 1.17.1) and the$limit_req_statusvariable let you measure what a limit would reject before enforcing it.
What to alert on
- Requests for dotfiles, backups and path traversal sequences that return 200 (for example
/.git/,/.env,..after a static prefix). - Spikes of 401, 403, 413, 429 or 503 from one source, which indicate credential guessing or limit testing.
- Requests to
stub_status, the Plus API or admin paths from outside the allowed ranges. - Configuration file changes and reloads outside the deployment pipeline, detected by file integrity monitoring on
/etc/nginx. - New executable files in document roots, a sign of a web shell (ATT&CK T1505.003).
Detection content for these logs is in the Web Server Access Log Detection Guide.
Metrics
- Baseline compliance: Percentage of instances passing an automated check of the baseline below.
- Patch latency: Days from an nginx.org security advisory to deployment of the fixed release.
- Upstream verification: Percentage of HTTPS upstreams with
proxy_ssl_verify on. - Exposed internals: Count of status, API or admin locations reachable from the internet (target zero).
- Out-of-pipeline changes: Number of configuration changes not traced to a reviewed commit.
Hardening baseline
| Area | Directive or action | Default | Recommended |
|---|---|---|---|
| Version disclosure | server_tokens | on | off |
| Client TLS protocols | ssl_protocols | TLSv1.2 TLSv1.3 from 1.27.3; earlier releases also enabled TLSv1 and TLSv1.1 | Set TLSv1.2 TLSv1.3 explicitly, or TLSv1.3 only where all clients support it. See the TLS Server Configuration Baseline Guide |
| Ciphers | ssl_ciphers | HIGH:!aNULL:!MD5 | Restrict TLS 1.2 to forward-secret AEAD suites |
| Upstream TLS verification | proxy_ssl_verify, proxy_ssl_trusted_certificate, proxy_ssl_server_name | off, none, off | on, internal CA bundle, on |
| Request body size | client_max_body_size | 1m | Smallest value each location needs; never 0 (unlimited) globally |
| Slow client timeouts | client_header_timeout, client_body_timeout, send_timeout, keepalive_timeout | 60s, 60s, 60s, 75s | Lower (for example 10 to 20 seconds) on internet-facing servers to resist slow-request exhaustion |
| Header buffers | large_client_header_buffers | 4 8k | Keep defaults unless an application needs more |
| Rate limiting | limit_req_zone $binary_remote_addr ..., limit_req, limit_req_status 429 | None; rejections return 503 | Limits on login, search and API paths. See the Rate Limiting and API Abuse Guide |
| Symlinks | disable_symlinks | off | if_not_owner or on on shared hosting and upload paths |
| Directory listing | autoindex | off | Keep off |
| Methods | limit_except | All methods | Only the methods each location needs |
| Status and API | stub_status, api | Not built by default; Plus API read-only unless write=on | Loopback or management network only, with allow/deny |
| Response headers | add_header ... always | None | HSTS, content type options and framing controls. See the Security Headers Guide |
Rollout, testing and rollback
- Inventory: List every NGINX instance, version, build options (
nginx -Vshows compiled modules) and role (web server, reverse proxy, ingress, API gateway). - Patch first: Move to a currently supported release. nginx.org publishes security advisories with fixed versions on both current release branches, so track that page.
- Codify the baseline: Put the directives above in a shared included file, keep site files small, and lint every change with
nginx -tin CI. - Test in report mode: Enable new rate limits with
limit_req_dry_run on, scan TLS with a client test tool, and turn onproxy_ssl_verifyin staging to find backends with bad certificates. - Canary: Deploy to one instance or a small share of traffic, watch error rates and 4xx and 5xx trends for a day, then roll out in rings.
- Rollback: Keep the previous configuration commit deployable; a failed reload leaves the old workers running, and reverting the commit plus a reload restores service.
Choosing between options
- Basic auth or auth_request: Basic auth is acceptable for a single internal tool with a handful of users over HTTPS; anything used by staff at scale should use
auth_requestto an SSO proxy so MFA, joiner and leaver processes apply. - TLS at NGINX or end to end: Terminating at NGINX and re-encrypting with verified upstream TLS suits most estates; mTLS to backends is worth the certificate effort where backends are shared or in another trust zone.
- Small versus large estates: A small team can manage one hardened include file by hand with
nginx -t; large estates need configuration generated from templates, policy checks in CI and drift detection on every host.
Framework mapping
| Area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|
| Secure baseline and least functionality | CM-6, CM-7 | PR.PS-01 | 8.9 Configuration management |
| Client and upstream TLS | SC-8 | PR.DS-02 | 8.24 Use of cryptography |
| Access control on locations | AC-3 | PR.AA-05 | 8.3 Information access restriction |
| Rate limits and timeouts | SC-5 | PR.IR-01 | 8.20 Networks security |
| Patching | SI-2 | ID.RA-01 | 8.8 Management of technical vulnerabilities |
| Logging | AU-12 | DE.CM-09 | 8.15 Logging |
Relevant ATT&CK techniques: T1190 Exploit Public-Facing Application, T1505.003 Web Shell, T1499.002 Service Exhaustion Flood and T1592 Gather Victim Host Information (version banners).
Common misconfigurations
- Alias without matching slashes: The core documentation advises keeping location and
aliaspaths consistent and preferringrootwhere the location matches the end of the path. A pattern such aslocation /staticwithalias /var/www/static/;can let requests like/static../reach the parent directory. See the Path Traversal Guide. - Re-encrypting without verifying:
proxy_pass https://with the defaultproxy_ssl_verify offgives the appearance of encrypted backends with no protection against a spoofed upstream (ATT&CK T1557 Adversary-in-the-Middle). satisfy anyby accident: Combining an IP allow list with Basic auth undersatisfy anyopens the location to anyone with a password from anywhere, or to anyone on the allowed network without one.- Trusting forwarded headers:
set_real_ip_from 0.0.0.0/0or passing client-supplied identity headers to backends lets attackers bypass address rules or impersonate users. - Exposed internals:
stub_status, the Plus API,.gitdirectories or backup files served from the document root because no location blocks them. - Unlimited bodies and long timeouts:
client_max_body_size 0and default 60-second timeouts on internet-facing listeners make resource exhaustion cheap. Pair server limits with upstream protection from the DDoS Protection Guide. - Unmaintained ingress controller: Running a controller whose project no longer ships security fixes, with cluster-wide Secret read and an admission webhook reachable from every pod.
Practitioner checklist
- Inventory every NGINX instance and ingress controller with version, modules and role, and patch to a supported release.
- Restrict configuration, key and htpasswd files to root, deploy configuration only through a pipeline that runs
nginx -t. - Set
server_tokens off, explicitssl_protocols TLSv1.2 TLSv1.3and a forward-secret cipher list. - Turn on
proxy_ssl_verifywith a trusted CA bundle andproxy_ssl_server_name onfor every HTTPS upstream. - Bind
stub_statusand the Plus API to loopback or a management network withallow/deny. - Replace Basic auth for staff applications with
auth_requestto SSO, and strip client-supplied identity headers. - Set body size limits, shorter client timeouts and
limit_reqon login and API paths, tested first in dry-run mode. - Ship JSON-escaped access and error logs off the host and alert on traversal, status page access and out-of-pipeline reloads.
- Confirm which Kubernetes ingress controller you run, plan migration off the retired community controller, and narrow its Secret access.
Standards and references
- nginx.org: Configuring HTTPS servers
- nginx.org: Module ngx_http_core_module
- nginx.org: Module ngx_http_proxy_module
- nginx.org: Module ngx_http_auth_request_module
- nginx.org: nginx security advisories
- NIST NVD: CVE-2025-1974 Detail
- Kubernetes: Ingress NGINX Retirement, What You Need to Know
- CIS: NGINX Benchmark
Related NHI Mgmt Group resources: Proxies and Forward vs Reverse Proxy Security Guide · Apache HTTP Server Hardening Guide · TLS Server Configuration Baseline Guide · Web Server Access Log Detection Guide · Kubernetes Security Guide