In short

In December 2022 malware on a CircleCI engineer's laptop, undetected by antivirus, stole a valid 2FA-backed single sign-on session. The attacker used it to impersonate the engineer, whose role let them generate production access tokens, and on 22 December exfiltrated customers' environment variables, tokens and keys. The data was encrypted at rest, but the attacker extracted the encryption keys from a running process. CircleCI disclosed on 4 January 2023 and told every customer to rotate every secret stored on the platform. For operators, the lesson is that a stolen session must not equal production access; for CI users, it is to store as few long-lived secrets in CI as possible and use short-lived OIDC credentials instead.

  • MFA protected the sign-in, not the session: once the session cookie was stolen, 2FA no longer stood in the way.
  • One engineer's routine privilege, generating production access tokens, was enough to reach customer secret stores.
  • Encryption at rest did not help, because the keys were taken from a running process that could decrypt.
  • Customers could not tell whether their secrets had been used elsewhere; CircleCI told everyone to rotate everything and to investigate from 16 December 2022.
  • CircleCI's changes: step-up authentication for production, fewer people with access, more ephemeral permissions, OAuth token rotation and a move to GitHub Apps, and audit logs for all plans.
  • As a CI user, prefer OIDC federation (CIRCLE_OIDC_TOKEN_V2 on CircleCI) so a provider breach leaks nothing that outlives the job.

Incident at a glance

Dates
to
Category
Credential theft
Organization
  • CircleCI
Impact
A session stolen from an engineer's laptop gave production access; customers' environment variables, tokens and keys were exfiltrated and decrypted with keys taken from a running process. Every customer was told to rotate every stored secret.
How it was found
A customer reported suspicious GitHub OAuth activity on 29 December 2022; CircleCI's investigation traced it to the engineer's laptop by 4 January 2023.

Timeline

  1. Malware on an engineer's laptop steals a 2FA-backed SSO session.
  2. The attacker begins reconnaissance in production systems.
  3. Customer environment variables, tokens and keys are exfiltrated.
  4. A customer reports suspicious GitHub OAuth activity.
  5. CircleCI cuts production access, rotates hosts and discloses; customers are told to rotate all secrets.
  6. Rotation of customers' GitHub OAuth tokens completes.
  7. CircleCI publishes its incident report.

Verify it

  1. List every secret stored in your CI provider (project variables, contexts, SSH and deploy keys, API tokens) with what it can reach, and mark which could be replaced by OIDC federation.
  2. Confirm cloud deploys from CI use short-lived credentials from an OIDC trust that is restricted to your organization, project and branch.
  3. If you run a platform: check whether a stolen employee session could mint production access without a fresh, phishing-resistant authentication step.
  4. Check where your service decrypts customer secrets, which processes hold the keys, and who can reach those processes.
  5. Rehearse 'the CI provider was breached': how long would it take to revoke and replace every secret, and who would do it?
Applies to
  • CircleCI cloud Secrets stored on the platform up to 5 January 2023
  • Any hosted CI/CD service that stores customer secrets The same exposure applies to project variables, contexts, deploy keys and OAuth tokens

Teams that operate a SaaS or production platform holding customer credentials, and teams that store secrets in a hosted CI/CD service.

Part of
Workflow: Respond to a vulnerability. Collection: case studies

Fix it in your stack

Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

Learning goals and prerequisitesAbout 26 minutes: 10 to read and 16 for the exercise

What you will learn

  • Trace how a stolen session on one laptop reached customers' secrets despite 2FA and encryption at rest.
  • Design production access that a stolen session cannot use on its own.
  • Replace stored cloud keys in CI with OIDC federation restricted to a project and branch.
  • Plan and order a full rotation after a CI provider breach.

Before you start

Engineers who operate SaaS platforms with privileged production access, and teams that store secrets in hosted CI/CD

  • Know how SSO sessions and session cookies work after a 2FA sign-in.
  • Know how CI secrets are stored and injected into jobs.
  • For the code: Python 3 for the rotation plan; a CircleCI project and an AWS account to try the OIDC configuration.

Refresh first: Contain a leaked secret, then investigate and clean up

Go to the guided practice

From a laptop to customers' CI secrets

CircleCIEngineer's laptopAttackerCircleCIproductionCustomers'systemsStolen 2FA-backed SSOsession1Mint production accessas the engineer2Customer env vars,tokens and keys3Encryption keys from arunning process4Stolen customer credentials5Trust boundaryUntrusted input1Step, described below
  1. Steal the sessionMalware on an engineer's laptop takes a valid SSO session cookie that had already passed 2FA.
  2. Impersonate the engineerFrom another location, the attacker uses the session as the engineer, whose role includes generating production access tokens.
  3. Reach the secret storesWith production access, the attacker reads a subset of databases and stores holding customer environment variables, tokens and keys.
  4. DecryptThe data is encrypted at rest, so the attacker extracts the encryption keys from a running process.
  5. Use the secrets elsewhereStolen customer credentials work against third-party systems such as GitHub, which is how a customer first noticed.
Each control held on its own terms: 2FA at sign-in, encryption at rest. The session, the role and the running process sat outside them.

One laptop, one session, every customer's secrets

CircleCI is a hosted CI/CD service. Its customers store the credentials their pipelines need, such as cloud keys, package-registry tokens and deploy keys, as project environment variables and contexts on the platform. On 4 January 2023 CircleCI told every customer to rotate all of them. Its incident report, published on 12 January (13 January in UTC), explains why.

'An unauthorized third party leveraged malware deployed to a CircleCI engineer's laptop in order to steal a valid, 2FA-backed SSO session. This machine was compromised on December 16, 2022. The malware was not detected by our antivirus software.' The malware performed 'session cookie theft, enabling them to impersonate the targeted employee in a remote location and then escalate access to a subset of our production systems.' Because the employee 'had privileges to generate production access tokens as part of the employee's regular duties', the attacker could 'access and exfiltrate data from a subset of databases and stores, including customer environment variables, tokens, and keys.'

The data was encrypted at rest. That did not matter: 'the third party extracted encryption keys from a running process, enabling them to potentially access the encrypted data.' CircleCI's report is the only primary source for the intrusion; it does not name the attacker.

Timeline

Timeline
When (UTC)EventSource
2022-12-16Malware compromises a CircleCI engineer's laptop and steals a 2FA-backed SSO session.CircleCI report
2022-12-19The attacker begins reconnaissance in production.CircleCI report
2022-12-22Customer data is exfiltrated; CircleCI's last record of unauthorized activity in production.CircleCI report
2022-12-29A customer alerts CircleCI to suspicious GitHub OAuth activity.CircleCI report
2022-12-31CircleCI starts rotating all customers' GitHub OAuth tokens on their behalf (completed 7 January).CircleCI report
2023-01-04CircleCI cuts off the employee, then production access for nearly all employees, and rotates all production hosts by 22:30.CircleCI report
2023-01-0502:30: disclosure to customers, asking them to rotate every secret; 03:26: all Project API Tokens revoked.CircleCI report
2023-01-06Personal API Tokens created before 5 January revoked; Bitbucket tokens rotated with Atlassian.CircleCI report
2023-01-07 to 12CircleCI and AWS notify customers of potentially affected AWS tokens.CircleCI report
2023-01-13CircleCI publishes its incident report (12 January, US Pacific time).CircleCI report

Why 2FA and encryption did not stop it

Two-factor authentication protects the moment of sign-in. After that, the session cookie is the credential, and malware on the endpoint can copy it. The same pattern ran through the Okta support-system breach later in 2023 (/domains/identity-crypto/authentication/case-study-okta-har-2023/), and the session guide (/domains/identity-crypto/authentication/session-lifecycle/) covers what limits a stolen session: short lifetimes, binding to the device, and re-authentication before sensitive actions.

The decisive step was the role. The engineer could generate production access tokens as a normal part of the job, so a stolen session became production access with no further check. CircleCI's response attacked exactly that: production access was cut back to 'a very limited number of employees', and 'For the employees who retain production access, we have added additional step-up authentication steps and controls ... even in the case of a stolen 2FA-backed SSO session.' Its plan added 'reducing session trust, adding additional authentication factors, and performing more regular access rotations' and 'making our system permissions more ephemeral'.

Encryption at rest protects disks and backups, not data that a running service decrypts. If a process can decrypt customer secrets, anyone who controls that process can too. Limit which services ever hold decryption keys, keep that step in a separate, tightly accessed component, and monitor it. The envelope encryption guide (/domains/identity-crypto/key-management-pki/envelope-encryption-rotation/) covers key separation.

What it meant for customers: rotate everything

From the customer's side, the platform that held their secrets had been breached and they could not know which secrets had been used. CircleCI was explicit: 'If you stored secrets on our platform during this time period, assume they have been accessed', and 'there is no way for us to know if your secrets were used for unauthorized access to those third-party systems.' It advised investigating activity from 16 December 2022 until each secret was rotated, and published IP addresses, VPN providers, file names and a domain to search for. When the report was published, 'fewer than 5 customers' had reported unauthorized access to third-party systems.

CircleCI built tooling to help: a script to find secrets for rotation, an updated_at field in the Contexts API to confirm rotation, and audit logs made available to customers on all plans. The rotation still fell on every customer, for every secret, at once. The leaked secret guide (/domains/identity-crypto/secrets/leaked-secret-response/) covers how to run a rotation like that; the plan script below orders it.

The best position to be in was to have stored nothing worth stealing. CircleCI's own best-practice list starts with 'Use OIDC tokens wherever possible to avoid storing long-lived credentials in CircleCI.' A job that federates to the cloud provider with a token valid for one hour leaves nothing in the CI platform that outlives the job. The workload identity guide (/domains/identity-crypto/identity/workload-identity-models/) covers the trust setup.

Deploy with OIDC instead of stored cloud keys

This CircleCI configuration uses the circleci/aws-cli orb, pinned to 5.4.2, the current release, to exchange the job's CIRCLE_OIDC_TOKEN_V2 for AWS credentials through sts:AssumeRoleWithWebIdentity, for 15 minutes. No AWS key is stored in CircleCI. The YAML parses; it was checked against CircleCI's OIDC documentation and the orb's source, not run in a CircleCI project for this page.

Configuration.circleci/config.ymlYAML
# .circleci/config.yml: deploy with short-lived AWS credentials from CircleCI's
# OIDC token instead of a stored AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@5.4.2

jobs:
  deploy:
    executor: aws-cli/default
    steps:
      - checkout
      - aws-cli/setup:
          role_arn: arn:aws:iam::123456789012:role/circleci-deploy
          role_session_name: circleci-deploy
          session_duration: "900" # 15 minutes, the minimum AWS allows
      - run: aws sts get-caller-identity # proves which role the job holds

workflows:
  release:
    jobs:
      - deploy:
          context: aws-deploy # restrict this context to the release group
          filters:
            branches:
              only: main

Restrict which jobs can assume the role

The AWS role's trust policy decides who can use it. CircleCI's issuer is https://oidc.circleci.com/org/<organization_id>, the default audience is the organization ID, and the V2 sub claim names the organization, project, user, VCS origin and ref. The policy below accepts tokens only for one project's main branch. Replace ORG_ID and PROJECT_ID with your IDs, and add the OIDC provider to IAM first. Without the sub condition, any project in your CircleCI organization could deploy.

Configurationtrust-policy.jsonJSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.circleci.com/org/ORG_ID"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {"oidc.circleci.com/org/ORG_ID:aud": "ORG_ID"},
        "StringLike": {
          "oidc.circleci.com/org/ORG_ID:sub": "org/ORG_ID/project/PROJECT_ID/user/*/vcs-origin/*/vcs-ref/refs/heads/main"
        }
      }
    }
  ]
}

Order the rotation when your CI provider is breached

The script reads an inventory of the secrets stored in your CI platform and prints the order to act in: production first, then by kind (cloud and signing keys before others) and breadth of scope. Secrets that can be replaced by OIDC are revoked rather than reissued. It ran with Python 3.14 on a five-secret sample. Keep the inventory current before you need it; building it during an incident is the slow part.

Testrotation_plan.pyPython
"""Plan secret rotation after a CI provider breach: assume every stored secret leaked.

Input: a JSON inventory of the secrets stored in the CI platform, one entry per
secret: name, kind (cloud-key, deploy-key, api-token, signing-key, oauth...),
scope (what it can reach), prod (true/false), oidc_possible (true/false).
Output: the order to act in, and which secrets to replace with OIDC instead of reissuing.
"""
import json
import sys

KIND_WEIGHT = {"cloud-key": 3, "signing-key": 3, "deploy-key": 2, "api-token": 2, "oauth": 2}


def plan(secrets):
    def priority(s):
        return (s.get("prod", False), KIND_WEIGHT.get(s["kind"], 1), len(s.get("scope", [])))

    for s in sorted(secrets, key=priority, reverse=True):
        action = "revoke now; replace with OIDC" if s.get("oidc_possible") else "rotate now"
        yield f"{s['name']:<24} {s['kind']:<12} {'prod' if s.get('prod') else 'non-prod':<9} {action}"


if __name__ == "__main__":
    with open(sys.argv[1], encoding="utf-8") as f:
        for line in plan(json.load(f)):
            print(line)

Which controls would have stopped or limited it?

Which controls would have stopped or limited it?
ControlWhoSSDF practice or standardGuide
Endpoint detection and hardening for engineers with production accessPlatform operatorSSDF PO.5.2/domains/supply-chain/ci-cd/developer-workstation-security/
Step-up, phishing-resistant authentication before minting production access; short, bound sessionsPlatform operatorSSDF PO.5.1; ASVS V7/domains/identity-crypto/authentication/session-lifecycle/
Few people with standing production access; just-in-time, expiring grantsPlatform operatorSSDF PO.5.1/domains/runtime/cloud-posture/cloud-iam-trust/
Decrypt customer secrets only in an isolated component whose keys no general process holdsPlatform operatorSSDF PS.1.1; ASVS V13.3.1/domains/identity-crypto/key-management-pki/envelope-encryption-rotation/
Audit logs for every customer, so they can investigate the providerPlatform operatorASVS V16.2.1/domains/runtime/detection/security-event-logging/
Short-lived OIDC credentials instead of stored cloud keys; scoped contextsCI userASVS V13.2.1/domains/identity-crypto/identity/workload-identity-models/
An inventory of CI secrets and a rehearsed rotationCI userSSDF RV.2.2/domains/identity-crypto/secrets/leaked-secret-response/

Lessons that still apply

  • A session is a credential. Protect what it can do: step-up authentication for production, short lifetimes, device binding.
  • Standing production access for routine work turns one stolen session into a breach. Make access just-in-time and expiring.
  • Encryption at rest does not protect data from a process that can decrypt it.
  • As a CI user, assume your provider can be breached; store as little as possible there and federate the rest.
  • Treat it as a systems failure, as CircleCI did: 'a security incident is a systems failure', not one employee's mistake.

What the public evidence does not establish

CircleCI's report does not say how the malware reached the engineer's laptop, who was behind the attack, or how many customers' secrets were read or later used; at publication, fewer than five customers had reported unauthorized access to third-party systems, which is a lower bound. CircleCI engaged third-party investigators to validate its findings, but their report is not public. The timeline and mechanism on this page come from CircleCI alone; no regulator or independent review has published findings on the incident.

Guided practice

It is 5 January 2023 and your hosted CI provider has just told every customer to rotate all secrets stored on it. Decide what to do with each item. Treat the facts as true.

Supplied scenario material
Plain text
E1: AWS access key for a deploy user with AdministratorAccess, stored as a project variable since 2020.
E2: npm publish token for your public packages, in an org-wide context.
E3: GitHub deploy key with write access to the infrastructure repo.
E4: Slack webhook for build notifications.
E5: Nothing else is logged: you have CloudTrail for AWS, npm and GitHub audit logs, and no record of when E1 was last used outside CI.
  1. 1. Order the work

    In what order do you act, and why?

    Expected result and reasoning

    E1, then E2 and E3, then E4. E1 can change your whole AWS account; E2 can ship code to your users; E3 can change infrastructure. Revoke before you rotate where a replacement is not needed immediately.

  2. 2. Replace E1

    What replaces E1?

    Expected result and reasoning

    An IAM role assumed through the CI provider's OIDC, restricted to your organization, project and main branch, with only the permissions the deploy needs; then delete the access key.

  3. 3. Investigate

    What do you search, and over what window?

    Expected result and reasoning

    CloudTrail for E1's key, npm publish history for E2, and GitHub audit logs for E3, from 16 December 2022 until each secret was revoked, including the provider's published IP addresses.

  4. 4. Prevent a repeat

    What changes so the next provider breach is a non-event?

    Expected result and reasoning

    Short-lived federated credentials for cloud and npm publishing, deploy keys replaced by app installations with narrow scopes, and a maintained inventory of every secret in CI with a rotation runbook.

Your deliverable: An ordered response list with the replacement for each secret, the logs you checked and the window, and two changes to how secrets live in CI.

The fixture is fictional. Whether npm or another registry accepts OIDC-based publishing from a given CI provider depends on that registry's support; check it before relying on it.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. CircleCI: Jan 4, 2023 security incident report. 12 January 2023 (US Pacific).
  2. CircleCI docs: Using OpenID Connect tokens in jobs. Updated 28 September 2026.
  3. CircleCI-Public/aws-cli-orb: setup command and web identity example. v5.4.2 (15 June 2026).
  4. AWS: Create a role for OpenID Connect federation
  5. OWASP ASVS 5.0.0: V13 Configuration. Version 5.0.0.

Reference information

ASVS 5.0.0 requirements
v5.0.0-13.2.1 · v5.0.0-13.3.1

Download this guide as Markdown