Post-Compromise Checklist for Self-Hosted Services
When a self-hosted service is exploited, a fast miner is the visible part. This checklist covers containment, persistence hunting, secret rotation, and the rebuild decision in the right order.
The Scenario
A self-hosted application, say a Gitea instance, was internet-exposed and running a version with a known, actively exploitedActively Exploited🛡️A vulnerabilityVulnerability🛡️A weakness in software, hardware, or processes that can be exploited by attackers to gain unauthorized access or cause harm. that attackers are currently using in real-world attacks, requiring immediate patching regardless of severity score. flaw. You discover it because CPU is pegged, the host provider sent a warning, or you read the CISA KEVCISA KEV🛡️The Known Exploited Vulnerabilities catalog maintained by CISA, listing vulnerabilities actively exploited in attacks that federal agencies must patchPatch🛡️A software update that fixes security vulnerabilities, bugs, or adds improvements to an existing program. by specific deadlines. entry for CVE-2026-60004 and realized the deadline applied to you. Something ran as the service accountService Account🛡️A non-human operating system or application account under which a service runs. Its permissions define the blast radius of any exploitExploit🛡️Code or technique that takes advantage of a vulnerability to cause unintended behavior, such as gaining unauthorized access. against that service, since attacker code executes with the service account's access to files, secrets, and the network.. You do not yet know what.
The case that put Gitea on the KEV list in August 2026 is a useful template because it was fully documented: an automated attacker registered an account, created a repository, exploited the diffpatch endpoint, and ran a dropper that killed competing miners, downloaded a payload for the host's architecture, and mined cryptocurrency. The active phase took around eleven seconds. Investigation found no cron jobs, no systemd units, and no added SSH keys. That is the best case. The checklist below assumes you are not guaranteed the best case.
Phase 1: Contain Without Destroying Evidence
- Cut the service off from the network before anything else. FirewallFirewall🌐Security system that monitors and controls network traffic based on predetermined rules. the host or stop the container's network, but do not delete the container or wipe the filesystem yet. Everything you need to understand the scope is on that disk.
- Snapshot. Take a filesystem snapshot or a copy of the container layer and the service's data directory. If you rebuild later, this copy is what you investigate.
- Record the running state: process list, open network connections, logged-in sessions, and the service's own logs for the last several days. Capture them to a location outside the compromised host.
- Note the timeline anchors you already have: the vendor's fix date, when your instance became vulnerable relative to it, and when the first symptom appeared. For the Gitea case, the fix shipped July 27 and exploitation was confirmed in late August; every day in between is the exposure window.
Phase 2: Establish What Ran
The question is not just whether a miner ran, but whether anything else did. Work through:
- Application-level artifacts. For a forge, that means user accounts created during the window, repositories created by those accounts, and unexpected branches or commits. The Gitea attack fingerprint was a throwaway user plus a throwaway repository. The attacker's payload may also be preserved in the repository as Git objects, which is useful evidence.
- Filesystem artifacts owned by the service account. Files in temporary directories, unexpected binaries, and executable files under hooks directories in repository storage. Understanding why git hooks are a server-side attack surface tells you exactly which paths to inspect.
- Process and network history. Sustained CPU, connections to unfamiliar hosts, and processes with the service user that are not the service.
If your findings match the documented cryptojackingCryptojacking🛡️Unauthorized use of a compromised system's CPU or GPU to mine cryptocurrency for the attacker. Typically delivered by an automated dropper after an exploit, it is usually detected through sustained high CPU load rather than through data loss. campaign closely, you have a reasonable basis for scoping the incident as opportunistic. If you find anything that does not match, such as reads of the configuration file, database dumps, or Git fetches of repositories the throwaway account should not have accessed, escalate the scope to data theft.
Phase 3: Hunt for Persistence
A miner that runs for eleven seconds and leaves nothing behind is what a commodity dropper does. A more careful operator plants a foothold and leaves. Check, at minimum:
- Scheduled tasks: cron entries for the service user and for root, systemd timers and units, and any at jobs.
- Authentication: SSH authorized_keys for every account on the host, new local users, and changes to PAM or sudoers.
- Application-level persistence: new admin users in the service, new OAuthOAuth🛡️An open standard authorization protocol that allows applications to access user resources without exposing passwords, using tokens instead of credentials. applications or personal access tokens, new webhooks pointing at external hosts, new deploy keys, and new SSH keys attached to existing accounts. For Gitea specifically, 1.27.2 fixed a separate bug that let a limited-scope token add SSH keys, so key inventories deserve a careful look.
- Container and orchestration layer: modified images, changed entrypoints, and added volumes or mounts.
- Shell startup files and library preload configuration for the service user. The observed dropper cleared LD_PRELOAD and LD_LIBRARY_PATH before running, which is a reminder that these are common hiding places.
Document each check with a result, including the negative ones. "No cron, no systemd, no SSH keys" is a finding, not an absence of one.
Phase 4: Rotate Everything the Service Account Could Read
This is the phase teams skip, and it is the one that determines whether the incident is actually over. The Gitea advisory lists what the service account can typically reach: application configuration and its secrets, environment variables with credentials, mounted repositories, the database, and OAuth or integration tokens. Assume every one of those was read if you cannot prove otherwise.
Rotate in this order:
- Credentials that grant access to other systems: CI runner registration tokens, deploy keys, cloud provider keys in environment variables, SMTP passwords, and OAuth client secrets. These are what an attacker uses to move.
- The service's own internal secrets: session keys, signing keys, and internal tokens in the configuration file. Rotating these invalidates any session or token the attacker minted.
- The database password, and check the database for rows the attacker may have added.
- User-facing credentials: force password resets or re-authentication if the service stores password hashes and you cannot rule out a database read.
- Anything embedded in repositories. If source code contains hardcoded credentials, those are now public as far as you are concerned.
If the inventory of secrets does not exist, building it is the first action item after the incident. Hardening a self-hosted Git server includes making that rotation cheap before it is needed.
Phase 5: Rebuild or Restore
Patching the exploited flaw in place is acceptable only if you can account for everything that ran. In practice, a rebuild from a known-good image plus a restore of application data is faster and more defensible than cleaning a host you do not fully trust. Restore the data directory and database from your snapshot after reviewing them for attacker-added content, apply the current vendor release, and apply the hardening changes before reconnecting the network. For the Gitea case, that means 1.27.2 or later, registration closed, and egress restricted.
Phase 6: Close the Loop
Write down why the instance was vulnerable in the first place. Nearly always the answer is a patch cadence measured in months for a service that faces the internet and holds credentials. The fix is a process change, not a technical one: subscribe to vendor advisories, treat critical CVEs in exposed services as same-day work, and track the CISA KEV catalog as a forcing function. The procedure in how to triage a CISA KEV addition in 72 hours is the cadence to adopt.
A compromise that ends with a rotated secret inventory, a hardened rebuild, and a patch process is a good outcome. One that ends with a killed miner process and nothing else is an incident that has not finished yet.