When Your Self-Hosted App Lands in CISA KEV: The Operator 72-Hour Response Checklist

CISA added four self-hosted apps to KEV in three weeks: Metabase, MLflow, Zimbra, Gitea. The 72-hour operator checklist: exposure check, patch or compensate, compromise check, scoped credential rotation, documentation for the next incident.

Share

In three weeks this August, CISA added four self-hosted apps to its Known Exploited Vulnerabilities catalog: Metabase August 11, MLflow August 19, Zimbra August 21, Gitea August 25. All four are apps a homelab runs, and all four were exploited in the wild while the default install was reachable. Three of the four carry a three-day federal due date, which is where the 72 hours comes from. That is the pattern; this is the response: five phases, in order, applied to whichever of the four you run.

One thing first, because it changes how you read the clock: the KEV date is not your exposure date. Zimbra's fix shipped July 20, made the catalog August 21, thirty-two days later. Gitea's shipped July 27, made it August 25, twenty-nine days later. MLflow's 3.15.0 shipped July 31, made it August 19, and watchTowr telemetry shows exposed instances scanned within hours of the August 17 CVE assignment. Metabase sits at the other end: the vendor says Metabase Cloud was hit with a 0-day on the same unauthenticated route, versions 1.58 and above, before the entry landed five days after the August 6 advisory. In all four the exploitable window was open when the catalog page reached your feed. The listing is the notification; the real clock started at the vendor fix.

Phase 1: Exposure check

Thirty minutes, three data points per app: which version runs, what the patch floor is, whether the vulnerable surface is reachable from outside. The third answer decides whether the next phases are a formality or a full response.

Version check, per app:

gitea --version
mlflow version

For Metabase, the detection checklist has the version-check commands (docker inspect, the startup banner, or the jar run directly); for Zimbra, check the installed release against the vendor matrix. You are comparing against a floor, not a latest, because that is what these four cases give you:

App CVE KEV added Federal due Patch floor
Metabase CVE-2026-72898 (SQLi, CVSS 10.0) 2026-08-11 2026-08-14 per-line: 58.24 / 59.21 / 60.17 / 61.11 / 62.9 / 63.5; below v58 not affected
MLflow CVE-2026-64849 (SSRF, CVSS 9.3) 2026-08-19 2026-09-02 3.15.0; no 3.14.x backport exists
Zimbra CVE-2026-73570 (command injection, CVSS 8.9) 2026-08-21 2026-08-24 10.1.20 (released 2026-07-20)
Gitea CVE-2026-60004 (code injection, CVSS 9.8) 2026-08-25 2026-08-28 1.27.1 (released 2026-07-27); 1.27.2 is latest

Then the reachability probe, which differs per app because the exploited default differs per app.

Metabase. The sink, POST /api/session/reset_password, is an unauthenticated part of the password-reset flow that ships in every build. Any instance whose /api/ path is internet-reachable is exposed, directly or through a proxy that forwards the path. A proxy in front does not fix it unless it blocks or authenticates that path.

MLflow. The default mlflow server runs unauthenticated on port 5000 or 5001, and the model-registry webhook API, including the /test endpoint, is unauthenticated by default; the only webhook authorization is an optional plugin that is not loaded by default. Probe from outside the server:

curl -s -o /dev/null -w "%{http_code}\n" http://TARGET:5000/api/2.0/mlflow/webhooks
# a 401 means auth is in front; a 200 or a JSON error means anyone can talk to the webhook API

Zimbra. Three conditions decide whether the clock applies to you at all: zimbra-snmp installed, snmp_notify enabled, swatchdog running. If any of them is no, the RCE path is closed even on an old version. The operational trap is where the input arrives: crafted SMTP requests, riding the mail port. That is the port you never think to restrict, because it is just mail.

Gitea. Two defaults combine: the diffpatch API route ships enabled, and open self-registration ships enabled, so the KEV's "repository write access" prerequisite is what an anonymous visitor gets by registering and creating a repo. UI internet-reachable and registration on means exposed. Check DISABLE_REGISTRATION in app.ini. The exploit also needs Git 2.32 or newer on the server, which most installs have.

If you have never probed your own exposure from the outside, do it once now, before the next incident does it for you in a hurry. The external attack surface audit I ran on this lab walks the process with real scan output: How exposed is your homelab? (not yet published).

Phase 2: Patch or compensate

If you can patch within hours, patch. The interim controls below cover the gap until you do; each is vendor-acknowledged, and none of the four needs downtime to apply.

Metabase. Block the endpoint at the proxy. It is the vendor's own workaround, and it is safe to apply before or instead of the upgrade:

# Caddy
@mb_reset path /api/session/reset_password
handle @mb_reset {
    abort
}
# nginx
location = /api/session/reset_password {
    return 404;
}

MLflow. 3.15.0 is the only real fix, and the same release closes two more CVEs (CVE-2026-69146, CVE-2026-69148). On 3.14.0 there is no patch to grab: the fix never got a 3.14.x backport, so plan a tested minor-version bump, not a hotfix. Meanwhile, load the auth plugin (Basic or OIDC) or put SSO plus source-address restriction in front, and keep 5000 and 5001 off the internet and the guest network. MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS is a development-only escape hatch that disables the URL check; never set it on a server anyone else can reach.

Zimbra. Kill the trigger, not the mail flow: turn off snmp_notify, and if you are not actually using SNMP monitoring, uninstall zimbra-snmp. Removing the vendor-acknowledged trigger condition closes the unauthenticated path. If your mail flow allows it, restrict the SMTP endpoint at the edge to expected senders, because the crafted request rides the mail ports.

Gitea. Two moves. Set DISABLE_REGISTRATION = true under [service] in app.ini, now; that kills the anonymous path while the patch is tested, and it is the highest-leverage interim control in this list. Then put proxy rules on the diffpatch endpoint: JSON only, rate-limited, size-capped, source-IP allowlisted. The Gitea detection guidance has the full nginx and fail2ban snippets.

One order-of-operations point: patch before you clean up. The vendor's Metabase post-upgrade hardening list assumes the endpoint is already closed, because until you patch or block it, anyone can walk it again while you review logs. Close the door first; then look for what came in.

The Zimbra timeline (advisory June 26, patch July 20, CERT Polska confirmation August 17, KEV August 21) is in the Zimbra cyber briefing (not yet published). Why gaps like these exist is in the patch-gap deep dive (not yet published).

Phase 3: Compromise check

This is the phase that carries the weight. All four cases have confirmed active exploitation, and in each the vulnerable surface was the default surface. If your instance was reachable in the window, the working assumption is compromised until the checks below say otherwise. A clean patch on a compromised box is still compromised.

Five checks, each grounded in what actually happened in these cases.

Log review, per app.

  • Metabase. The vendor-documented signature: POST /api/session/reset_password returning 400, then GET /api/user/current returning 200. The vendor's words: find that pattern in your application logs or your Metabase server ingress logs and it is likely the instance is compromised. Then query the application database, the Postgres or MariaDB Metabase uses for its own state: core_user for superusers with a date_joined in the window, metabase_database for unexplained recent updated_at, query_execution for result_rows above 10000, api_key for keys you do not recognize. The detection checklist has the full runnable SQL.
  • MLflow. Grep the tracking server log for webhook test calls; the giveaway is an internal or metadata URL in the reflected response: 169.254.169.254, metadata.google.internal, 127.0.0.1, RFC1918. Behind a proxy, an anonymous POST to any webhooks path you did not generate is the same signal. On the cloud side the damage shows up as IAM users or roles created around the window that nobody remembers, API calls under the instance profile you do not recognize, and a bill with a new compute line item, a tell that does not require log access.
  • Zimbra. CERT Polska gives two operator checks. In /var/log/zimbra.log, unexplained service state transitions, changed from stopped to running and back. Then files created by the zimbra user in the last 30 days under /opt/zimbra/jetty/webapps/, /opt/zimbra/jetty_base/webapps/, and /tmp/. A webapps drop is the webshell location; anything zimbra-owned you did not deploy there is a confirmed breach marker.
  • Gitea. In the application log: WARN patch apply returned non-zero exit code is the add/add collision that triggers the exploit; git apply finishing in sub-second time on a repo that should not have received pushes is the same event from the other side; any .git/hooks/ path inside a /tmp/git-* temp clone is the plant. At the proxy: two nearly identical POSTs to the same diffpatch endpoint, 30 to 90 seconds apart, from one source IP, is the PoC signature. One hit, investigate; two hits, assume exploitation. On disk:
sudo find /var/lib/gitea/data/repositories -path '*/hooks/post-index-change' -ls
sudo find /tmp -name 'post-index-change' -newermt "2026-08-20" -ls

Any post-index-change that is not part of your own hook set means the host is compromised, and the answer is a rebuild from verified-good media, not cleanup.

New users and accounts. Same move, different store: Metabase's core_user, Zimbra's directory plus any local account the zimbra user could write, Gitea's users and tokens created in the window (on open registration, assume the attacker created accounts), and the MLflow IAM side if the server runs on a cloud VM.

Cron jobs and scheduled tasks. Check crontab for the service user, /etc/cron.d, and systemd units for anything the app's user could plant. In the Zimbra case the service transitions in the log are the tell that the injection path is being used against monitored services. In the Gitea case the persistence is the git hook itself; the single-source Habr account found no cron, systemd, or SSH persistence at all, and the box was compromised regardless.

SSH keys. authorized_keys for the service account (zimbra, git) and for root. On a Zimbra host the zimbra user owns the mail data plane, and a key it could write is standing access.

Outbound connections. The strongest generic signal: the application log tells you what the app did; the connection table tells you what the box did. ss -tnp on the host, filter out what you expect (time sync, DNS, package mirrors, the relay), and read what is left. A new egress to an unfamiliar host from the app's service user is a finding until proven otherwise. For Gitea the server-side egress tell is weaker, because the PoC retrieves its output attacker-side over authenticated smart HTTP; watch for curl, wget, ncat, or socat spawns from the git user regardless.

One note on attribution: I am not naming actors in this piece. CERT Polska observed the Zimbra campaign and did not publicly attribute it. For the MLflow wave there is scanning telemetry, and Rescana has reported that stolen IAM credentials were used to deploy cryptominers and new IAM users; that is Rescana's report, carried as such, and no one has publicly named an actor or a victim. The Habr Gitea account is one operator describing a post-compromise miner-like dropper with sustained high CPU. One account, no corroboration, but the shape is what to keep in mind: high CPU on the git user, no obvious persistence. That is why the hook sweep matters more than the process list.

Phase 4: Credential rotation, scoped to what the app could touch

The scope is not what you saw in the logs. It is what the app's service account could read or reach, from first exposure to patch. The logs show what happened; the capability defines what could have. Rotate on the capability, verify with the logs.

Metabase. The vendor's instruction: rotate the credentials for every database connected to Metabase, not just the ones that look touched. The attacker holds admin of the app, and the app holds the stored credentials for every connection, so the blast radius is the connection list, full stop. Add the application database's own credentials, the admin accounts, and every active session (the vendor's post-upgrade step is to delete core_session wholesale), plus any API key you do not recognize. Then cross-check each connected warehouse's audit log for access from the Metabase service account in the window. The SQLi does not hand the attacker a readable password; it hands them Metabase admin, and they use Metabase itself to pull the data through the stored connections.

MLflow. If it runs on a cloud VM, the primary target is the instance profile: rotate it and everything derived from it, and assume the identity was held from exposure until patch. The metadata surfaces are per-cloud: the AWS iam/security-credentials path, GCP's metadata.google.internal, Azure's managed-identity token endpoint. The homelab version of the same primitive is the Docker daemon API, Elasticsearch, and any internal admin panel reachable on 127.0.0.1 or RFC1918. The full-read variant reads those back to the attacker; the blind-write variant, a redirect that preserves the POST, can push payloads at internal endpoints that act on POST. That second half is capability, not yet observed in the wild, but it is inside what the app could touch.

Zimbra. The attacker runs as zimbra, and zimbra owns the mail data plane. Scope: the mail store and the directory, which means mailboxes, accounts, aliases, delegated access, passwords. Service-to-service credentials: relay auth, spam-filter tokens, and extension credentials for integrations like EWS and Nextcloud, which the same 10.1.20 release shows exist and are a lateral path. System-level secrets under /opt/zimbra that the zimbra user can read. If Phase 3 found anything, assume zimbra's host access from first exposure to patch, and rotate what the mail host can reach with it.

Gitea. The GHSA's impact list is the scope, and it is a long one: app.ini, which holds the DB connection, the SMTP/LDAP/OAuth secrets, and Gitea's internal tokens. The Gitea DB credentials and the database contents, which means every user, token, session, and OAuth grant. Process environment secrets. The mounted repositories, because secrets committed in repo history, .env files and deploy keys and CI tokens, are readable by the service account. Every personal access token issued in the exposure window, which on an open-registration instance means assuming the attacker created accounts and tokens. And the internal services reachable from the Gitea host, scoped to what the gitea user could actually reach on your network.

The per-cloud credential chain for MLflow is walked in the MLflow SSRF advisory (not yet published), and the Gitea detection guidance carries the pre-rotation checklist, including the PAT revoke window.

Phase 5: Documentation for the next incident

The incident is done when the documentation is done: the next incident's response speed is set by what you wrote down this time. Five things, while it is fresh:

  1. The exposure window, as four timestamps. When the surface became reachable (first publication, port open, registration on). When you saw the KEV entry. When you patched or blocked. When you closed the exposure. These are impossible to reconstruct later, and they are the entire timeline of the next response.
  2. What was reachable, and from where. The exact endpoint, the proxy rule or port that let it through, the scanner hits you can tie to it.
  3. What you checked, and what you found. Even "clean" is a finding. Write down which Phase 3 checks came back empty on this fleet, because next time you will know which ones actually produce signal here and which ones are ceremony.
  4. What you rotated, and what you deliberately did not, and why.
  5. The gap numbers. When the vendor fix shipped and when the KEV entry landed. For this wave: Zimbra 32 days, Gitea 29, MLflow 19, Metabase about five with exploitation already in the wild before the listing. The fleet-level pattern you are building tells you where your patch cycle is losing, and which vendor release pages to watch on the day they ship.

Done a few times, Phase 5 becomes a standing posture: logs off-device, because the application log is the primary forensic artifact in all four of these cases, and a log the attacker can reach is a target, not a log. A version inventory that tells you what is running before the KEV page does. Exposure re-checks on a schedule: the homelab audit (not yet published) in standing mode instead of incident mode.

The 72 hours, condensed

  1. Version check on the affected app, compared against the Phase 1 floor.
  2. Reachability probe from outside the network, using the per-app probe from Phase 1.
  3. If exposed and unpatched: apply the interim control (proxy block, snmp_notify off, DISABLE_REGISTRATION = true, auth plugin). Now, before the logs.
  4. Patch to the floor. Verify the version after.
  5. Log review per Phase 3: the vendor signatures, the per-app greps, the hook sweeps.
  6. New accounts in the app's user store and on the host.
  7. Cron, systemd, and authorized_keys for the service user and root.
  8. Outbound connection sweep from the app's service user.
  9. If any of 5 through 8 hit: rebuild from verified-good media, then rotate on the Phase 4 scope.
  10. If clean: still rotate on the Phase 4 scope if the surface was ever internet-reachable.
  11. Write the five Phase 5 entries before you close the ticket.

Three days if you have the logs and the inventory. Fewer if you run the exposure check on a schedule.

Further reading

The four case pieces, in the order they landed:

  • Metabase: the detection checklist with the runnable version checks, the proxy-block snippets, and the indicator queries against core_user, metabase_database, query_execution, and api_key.
  • MLflow SSRF advisory (not yet published): the per-cloud credential chain, the tracking-server greps, and the telemetry.
  • Zimbra cyber briefing (not yet published): the full advisory-to-KEV timeline.
  • Gitea: the detection guidance (companion artifact, not yet published) with the auditd rules, the hook sweep, and the diffpatch proxy and fail2ban rules.

The structural pieces:

  • How exposed is your homelab? (not yet published) for the exposure audit this checklist assumes you have already done.
  • The patch gap from the operator's seat (not yet published) for why the exploitable window is the patch-to-KEV gap, not the listing.

The KEV page is the alarm. The next four phases are for what came in.

Topics: