Ghostcat (CVE-2020-1938) is a configuration-driven vulnerability in Apache Tomcat’s AJP connector. Before the 9.0.31 / 8.5.51 / 7.0.100 fixes, the AJP protocol carried no authentication. Any client that could reach port 8009 could craft AJP requests that made Tomcat read and return arbitrary files from the server, including WEB-INF/web.xml and server.xml. In some configurations the same primitive enables remote code execution through JSP inclusion.
The fix was not a patch to existing behavior. It was a set of new defaults: the AJP connector binds to the loopback address and requires a secret attribute with secretRequired=true. Deployments that carried an older server.xml forward, or operators who re-enabled AJP without setting the new attributes, remain exposed on instances that look perfectly healthy to a normal HTTP health check.
What Ghostcat is
The AJP (Apache JServ Protocol) connector is a binary protocol Tomcat uses to receive requests from a fronting web server, typically Apache httpd with mod_jk or mod_proxy_ajp. It is more efficient than HTTP for the proxy-to-backend hop because it reuses a compact binary representation instead of re-parsing HTTP headers on both sides.
AJP was designed for a trusted network segment between a web server and an application server. It had no authentication, no integrity protection, and trusted the fronting server to supply request attributes that Tomcat would honor. Tomcat’s DefaultServlet respects servlet-container attributes set via AJP, including ones that control which file to serve and whether to interpret it as a JSP. A client that speaks raw AJP can set those attributes directly, bypassing the fronting web server entirely.
The two concrete impacts:
- Arbitrary file read. The attacker sets AJP request attributes (
javax.servlet.include.request_uri,javax.servlet.include.path_info,javax.servlet.include.servlet_path) to point at a file inside the web application. Tomcat’sDefaultServlethonors them and returns the file content. The classic targets areWEB-INF/web.xml(database credentials, servlet mappings) and any file under the webapp root that is normally protected from direct HTTP access. - Potential RCE via JSP inclusion. If the attacker can upload or reference a file with attacker-controlled content ending in
.jsp, and the webapp processes JSP includes, the included JSP compiles and executes on the server. This requires a writable path the attacker can influence, which is why file read is the near-universal impact and RCE is the conditional one.
CVSS 3.1 base score is 9.8 (Critical).
How exposure happens
Ghostcat is not a code bug you trigger by sending a malformed request to an HTTP port. It is a configuration condition: the AJP connector is enabled and reachable from a network the attacker can access, and no secret gates the connection.
| Configuration | What it looks like | Risk |
|---|---|---|
AJP enabled, address="0.0.0.0", no secret (pre-9.0.31) | ss -tnl 'sport = :8009' shows 0.0.0.0:8009 | Direct file read and potential RCE from any reachable host |
AJP enabled, address="0.0.0.0", secretRequired="false" | Port 8009 on all interfaces, connector starts without a secret | Same as above. Setting secretRequired="false" on an internet-facing bind recreates the original vulnerability |
AJP enabled, address="127.0.0.1", no secret | Port 8009 on loopback only | Safe from remote attack, but any process on the host or a server-side request forgery in a co-located app can still reach it |
AJP commented out or absent from server.xml | ss -tnl 'sport = :8009' returns nothing | Not exposed |
AJP enabled, address="127.0.0.1", secretRequired="true", secret="<long random>" | Loopback bind, connector starts only with the shared secret | Hardened. Safe for httpd/mod_jk on the same host |
The most common path to exposure is a carried-forward server.xml. An older config has the AJP connector uncommented, bound to all interfaces, with no secret attribute. On a pre-9.0.31 Tomcat this starts silently. After upgrading, the operator hits a startup failure, sets secretRequired="false" to make the connector start again, and recreates the original vulnerability on the new version.
Quick checks
All of these are read-only. Run them on each Tomcat host.
# Is AJP listening, and on what address?
ss -tnl 'sport = :8009'
Read the Local Address:Port column. 0.0.0.0:8009 or *:8009 means all interfaces. 127.0.0.1:8009 or [::1]:8009 means loopback only.
# What does server.xml say about AJP?
grep -ni "ajp\|8009\|secretRequired\|secret=" "$CATALINA_BASE/conf/server.xml"
If the connector block is present, confirm the address, secret, and secretRequired attributes. If the block is missing or wrapped in <!-- -->, AJP is disabled.
# Confirm the Tomcat version (determines which defaults apply)
"$CATALINA_HOME/bin/version.sh" 2>/dev/null | grep -i "server version"
# From a different host: is the port reachable from outside?
nmap -sV -p 8009 <tomcat-host>
If nmap reports the port open and identified as ajp13, the connector is reachable from that vantage point. Repeat the scan from an internet-facing perspective if the host has a public address.
# Check for the old attribute name (renamed in 9.0.31)
grep -i "requiredSecret" "$CATALINA_BASE/conf/server.xml"
requiredSecret was renamed to secret in 9.0.31, and secretRequired was added with a default of true. If you are on 9.0.31 or later and your config still uses requiredSecret, migrate it to secret to avoid ambiguity.
Interpreting the results
flowchart TD
A["ss -tnl sport = :8009"] --> B{Port 8009 listening?}
B -- No --> C["AJP disabled. Not exposed."]
B -- Yes --> D{Bind address?}
D -- "0.0.0.0 / * / public IP" --> E{secretRequired=true and secret set?}
D -- "127.0.0.1 / ::1" --> F{Reachable from untrusted network?}
E -- No --> G["Exposed: arbitrary file read, possible RCE"]
E -- Yes --> H["Authenticated. Verify the secret is not default or empty."]
F -- Yes, via SSRF or co-located process --> G
F -- No --> I["Contained to the host."]Three outcomes matter:
- Port not listening. AJP is disabled. Not exposed through this connector.
- Port listening on all interfaces without a secret. You are exposed. Treat the host as potentially compromised if it was reachable from an untrusted network. File read of
WEB-INF/web.xmlis trivial with public exploit tooling. Check whether anything in the webapp or a writable upload directory could have supplied a.jspfor inclusion. - Port listening on loopback or with a secret. Exposure is contained. Confirm the secret is not empty, not a default value, and not guessable. If you depend on the secret alone because the bind is
0.0.0.0, treat any host that can route to port 8009 as fully trusted.
Securing the AJP connector
The right fix depends on whether you need AJP at all.
If you do not need AJP
Comment out or remove the AJP connector block in server.xml. If your fronting proxy speaks HTTP (nginx, HAProxy, httpd with mod_proxy_http), you do not need AJP.
<!-- Comment out the entire AJP Connector element -->
<!--
<Connector protocol="AJP/1.3"
port="8009"
address="127.0.0.1"
secret="<long-random-string>"
secretRequired="true"
redirectPort="8443" />
-->
Restart Tomcat and confirm the port is gone with ss -tnl 'sport = :8009'.
If you need AJP on the same host
Keep the connector but bind it to loopback and require the secret. The secret must match on both sides: the secret attribute in Tomcat’s server.xml and the secret worker property in httpd’s workers.properties.
<Connector protocol="AJP/1.3"
port="8009"
address="127.0.0.1"
secret="<long-random-string>"
secretRequired="true"
redirectPort="8443" />
Corresponding httpd worker:
worker.tomcat1.type=ajp13
worker.tomcat1.host=127.0.0.1
worker.tomcat1.port=8009
worker.tomcat1.secret=<long-random-string>
Never set secretRequired="false" on a connector bound to 0.0.0.0. That combination is the exact pre-fix condition.
If the AJP peer is on a different host
You cannot use loopback. Put the two hosts on a private network segment or an encrypted tunnel, bind AJP to the interface on that segment, and require the secret. Treat any host that can route to that port as fully trusted, because AJP has no per-request authentication beyond the shared secret.
Upgrading from a pre-9.0.31 config
After upgrading Tomcat to 9.0.31 or later, if you keep AJP, add the secret attribute before restarting. With secretRequired defaulting to true, the connector refuses to start without a non-empty secret. The symptom is a startup failure in catalina.out.
If your reverse proxy sends custom AJP request attributes that Tomcat does not recognize, set allowedRequestAttributesPattern to a regex matching them. The default is null, which rejects any unrecognized attribute with a 403. This is correct for security but breaks proxies that inject custom attributes without an explicit allow pattern.
Verifying the fix
# Confirm the bind address changed
ss -tnl 'sport = :8009'
# Confirm the connector started without errors
grep -i "ajp\|secret\|protocol handler" "$CATALINA_BASE/logs/catalina.out" | tail -20
# From an external host, confirm the port is closed
nmap -p 8009 <tomcat-host>
If you kept AJP on loopback with a secret, exercise the full httpd-to-Tomcat path with a real request to confirm the shared secret matches on both sides. A mismatch produces 403s or connection failures in the httpd error log.
Signals to monitor
Ghostcat is a configuration drift problem, not a runtime metric. The signals that matter are posture checks, not time series.
| Signal | Why it matters | Warning sign |
|---|---|---|
| AJP port 8009 listening state | Detects re-enablement after a config change or upgrade | Port appears on a host where it was previously disabled |
| AJP bind address | Catches a loopback bind widening to 0.0.0.0 | Bind address is not 127.0.0.1 or ::1 |
secretRequired value | Catches false set to work around a startup failure | Attribute is absent or set to false |
Access log entries for WEB-INF/ or META-INF/ | Indicates active exploitation or reconnaissance | Any 200 response to those paths |
| Tomcat version vs. fix line (9.0.31 / 8.5.51 / 7.0.100) | Flags instances too old to have the hardened defaults | Version is below the fix line and AJP is enabled |
How Netdata helps
Netdata surfaces several of the posture-relevant signals above as part of normal per-second collection, shortening the window between a config change that re-exposes AJP and your awareness of it.
- Port reachability from an external vantage point. A
portcheckcollector aimed at port 8009 from a separate host catches a bind that widened beyond loopback, even when the Tomcat process itself looks healthy. - Access log correlation. If you tail or ingest the Tomcat access log, any request for
WEB-INF/orMETA-INF/paths stands out against baseline traffic. Correlating those entries against AJP connector activity distinguishes HTTP probing from AJP exploitation. - Process and connector state. Per-second JMX collection confirms whether connectors started after a restart, so a failed AJP connector (secret mismatch, bind failure) surfaces immediately rather than waiting for a user report.
- Config and version inventory. Pairing per-host inventory (Tomcat version,
server.xmlpresence of the AJP block) with the listening-state checks means an upgrade or config push that re-exposes AJP triggers an alert before an attacker finds it.
The key correlation is between network posture (is the port reachable?) and application posture (is the connector authenticated?). Either signal alone is insufficient: a loopback bind with no secret is still reachable from a co-located process or an SSRF vector, and an all-interfaces bind with a secret trusts every host that can route to it.
Related guides
- Tomcat 5xx error rate: separating server failures from crawler 404s
- Tomcat accept queue overflow: acceptCount, somaxconn, and Recv-Q
- Tomcat access log setup: adding %D and %T for per-request latency
- Tomcat java.net.BindException: Address already in use: the connector never starts
- Tomcat average latency lies: why you need p95/p99 from the access log
- Tomcat threads blocked forever: the missing outbound timeout
- Tomcat classloader leak on redeploy: why the old WebappClassLoader never dies
- Tomcat connection refused: maxConnections and acceptCount both exhausted
- Tomcat accepts connections but never responds: the TCP-connect trap
- Tomcat file descriptor usage: OpenFileDescriptorCount vs the ulimit
- Tomcat frequent Full GC: pause time, G1, and the 5% overhead rule
- Tomcat GC death spiral: full GCs dominating and throughput collapsing






