Package Runtime Secret File Access Followed by IMDS Request

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Package Runtime Secret File Access Followed by IMDS Request

edit

Identifies a Python, Node.js, Bun, Deno, Ruby, PHP or Perl runtime, or a binary executing from a package install location, that opens a developer secret (SSH keys, package registry tokens, source control credentials or AI coding assistant credentials) and then connects to a cloud instance metadata service (IMDS) endpoint within one minute. This sequence matches credential stealers delivered through compromised packages, such as the malicious LiteLLM PyPI releases, while cloud provider credential files are excluded because legitimate cloud SDK credential chains read them before falling back to IMDS.

Rule type: eql

Rule indices:

  • logs-endpoint.events.file-*
  • logs-endpoint.events.network-*

Severity: high

Risk score: 73

Runs every: 5m

Searches indices from: now-9m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References: None

Tags:

  • Domain: Endpoint
  • Domain: Cloud
  • OS: macOS
  • OS: Windows
  • Use Case: Threat Detection
  • Tactic: Credential Access
  • Tactic: Initial Access
  • Data Source: Elastic Defend
  • Resources: Investigation Guide
  • Threat: Supply Chain
  • Threat: IMDS Credential Theft
  • Rule Type: Event Correlation (EQL)
  • Platform: macOS
  • Platform: Windows
  • Domain: GenAI
  • Mitre Atlas: AML.T0010
  • Mitre Atlas: AML.T0010.001
  • Mitre Atlas: AML.T0055
  • Mitre Atlas: AML.T0083

Version: 1

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Package Runtime Secret File Access Followed by IMDS Request

This rule correlates two behaviors from the same package ecosystem runtime process (process.entity_id) within one minute: opening a local secret store that cloud SDKs never read, followed by a connection to a cloud instance metadata endpoint (169.254.169.254, the ECS and EKS Pod Identity credential endpoints, the AWS IPv6 IMDS endpoints, or the Alibaba Cloud metadata endpoint). Legitimate software rarely needs both an SSH key, registry token or AI assistant credential and cloud role credentials. Infostealers delivered through compromised packages routinely do both, typically from a Python or Node.js interpreter that imported the malicious package.

Possible investigation steps

  • Identify the process (process.executable, process.entity_id) and walk its ancestry to determine what launched it. Pay close attention to package managers, CI runners, notebooks, IDEs, AI coding agents, and interpreters executing from virtual environments or site-packages/node_modules.
  • Review all file events for the same process.entity_id to scope which secrets were accessed. Stealers usually enumerate many credential locations in a short burst.
  • Review all network events for the same process.entity_id for outbound connections to non-metadata destinations immediately after the IMDS request, which may indicate exfiltration of the harvested data.
  • Look for Python .pth files, sitecustomize.py/usercustomize.py, or recently installed or upgraded packages in the interpreter’s environment that could have executed the code at import time.
  • Check cloud audit logs (CloudTrail, GCP Audit, Azure Activity) for use of the instance role credentials from IP addresses outside the instance’s expected egress.

False positive analysis

  • Release or deployment scripts on cloud-hosted build agents may read a registry or source control token and then request instance role credentials from the same interpreter. Confirm the script is an approved part of a build or release pipeline on that host.
  • AI coding assistants on cloud instances may read their own credential file before falling back to the metadata server for cloud authentication. Confirm the process is the assistant that owns the credential file.

Response and remediation

  • Isolate the host and terminate the offending process tree.
  • Rotate every secret the process opened (SSH keys, npm/PyPI tokens, Git and GitHub CLI tokens, Vault and Terraform tokens, AI assistant OAuth tokens) and revoke active sessions for the instance role or workload identity.
  • Identify and remove the malicious package version from the environment, lock files and internal mirrors, and hunt for the same package version across other hosts and CI runners.
  • Enforce IMDSv2 with a hop limit of 1 where possible and scope instance roles to least privilege.

Setup

edit

Setup

This rule requires data from Elastic Defend with file and network event collection enabled.

Elastic Defend only generates file open events on macOS and Windows, and only for paths in its event-on-access file path list. That list is delivered through the Elastic Defend global configuration artifact and can be extended per policy with the advanced.events.event_on_access.file_paths advanced setting. Credential paths in this rule that are not yet present in the deployed global configuration only match after they are added to that setting. On Windows, repeated access events for the same path are deduplicated for one hour by default.

This rule is scoped to macOS and Windows because Elastic Defend does not generate file open events on Linux.

Rule query

edit
sequence by process.entity_id with maxspan=1m
  [file where event.action == "open" and host.os.type in ("macos", "windows") and
    file.path like~ (
      /* SSH private keys */
      "/Users/*/.ssh/id_*", "?:\\Users\\*\\.ssh\\id_*",

      /* Package registry, source control and secrets manager tokens */
      "/Users/*/.npmrc", "?:\\Users\\*\\.npmrc",
      "/Users/*/.pypirc", "?:\\Users\\*\\.pypirc",
      "/Users/*/.git-credentials", "?:\\Users\\*\\.git-credentials",
      "/Users/*/.config/git/credentials", "?:\\Users\\*\\.config\\git\\credentials",
      "/Users/*/.config/gh/hosts.yml", "?:\\Users\\*\\AppData\\Roaming\\GitHub CLI\\hosts.yml",
      "/Users/*/.vault-token", "?:\\Users\\*\\.vault-token",
      "/Users/*/.terraform.d/credentials.tfrc.json", "?:\\Users\\*\\AppData\\Roaming\\terraform.d\\credentials.tfrc.json",

      /* Additional package registry and AI/ML platform tokens */
      "/Users/*/.yarnrc.yml", "?:\\Users\\*\\.yarnrc.yml",
      "/Users/*/.gem/credentials", "?:\\Users\\*\\.gem\\credentials",
      "/Users/*/.cargo/credentials.toml", "?:\\Users\\*\\.cargo\\credentials.toml",
      "/Users/*/.composer/auth.json", "/Users/*/.config/composer/auth.json", "?:\\Users\\*\\AppData\\Roaming\\Composer\\auth.json",
      "/Users/*/.cache/huggingface/token", "/Users/*/.huggingface/token", "?:\\Users\\*\\.cache\\huggingface\\token", "?:\\Users\\*\\.huggingface\\token",
      "/Users/*/.config/github-copilot/apps.json", "/Users/*/.config/github-copilot/hosts.json",
      "?:\\Users\\*\\AppData\\Local\\github-copilot\\apps.json", "?:\\Users\\*\\AppData\\Local\\github-copilot\\hosts.json",
      "/Users/*/.ollama/id_ed25519", "?:\\Users\\*\\.ollama\\id_ed25519",

      /* AI coding assistant credentials */
      "/Users/*/.claude/.credentials.json", "?:\\Users\\*\\.claude\\.credentials.json",
      "/Users/*/.codex/auth.json", "?:\\Users\\*\\.codex\\auth.json",
      "/Users/*/.codex/.credentials.json", "?:\\Users\\*\\.codex\\.credentials.json",
      "/Users/*/.codex/secrets/*.age", "?:\\Users\\*\\.codex\\secrets\\*.age",
      "/Users/*/.gemini/oauth_creds.json", "?:\\Users\\*\\.gemini\\oauth_creds.json",
      "/Users/*/.gemini/gemini-credentials.json", "?:\\Users\\*\\.gemini\\gemini-credentials.json",
      "/Users/*/.gemini/mcp-oauth-tokens.json", "?:\\Users\\*\\.gemini\\mcp-oauth-tokens.json",
      "/Users/*/.gemini/a2a-oauth-tokens.json", "?:\\Users\\*\\.gemini\\a2a-oauth-tokens.json",
      "/Users/*/.qwen/oauth_creds.json", "?:\\Users\\*\\.qwen\\oauth_creds.json",
      "/Users/*/.qwen/mcp-oauth-tokens.json", "?:\\Users\\*\\.qwen\\mcp-oauth-tokens.json",
      "/Users/*/.aws/sso/cache/kiro-auth-token.json", "?:\\Users\\*\\.aws\\sso\\cache\\kiro-auth-token.json",
      "/Users/*/.local/share/opencode/auth.json", "?:\\Users\\*\\.local\\share\\opencode\\auth.json",
      "/Users/*/.local/share/opencode/mcp-auth.json", "?:\\Users\\*\\.local\\share\\opencode\\mcp-auth.json",
      "/Users/*/.local/share/amp/secrets.json", "?:\\Users\\*\\.local\\share\\amp\\secrets.json",
      "/Users/*/.augment/session.json", "?:\\Users\\*\\.augment\\session.json",
      "/Users/*/.kimi/credentials/kimi-code.json", "?:\\Users\\*\\.kimi\\credentials\\kimi-code.json",
      "/Users/*/.cursor/auth.json", "?:\\Users\\*\\.cursor\\auth.json",
      "/Users/*/.pi/agent/auth.json", "?:\\Users\\*\\.pi\\agent\\auth.json",
      "/Users/*/.config/goose/secrets.yaml", "?:\\Users\\*\\AppData\\Roaming\\Block\\goose\\config\\secrets.yaml",
      "/Users/*/.config/muse/auth.json", "?:\\Users\\*\\.config\\muse\\auth.json",
      "/Users/*/.local/share/devin/credentials.toml", "?:\\Users\\*\\AppData\\Roaming\\devin\\credentials.toml",
      "/Users/*/.openclaw/credentials/oauth.json", "?:\\Users\\*\\.openclaw\\credentials\\oauth.json",
      "/Users/*/.openclaw/agents/*/agent/auth-profiles.json", "?:\\Users\\*\\.openclaw\\agents\\*\\agent\\auth-profiles.json",
      "/Users/*/.openclaw/agents/*/agent/auth.json", "?:\\Users\\*\\.openclaw\\agents\\*\\agent\\auth.json"
    ) and

    /* SSH public keys are not secrets and are routinely read by SSH and cloud tooling */
    not file.path like~ "*.pub" and

    /* Package ecosystem runtimes or binaries executing from package install locations */
    (
      process.name like~ ("python*", "node", "node.exe", "nodejs", "bun", "bun.exe", "deno", "deno.exe", "ruby*", "php*", "perl*") or
      process.executable like~ ("*/site-packages/*", "*/dist-packages/*", "*/node_modules/*", "*\\site-packages\\*", "*\\node_modules\\*",
                                "*/.venv/*", "*/venv/*", "*\\.venv\\*", "*\\venv\\*")
    )
  ]
  [network where
    cidrmatch(destination.ip, "169.254.169.254/32", "169.254.170.2/32", "169.254.170.23/32", "100.100.100.200/32", "fd00:ec2::254/128", "fd00:ec2::23/128") and
    destination.port == 80 and event.action == "connection_attempted" and host.os.type in ("macos", "windows")
  ]

Framework: MITRE ATT&CKTM