Skip to content
The Future of Agentic Cyber Defense is Here: Vega II is Live | Register
Blog

OpenCode Console Breach: Finding the Provider Keys Your Logs Can’t See

19 Oct 20269 min
Pixel-art Vega research banner: a warning window reading Threat Detected over a rain-soaked landscape.

Key findings

  • OpenCode says 1.4% of Console users, all of them with external providers linked, may have had saved provider keys exposed through a legacy system it was decommissioning.
  • The OpenCode CLI and desktop app aren’t named as affected: keys configured locally never reach OpenCode’s servers, so the exposure is limited to what was saved in Console.
  • Logs can show who used Console, but not who stored a key or whether it was abused; those answers come from workspace admins, OpenCode’s notice, and provider usage records.
  • Across the environments we checked, OpenCode traffic was common, Console activity was rare and mostly sign-ins, and connected-provider traffic was absent, which proves nothing because a stored key never has to be used.

On October 17, 2026, OpenCode disclosed a breach of a legacy system it was shutting down, which it described as “related to a large scale distillation attack”. It said 1.4% of Console users were affected, all of them users who had linked external model providers. According to recipients, its email said provider keys saved in their workspaces may have been exposed. Some users have reported unexpected charges, and staff at Anthropic and Google have posted that keys they believed were affected were revoked or blocked.

OpenCode said, “If you did not get an email you were not impacted.” Security teams may still want to check for themselves. Notices may have gone straight to developers rather than to security, and a revoked key doesn’t tell you who connected it or whether it was misused before revocation. Vega Threat Research used visibility across customer environments to work out what the logs can answer, what they can’t, and where to look instead. This post shares that approach and the queries behind it.

What was and wasn’t compromised

  • OpenCode CLI and desktop app: not named as compromised. They run locally, and according to OpenCode’s Dax Raad, provider keys configured locally don’t reach OpenCode’s servers.
  • OpenCode Console: the affected service. Console stores external provider credentials. The breach of its legacy infrastructure may have exposed these keys for 1.4% of Console users, all of whom had linked external providers.

The distinction matters for scoping. Local OpenCode use wasn’t the issue. Credentials stored in Console were potentially exposed.

Three questions your logs can and can’t answer

  • Who uses Console? Gateway, DNS, and endpoint logs can help build a list of people to ask.
  • Who stored a provider key? A workspace admin, the workspace’s configuration, or its audit records may help. Gateway logs alone usually can’t.
  • Was that key exposed or abused? That likely needs OpenCode’s notice or your provider’s usage records.

Logs mostly answer the first question, which tells you who to ask about the second.

Step 1: Start with the notice and the workspace admins

The notice reportedly had the subject line “Action Required: Potential provider key breach” (VibeIncidents). Search your full message trace for it, not just security alerts. If your organization uses a Console workspace, ask its admins. The Providers page shows current connections, and OpenCode says Console keeps an audit log of configuration changes.

Step 2: Gateway logs show who uses Console

If your gateway logs full URL paths, the path can separate Console activity from ordinary OpenCode use. These paths come from OpenCode’s API docs, and the legacy system may have used others. In rough priority order:

  • Look first: /inference/custom/<connection_id>/... is traffic through a Console-connected provider.
  • Look: /console/api/v2/config is a workspace config fetch, likely by a signed-in person or service account.
  • If authenticated: other /inference/... paths are hosted model use. Free models need no sign-in.
  • Lead only: /console/auth/device/... and other /console pages are a sign-in attempt or a page view.
  • Usually ignore: models.opencode.ai, /update, /download, and /zen are catalog, update, or Zen traffic.

Response codes help too. A 401 means a bad credential, a 403 means a valid one without access, and device token requests return 400 until sign-in is approved. A stored key may never be used, so no /inference/custom/ traffic doesn’t mean no key.

Step 3: Endpoint and cloud logs fill the gaps

Endpoint DNS. If your EDR records which process made each DNS lookup, opencode resolving opencode.ai points to machines running the app. That works even without a decrypting gateway, though it won’t show Console use.

Bedrock in CloudTrail. Console can connect Bedrock with an API key. Look for:

  • IAM users named BedrockAPIKey-xxx, the default for keys created in the Bedrock console.
  • Calls flagged with additionalEventData.callWithBearerToken = true, which indicate API-key authentication, if your pipeline retains that field.
  • User agents starting with opencode/, which we observed from local OpenCode installations.

The CloudTrail query at the end of this post looks for the first and third. It doesn’t check callWithBearerToken, so where that field is available, search it separately. It also doesn’t cover Bedrock Mantle, which logs inference as CreateInference data events that are off by default. Unfamiliar source IPs may be worth investigating, but we don’t know Console’s egress IPs or user agents, so treat them as leads, not proof.

Bedrock invocation logs. If you log Bedrock model invocations, the records identify the IAM user or role making each request. Unlike CloudTrail, they generally don’t include source IPs or user agents. These logs can point to identities worth investigating, but they don’t establish which credential was used or whether it was stored in Console.

For Anthropic, OpenAI, and Google keys, check those providers’ usage records.

What we saw

This tells us who to ask, not whether anyone stored a key. Our coverage was uneven, so treat it as a rough picture:

  • Several organizations had OpenCode traffic at the gateway, and some showed Console activity across more than a dozen identities, mostly weak signals such as sign-in attempts.
  • We found no connected-provider traffic in the windows we could search.
  • Domain counts overstate exposure. In one organization, hundreds of people reached the model catalog in a day, but only a handful touched Console that week.
  • Endpoint DNS filled gaps. In one organization without gateway visibility, it showed several people running OpenCode.
  • Bedrock usage appeared local. The calls we sampled came from corporate, home, or the organizations’ own cloud addresses. That’s consistent with local use, but it doesn’t rule out Console.

Why zero doesn’t mean clean

  • A stored key doesn’t have to be used. Someone could have added a provider key in Console without ever sending a model request through it, and the key could still have been exposed.
  • The breached system was a legacy one. It may have used different URLs from those documented today.
  • A key may predate your log retention.
  • Gateway visibility is limited. Many gateways don’t record full URL paths. In one feed we checked, none of the OpenCode events had one.

No matching traffic doesn’t mean no credential was at risk.

What should defenders do?

  • Build a list from mail, gateway, and endpoint logs.
  • Ask each person and workspace admin whether a provider was connected, and with which credential. Include service accounts.
  • Rotate any key that was connected to Console, at the provider. Create a new key, move consumers over, then delete the old one. Removing a key from Console doesn’t stop anyone who already copied it.
  • Review provider usage from at least October 16.
  • Set enforced spend caps where available.

Hunt for it yourself

1. Gateway: Console activity by identity

Field names vary by source, so expect to adapt them. On large feeds, start with a 1 to 3 day window.

@data_type:oovqu/Security-Service-Edge
| where url.domain endswith "opencode.ai" or url.url_string contains "opencode.ai/"
| extend url_host = tolower(extract("^([a-zA-Z][a-zA-Z0-9+.-]*://)?([^/:?#]+)", 2, tostring(url.url_string)))
| extend host = iff(isempty(url_host), tolower(tostring(url.domain)), url_host)
| where host == "opencode.ai" or host endswith ".opencode.ai"
| extend raw_path = tostring(url.path)
| extend path = tolower(iff(isempty(raw_path) or raw_path == "null", tostring(url.url_string), raw_path))
| where path contains "/console" or path contains "/inference/"
| extend status = tostring(http_response.code)
| extend signal = case(
    path contains "/inference/custom/", "1 - connected-provider inference",
    path contains "/console/api/v2/config", "2 - console config fetch",
    path contains "/inference/", "3 - hosted model inference",
    path contains "/console/auth/device/code", "3 - device login started",
    path contains "/console/auth/device/token", "3 - device token request",
    path contains "/console/auth", "3 - console auth",
    "3 - console browsing")
| extend identity = iff(isempty(tostring(actor.user.name)), "(no identity)", tostring(actor.user.name))
| summarize requests = count(), status_logged = countif(isnotempty(status)), ok_2xx = countif(status startswith "2"),
    first_seen = min(timestamp), last_seen = max(timestamp), user_agents = make_set(user_agent, 5)
  by identity, signal, host
| sort by signal asc, requests desc

Read signal as a rough priority, from 1 (look first) to 3 (a lead). If status_logged is zero, your gateway probably doesn’t log response status. If it’s above zero but ok_2xx is zero, the requests were likely refused.

2. CloudTrail: Bedrock API keys and OpenCode clients

@CloudTrail
| where service.name == "bedrock.amazonaws.com"
| where event_name in ("InvokeModel", "InvokeModelWithResponseStream", "Converse", "ConverseStream")
| where actor.user.name startswith "BedrockAPIKey" or tostring(user_agent) contains "opencode"
| summarize calls = count(), opencode_ua_calls = countif(tolower(tostring(user_agent)) contains "opencode"),
    ip_count = dcount(src_endpoint.ip), source_ips = make_set(src_endpoint.ip, 20),
    user_agents = make_set(user_agent, 6), first_seen = min(timestamp), last_seen = max(timestamp)
  by actor.user.name
| sort by opencode_ua_calls desc, calls desc

Start with identities that have OpenCode calls, then look at source IPs: addresses outside your egress ranges, or one key used from far more addresses than its owner would expect.

3. Bedrock invocation logs: IAM users calling models

@Amazon-Bedrock-Model-Invocation
| extend direct_arn = tostring(_raw.identity.arn)
| extend arn = iff(isempty(direct_arn) or direct_arn == "null", tostring(parse_json(tostring(_raw.message)).identity.arn), direct_arn)
| where arn contains ":user/"
| summarize events = count(), models = make_set(ai_model.name, 5), first_seen = min(timestamp), last_seen = max(timestamp) by arn
| sort by events desc

Each result identifies an IAM user that invoked a model, not necessarily a Bedrock API key. Confirm the credential type and ownership before drawing conclusions.

Conclusion

The OpenCode Console breach is a credential exposure, and the logs most teams keep weren’t built to answer who stored a credential where. Gateway, DNS, and endpoint telemetry can tell you who ran OpenCode and who touched Console. They can’t tell you who pasted a provider key into a workspace, and a key that was never used leaves no trace at all. The work is to build the list, ask the people and admins on it, rotate at the provider, and read the provider’s usage records from October 16 onward.

References

OpenCodeAPI keyscredential exposurethreat huntingAWS BedrockCloudTrailthreat research

Read more

All articles