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| G

Common causes

CauseWhat it looks likeFirst thing to check
Enabled for debugging, never tightened<Location "/server-status"> with Require all granted or no authz at allgrep -rn "server-status" /etc/apache2/ /etc/httpd/
Vhost precedence overrideGlobal status.conf is restricted, but a vhost has its own <Location> (for example <Location /> Require all granted) that winsCheck every enabled vhost for Location blocks
Reverse proxy masks the restrictionRequire 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 syntaxOrder/Allow/Deny directives in a 2.4 config; they only work via the deprecated mod_access_compat, and mixing them with Require is discouragedLook 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 answersCurl both endpoints plus /server-info from outside
balancer-manager left open with it/balancer-manager reachable, exposing backend members and allowing state changesCurl /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

  1. 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.
  2. Locate every place the handler is enabled. The grep above finds SetHandler server-status and friends. Note which file each match lives in: a global mods-enabled/status.conf, or inside a specific vhost file.
  3. 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 its Location context takes precedence over the global one. Test each public hostname with the Host header curl to find which vhost is leaking.
  4. Check the proxy path. If Apache sits behind a reverse proxy or load balancer, Require ip sees the proxy’s address. Without mod_remoteip and a trusted proxy list, IP-based rules are meaningless, and operators often “fix” the resulting breakage by granting access too broadly.
  5. Establish the leak window. Use the access log analysis above to find the earliest 200 response to /server-status from 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.
  6. Look for active reconnaissance. Scanner hits on /server-status rarely 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

SignalWhy it mattersWarning sign
200 responses to /server-status from external IPsProof the leak is active or has returnedAny occurrence from an IP outside your allowlist
401/403 rate on /server-status per source IPReconnaissance cadence; the playbook flags >50 401s/minute from one IP as brute-force-classRepeated denied hits from one IP, especially with non-browser User-Agents
Scanner URI pattern hits/server-status probes alongside .env/.git/phpmyadmin indicate inventory scanningAny hit on a path that actually exists and responds
Request rate per source IPSeparates background internet noise from targeted probing>100x the average per-IP rate (unless a known LB/CDN)
Config reload eventsA failed graceful reload silently keeps stale config running; a reload can also re-introduce a bad status.confReloads 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-manager exposes 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-status or 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.