A scanner, pentest, or audit just flagged your Apache server: /server-status is reachable from the internet. mod_status renders a live view of your server’s internals, and anyone who can load it can watch your traffic in near real time.
The operational problem is that mod_status is also the best monitoring source Apache has. BusyWorkers, the scoreboard, requests per second: all of it comes from /server-status?auto. So the fix is not “disable mod_status.” The fix is to restrict who can reach it, verify the restriction holds on every vhost, and keep scraping it locally.
What this means
With ExtendedStatus on (the default since Apache 2.3.6 whenever mod_status is loaded), the status page shows every active worker slot: client IP, vhost, and the full request URI including the query string. It also shows server version and build, uptime, MPM, and CPU load. /server-info, if enabled, goes further and dumps the effective configuration: loaded modules, directives, document roots, file paths.
What an outsider gets for free:
- Request URIs with query strings. Session tokens, password-reset links, API keys in URLs, internal endpoint names. Anyone watching the page sees your users’ requests as they happen.
- Client IPs of your real users, plus which URLs they visit.
- Internal hostnames and IPs, including backend addresses when Apache proxies.
- URL patterns and vhost inventory, including internal or staging vhosts sharing the server.
It is attack surface, not just disclosure. CVE-2014-0226 was a heap buffer overflow in mod_status on threaded MPMs (worker, event), reachable only when the status page was publicly accessible, fixed in 2.4.10. CVE-2012-3499 was an XSS in the status page output, fixed in 2.4.4. Old, but a public status page on an unpatched server is exactly what these needed.
One more wrinkle: SetHandler server-status is valid in directory and .htaccess context. Once mod_status is loaded, anyone who can write an .htaccess file can map a status handler, so a developer’s debug change can re-expose it without touching the main config. See the mod_status documentation.
Exposure usually survives the first fix attempt because of where the <Location> block lives relative to vhosts and proxies:
flowchart TD
A[Request for /server-status arrives] --> B{Which vhost matches?}
B --> C[Default or wildcard vhost]
B --> D[Named vhost]
C --> E{Global status.conf restriction}
D --> F{Vhost-level Location block?}
F -->|yes, e.g. Require all granted| G[200: status page served to anyone]
F -->|no| E
E -->|Require local / Require ip only| H[403 for outsiders]
E -->|missing or stale 2.2 syntax| GCommon causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Enabled for debugging, never tightened | <Location "/server-status"> with Require all granted or no authz at all | grep -rn "server-status" /etc/apache2/ /etc/httpd/ |
| Vhost precedence override | Global status.conf is restricted, but a vhost has its own <Location> (for example <Location /> Require all granted) that wins | Check every enabled vhost for Location blocks |
| Reverse proxy masks the restriction | Require ip matches the proxy or LB address, not the real client, so the operator opened it up “to make it work” | Is mod_remoteip configured with a trusted proxy list? |
| Stale Apache 2.2 syntax | Order/Allow/Deny directives in a 2.4 config; they only work via the deprecated mod_access_compat, and mixing them with Require is discouraged | Look for Order, Allow from, Deny from in status config |
| Only the HTML page was considered | /server-status blocked, but /server-status?auto or /server-info still answers | Curl both endpoints plus /server-info from outside |
| balancer-manager left open with it | /balancer-manager reachable, exposing backend members and allowing state changes | Curl /balancer-manager from outside |
Quick checks
Run the external checks from a host outside your network. Checking from the server itself proves nothing, because localhost is usually the one address that is supposed to work.
# Is it reachable from the internet? Test every form of the endpoint.
curl -s -o /dev/null -w "%{http_code}\n" http://<public-ip>/server-status
curl -s -o /dev/null -w "%{http_code}\n" "http://<public-ip>/server-status?auto"
curl -s -o /dev/null -w "%{http_code}\n" http://<public-ip>/server-info
curl -s -o /dev/null -w "%{http_code}\n" http://<public-ip>/balancer-manager
# Repeat with a Host header for each public vhost name. Exposure can differ per vhost.
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: www.example.com" http://<public-ip>/server-status
# Find where the handler is configured (Debian/Ubuntu paths, then RHEL paths)
grep -rn "server-status\|server-info\|balancer-manager" /etc/apache2/ 2>/dev/null
grep -rn "server-status\|server-info\|balancer-manager" /etc/httpd/ 2>/dev/null
# Confirm the module is actually loaded
apachectl -M 2>/dev/null | grep -i status
# Who has been requesting it, and what status did they get back?
grep -E '"(GET|HEAD) /server-status' /var/log/apache2/access.log | \
awk '{print $1, $9}' | sort | uniq -c | sort -rn | head -20
# Include rotated logs for history
zgrep -h -E '"(GET|HEAD) /server-status' /var/log/apache2/access.log*.gz 2>/dev/null | \
awk '{print $1, $9}' | sort | uniq -c | sort -rn | head -20
Any 200 in that output from a non-local, non-monitoring IP means the data was served to that address. That is your leak window.
How to diagnose it
- Confirm exposure externally. A 200 on any of the four curls above, from an outside host, confirms it. A 403 means an authz rule is doing its job. A 404 means the handler is not mapped for that vhost, which is also fine.
- Locate every place the handler is enabled. The grep above finds
SetHandler server-statusand friends. Note which file each match lives in: a globalmods-enabled/status.conf, or inside a specific vhost file. - Resolve the vhost question. If the global config looks correctly restricted but the page is still public, a vhost-level
<Location>block is overriding it. The vhost that matches the request wins, and itsLocationcontext takes precedence over the global one. Test each public hostname with theHostheader curl to find which vhost is leaking. - Check the proxy path. If Apache sits behind a reverse proxy or load balancer,
Require ipsees the proxy’s address. Withoutmod_remoteipand a trusted proxy list, IP-based rules are meaningless, and operators often “fix” the resulting breakage by granting access too broadly. - Establish the leak window. Use the access log analysis above to find the earliest 200 response to
/server-statusfrom an external IP. Everything served between then and now was readable: request URIs, client IPs, hostnames. Feed that into your incident assessment, especially if query strings carry tokens on this site. - Look for active reconnaissance. Scanner hits on
/server-statusrarely come alone:
# Broader scanner fingerprint around the same timeframe
grep -iE '/(\.env|\.git|wp-admin|wp-login|phpmyadmin|actuator|server-status|server-info)' \
/var/log/apache2/access.log | tail -20
Hits on .env, .git, and server-status from the same source IP is a scanner doing inventory, and it means the exposure was found by exactly the tooling you worried about.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| 200 responses to /server-status from external IPs | Proof the leak is active or has returned | Any occurrence from an IP outside your allowlist |
| 401/403 rate on /server-status per source IP | Reconnaissance cadence; the playbook flags >50 401s/minute from one IP as brute-force-class | Repeated denied hits from one IP, especially with non-browser User-Agents |
| Scanner URI pattern hits | /server-status probes alongside .env/.git/phpmyadmin indicate inventory scanning | Any hit on a path that actually exists and responds |
| Request rate per source IP | Separates background internet noise from targeted probing | >100x the average per-IP rate (unless a known LB/CDN) |
| Config reload events | A failed graceful reload silently keeps stale config running; a reload can also re-introduce a bad status.conf | Reloads without a corresponding change, or failed configtest after reload |
Fixes
All of these are config changes. Validate with apachectl configtest and apply with apachectl graceful (SIGUSR1), not a hard restart, so you do not drop connections.
Restrict to localhost and the monitoring subnet
This is the standard fix. The status page stays available to local scrapers and your monitoring network, and nobody else.
# Apache 2.4 syntax
<Location "/server-status">
SetHandler server-status
Require local
Require ip 203.0.113.0/24 # your monitoring subnet
</Location>
Require local covers connections originating on the machine itself, which is what local collectors use. The Require ip line opens it to the network your monitoring stack scrapes from. Both /server-status and /server-status?auto are covered because the restriction is on the path, not the query string.
Add authentication when IP allowlisting is not enough
If the page must be reachable from networks you do not control addressing for (a jump host with dynamic egress, an on-call laptop), layer Basic auth on top:
<Location "/server-status">
SetHandler server-status
AuthType Basic
AuthName "server-status"
AuthUserFile /etc/apache2/.htpasswd-status
Require valid-user
</Location>
Tradeoff: you now have a credential to rotate, and Basic auth must only ever ride over TLS.
Put the restriction inside the vhost
If your testing showed a vhost-level Location block overriding the global config, move the restriction into that vhost’s own configuration. A global status.conf does not protect a vhost that brings its own Location context. After the change, re-run the per-vhost Host header curls to prove every vhost now denies outsiders.
Behind a reverse proxy: fix mod_remoteip first
If Apache sits behind a proxy or LB, configure mod_remoteip with your trusted proxy list so Apache sees real client addresses. Only then does Require ip mean what you think it means. Be aware that Require local matches any connection originating on the same host, so a co-located proxy can make “local” much broader than intended. Test from outside again afterward.
Do not forget the adjacent endpoints
/server-info(mod_info) dumps your effective configuration. If you do not actively need it, do not map it at all. If you do, restrict it the same way./balancer-managerexposes backend member state and lets a visitor change balancer member status. Restrict it to the same allowlist, or remove the mapping.
Replace 2.2-era directives
Order, Allow, and Deny come from mod_access_compat, are deprecated in 2.4, and will go away in a future version. Mixing them with Require in the same scope is discouraged and produces surprises. Rewrite to Require ip / Require local and drop the old lines.
Prevention
- Add the external check to CI or a scheduled job. Four curls from an outside host, alerting on any 200, catches regressions from config management drift.
- Alert on 200s to /server-status from non-allowlisted IPs in access log monitoring. This is a deterministic signal, not a heuristic.
- Baseline scanner noise. Internet-facing Apache gets probed constantly; you want the alert to fire on success (a 200) and on concentrated repetition, not on every stray 403.
- Review mod_status config in code review the same way you would a firewall rule. It is an access control decision, not a debug toggle.
- Keep Apache patched. The mod_status CVEs needed exactly this misconfiguration to be reachable.
How Netdata helps
- Netdata’s Apache collector scrapes
http://localhost/server-status?auto, so the localhost-only lockdown above is compatible with full monitoring: BusyWorkers, IdleWorkers, scoreboard state distribution, requests per second, and uptime keep flowing with no internet exposure. - Access log parsing surfaces response-code breakdowns over time, so a spike of 403s on
/server-statusor a stray 200 from an external address shows up as a visible anomaly instead of a grep you forgot to run. - Correlating 403 bursts on status paths with per-IP request rates separates routine internet background noise from a scanner systematically working through your vhosts.
- Uptime and restart tracking catches unexpected reloads, which is when a stale or reverted status.conf typically re-exposes the endpoint.
- Netdata’s Apache HTTP Server monitoring with Netdata brings these signals together with per-second metrics and ML anomaly detection.
Related guides
- Apache BusyWorkers and IdleWorkers: reading worker utilization from mod_status
- Apache balancer member in error state: reading balancer-manager and failover
- Apache 503 Service Unavailable: worker exhaustion versus proxy pool exhaustion
- Apache 500 Internal Server Error: modules, handlers, and misconfiguration
- Apache 502 Bad Gateway: a backend that returned an invalid response
- Apache 504 Gateway Timeout: slow backends, ProxyTimeout, and worker pile-up
- Apache 5xx error rate: 500 vs 502 vs 503 vs 504 and what each one means
- Apache AH00558: Could not reliably determine the server’s fully qualified domain name
- Apache backend response time: telling ‘Apache is slow’ from ’the backend is slow’
- Apache SSL certificate expired: the total, preventable HTTPS outage
- Apache CLOSE_WAIT and TIME_WAIT: connection leaks versus normal churn
- Apache CPU saturation: TLS handshakes, mod_deflate, mod_rewrite, and mod_security






