Published
- 21 min read
Offboarding Needs a Last-Door Receipt
Books by the author
Compare all 5As an Amazon Associate I earn from qualifying purchases. Buying through these links costs you nothing extra and helps pay for the blog.
CrowdSec had already removed most of a departing developer’s access. The departure was amicable, though, and the developer wanted to finish some work. GitHub stayed open for a few more days.
That one exception was enough. On 22 May 2026, someone used an OAuth token tied to the former employee’s account to copy about 170 private repositories in roughly nine minutes. CrowdSec removed the account from its GitHub organization on 25 May, unaware that the copying had already happened. The company learned about the leak nearly four months later, when the archive appeared on a forum on 16 September (CrowdSec: TanStack supply-chain attack analysis).
The story does not support an accusation against the employee. CrowdSec says the laptop had been caught in May’s TanStack package compromise, which stole credentials from developer machines. Nor does the evidence show that an attacker changed CrowdSec code, entered its infrastructure, or reached its databases. The repositories were copied. That distinction matters.
So does the nine-minute gap. Offboarding often gets treated as a list of systems that somebody remembers to disable. The better model is a security transaction with an expected result, a deadline, an owner, and a receipt. If one useful door stays open, the receipt must name it, explain why, narrow what it can reach, and say exactly when it will close.
What CrowdSec found after the archive appeared
CrowdSec’s first public statement on 17 September said a source-code exposure from May had been confirmed. It separated the company’s public Security Engine from the private repositories containing its SaaS console, cloud routines, connectors, and automations. The company initially said no names or other client data had leaked and called the TanStack compromise the likely route (CrowdSec: initial source-code exposure statement).
A fuller report followed on 18 September. It corrected that first account. CrowdSec said the archive held 83 user email addresses used by its data-science team and the names, email addresses, and investment context of 51 potential investors from 2020. It also described one narrowly scoped Amazon Web Services notification credential that somebody had tested in August. CrowdSec said the credential could publish to one notification topic and that the test went no further.
The correction deserves attention because it shows how incident facts change under pressure. A first statement is a timestamped view, not a permanent certificate. CrowdSec’s team spent the next day inventorying repositories, tracing credentials, examining the leaked archive, and working with GitHub. The second account was more specific and less flattering. That is what a useful update looks like.
The main technical facts were clearer by then. CrowdSec dated the repository copying to 22 May, from 05:52:29 to 06:01:33 UTC. The archive contained more than 130 repositories that were already public and about 170 private ones. The private material included the console code, data-science scripts and models, automation, and the thresholds used by the company’s consensus system. CrowdSec said it found no evidence that the actor changed code, build pipelines, infrastructure, or databases (CrowdSec: TanStack supply-chain attack analysis).
That scope keeps the response honest. Source code can expose design detail, make later vulnerability research easier, and reveal old credentials or internal names. It is not the same thing as customer database access. CrowdSec says the exposed code was about four months old by September and had changed substantially. Those facts reduce some consequences without making an unauthorized copy acceptable.
The company also stated a limit that matters for anyone reading the report as a forensic case study. Its own audit trail did not initially reveal the token. The token no longer existed by September, and CrowdSec’s plan did not retain the Git activity needed to reconstruct a May clone from the ordinary log view. GitHub support supplied the narrow historical activity that linked the copying to the departed developer’s account. Without that outside record, the team would have known which credential type was used but not whose account had carried it.
Independent reporting on 19 September compared the two CrowdSec statements and preserved the same boundaries. The Hacker News reported that CrowdSec kept the GitHub access so the former employee could finish work, that other access had already been removed, and that the account was used to copy rather than alter the repositories. It also noted that CrowdSec did not identify the exact malicious package that had reached the laptop and did not publish GitHub’s own findings (The Hacker News: CrowdSec says TanStack attack led to repository copying).
Those missing details should remain missing in our retelling. CrowdSec’s conclusion about TanStack rests on the timing, token history, observed method, and GitHub’s help. The public evidence does not let an outsider independently reproduce every link. The useful lesson does not require pretending otherwise: a valid account retained one valuable permission after departure, and a stolen token used it before the exception closed.
The package compromise supplied the key
The GitHub account was the open door. The TanStack compromise appears to have supplied the key.
On 11 May 2026, an attacker published 84 malicious versions across 42 packages in the TanStack Router and Start repository. TanStack’s postmortem says the packages ran credential-harvesting code during installation. The code searched for GitHub tokens, Secure Shell keys, cloud credentials, package credentials, Kubernetes service-account tokens, and other authority available on the developer or continuous-integration machine (TanStack: npm supply-chain compromise postmortem).
TanStack detected and deprecated the affected versions quickly. Its timeline says an outside researcher raised the alarm 20 to 26 minutes after the malicious publication, with the full set deprecated within about one hour and 43 minutes of the first publish. npm removed the package files later that evening. Every version available after the cleanup was safe according to TanStack’s 15 May update.
Quick cleanup could not pull a stolen token back out of an attacker’s hands. Anyone who installed an affected package while it was available had to treat credentials reachable from that machine as exposed. TanStack explicitly recommended rotation for cloud, GitHub, npm, Secure Shell, Vault, and Kubernetes credentials available to the install host.
CrowdSec says the former employee’s account remained in the GitHub organization until 25 May. The repositories were copied on 22 May, eleven days after the package attack and three days before removal. That timing turns “we removed the account” into an incomplete statement. The security question is not whether access eventually ended. It is whether the account retained useful authority during the interval when a stolen credential could still exercise it.
Two controls that looked sensible in isolation failed to meet at the boundary. CrowdSec had revoked the employee’s other access. TanStack and npm had removed the malicious packages. Yet a GitHub token apparently remained useful because organization membership was still active, and the attacker needed only read access to cause the reported loss.
This is a familiar engineering failure. One team owns the departure ticket. Another owns the source-code organization. A manager grants a short extension for a reasonable business purpose. Security tooling records account removals but may not express the one exception in a place where everybody can see and challenge it. Each local decision appears explainable. The combined state remains dangerous.
The employee’s friendly departure made the exception feel safer, but intent was never the control. A stolen token does not know that somebody left on good terms. It only knows whether the server still accepts it.
The practical response is not to distrust every departing colleague. It is to stop asking goodwill to perform the job of access control. Human relationships determine how a departure should feel. Systems must still end authority on schedule.
Why “remove the user” is only the beginning
GitHub’s own documentation helps separate the account action from the full offboarding result. For enterprises using personal accounts, GitHub recommends removing the user from the enterprise. For Enterprise Managed Users, the identity provider should suspend the account. When offboarding is complete, personal access tokens, Secure Shell keys, and app authorizations can no longer reach enterprise and organization resources (GitHub Docs: user offboarding).
That sounds like one switch, but real estates contain side doors. Outside collaborators must be removed from each repository. A person removed from one organization may still belong to another organization in the enterprise. Internal repositories, enterprise roles, billing roles, direct Copilot licences, private forks, app installations, deployment keys, service accounts, and local copies all have different lifecycles.
GitHub also warns that removing a member does not erase local copies. A former member loses the ability to synchronize private forks with the organization, but the organization remains responsible for handling confidential material already copied. Private repositories forked into another organization can retain a separate access path. An offboarding receipt therefore cannot claim that revocation remotely deletes every past copy (GitHub Docs: removing an organization member).
The same pattern appears outside GitHub. Disabling a single sign-on account may not revoke a cloud access key issued to a command-line tool. Removing a person from a team may leave a repository grant assigned directly to their personal account. Closing email may leave a package registry token, mobile session, continuous-integration secret, or vendor dashboard account alive. A departure ticket can be green while useful authority survives in a system the checklist forgot.
That is why the object being offboarded should be authority, not just the human’s directory entry. Start with the person, then follow every path by which their identity, devices, tokens, applications, and delegated roles can still cause an effect. The result is a graph, not a row in a spreadsheet.
CrowdSec’s incident gives the graph a concrete shape. Other core access had closed, so the actor did not appear in Amazon Web Services. GitHub stayed open, so the OAuth token could read repositories. The narrow exception limited the incident, but it still exposed the exact asset attached to that exception.
A good offboarding process should be able to answer four questions without reconstructing them during a breach. What authority ended? What authority remains? Why does it remain? What evidence will prove its closure?
Most checklists answer only the first question, and often at the product-name level. “GitHub removed” is weaker than “enterprise membership removed, organization membership absent, outside-collaborator grants absent, app authorizations invalid, active tokens tested, and exceptions closed.” The longer sentence is less elegant. It is also testable.
An exception is a small privileged account
Sometimes access genuinely needs to outlive the employment relationship. A developer may need to hand over a release, answer a final technical question, or finish a fixed piece of work. Pretending that such exceptions never happen encourages teams to hide them.
The answer is to treat the exception as newly issued privileged access, not as the old employment account coasting for another week. The old authority should end on time. Any remaining task should get a narrower path with a new owner, a short lifetime, and enough logging to reconstruct what happened.
Suppose a departed developer must finish one migration. Keeping full membership in the main source organization preserves every repository the old role could read. A safer route might expose one repository through a time-limited outside-collaborator grant, from a managed device, with write access only where required. If the task needs advice rather than execution, a screen-sharing session or documented handover may remove the need for direct access entirely.
The distinction matters because “temporary” is not a technical property. An access grant becomes temporary only when a system will revoke it without relying on somebody to remember. A calendar reminder is useful. An automatic expiry plus a failed access test is better.
Every exception also needs a sponsor who owns the risk. The departing employee should not approve their own continuing reach. A manager can explain the business need, but the system owner must decide the smallest authority that satisfies it. Security or platform engineering should define the expiry and evidence. One person may fill more than one role in a small company, but the decisions should remain visible.
Write the exception as a compact contract. Name the account, repository or service, allowed actions, source device, start time, expiry, sponsor, and closure test. State what is explicitly out of scope. “Finish work” is too broad. “Submit changes to repository X before 17:00 UTC, no package publication, no organization administration, no access to other private repositories” gives a reviewer something to enforce.
Move the work away from the departing person’s long-lived tokens. A fresh short-lived credential tied to the exception creates a clean boundary in the audit trail and can be revoked without guessing which older token still matters. If the platform cannot issue such a credential, that limitation should weigh against granting the exception.
CrowdSec’s report says the old OAuth token had disappeared by the time the company investigated. That made attribution harder. A purpose-built exception credential would not have prevented a compromised machine from using its permitted access, but it could have reduced the repositories reachable and made the relevant activity easier to isolate.
This is the central design point: an exception should shrink the blast radius compared with ordinary employment access. If the exception merely leaves the old account untouched, the organization has delayed offboarding rather than designed a controlled handover.
Logs must survive the credential
CrowdSec found a token-shaped clue in the leaked Git configuration, but the credential itself no longer existed in September. Its ordinary organization audit searches did not reveal the historic clone activity. GitHub support reconstructed the relevant Git events within the known time window.
GitHub documents token attribution through a SHA-256 hash recorded as hashed_token, along with the programmatic access type and token scopes. Administrators can search ordinary audit events by that hash. Git events require an audit-log export, and the availability of historic Git activity depends on the product and retention in use (GitHub Docs: identifying audit events by token).
There is a painful operational trap here. Teams often begin looking for a credential only after they have revoked it. If the investigation needs the raw token to calculate its searchable hash, deletion can remove a useful join key. That does not mean keeping exposed secrets in a ticket. It means preserving safe identifiers, event streams, and issuance records before the underlying credential vanishes.
A token inventory should record who or what requested the credential, when it was issued, which scopes it carried, where it was expected to run, its non-secret identifier or hash, and when it was revoked. The raw value belongs in a secrets system and should not be copied into the inventory. The record needs to outlive the token.
Activity records need a similar design. Repository fetches can be the incident, even when no settings change and no commit appears. If your current GitHub plan or retention cannot answer who cloned a private repository four months ago, decide whether to stream those events elsewhere, shorten your detection interval, or accept the gap explicitly. Do not discover the limit only after source code appears online.
Log retention should follow the time it takes your organization to learn about an incident, not the time it hopes to notice one. CrowdSec did not receive the archive until September. The copying happened in May. A seven-day view would have expired many times over.
The strongest record crosses systems. The human-resources departure time, identity-provider suspension, GitHub removal, repository access events, endpoint status, token revocations, and exception expiry should line up on one timeline. When they do not, the mismatch is the finding.
For example, an account removed from the identity provider at 14:00 but still cloning repositories at 14:12 exposes a real control gap. A GitHub exception that expires at 17:00 while the managed laptop stopped checking in at noon should trigger review. A token used from a new country during a handover deserves a different response than the same read from the supervised device.
Logs cannot prevent every theft. They turn a vague story into a bounded one. CrowdSec could say the account copied repositories, did not commit changes, and did not reach the cloud environment because separate records supported those boundaries. Without them, the company would have faced a much larger unknown.
Build an offboarding receipt that can fail
An offboarding receipt should describe observed state after the change, not merely commands that somebody issued. “Ticket says disabled” is intent. “Login denied, organization membership absent, active grants enumerated, and test token rejected” is evidence.
The process starts before the departure time. Build the person’s authority map from the identity provider, source-control platform, cloud accounts, package registries, continuous-integration systems, device management, password manager, support tools, finance systems, and direct vendor accounts. Include identities the company did not create through single sign-on, because those are exactly the accounts central automation may miss.
Next, mark each authority for one of three outcomes: revoke at departure, transfer to a service or successor, or replace with a documented short exception. “Keep for now” is not a fourth outcome. It is an exception without controls.
At the agreed time, revoke in dependency order. Remove interactive sign-in and invalidate sessions. Remove enterprise and organization membership. Remove direct repository and outside-collaborator grants. Revoke personal access tokens, OAuth authorizations, Secure Shell keys, package credentials, cloud sessions, and device certificates. Rotate shared secrets that cannot be attributed safely to one person.
Then test the result from the user’s perspective. An administrator’s settings page can show the right answer while cached credentials still work. Attempt the important actions with a controlled verification method: read a private repository, use the package registry, enter the cloud console, reach the virtual private network, and retrieve a protected secret. The expected result is denial.
A safe verification does not require keeping the former employee’s password. Platforms can report effective access, issue an administrative test identity with the same groups before revocation, or check token status through supported APIs. The principle is to test the authorization outcome rather than trusting that one configuration change propagated everywhere.
The receipt should include the person, departure timestamp, systems checked, observed result, unresolved exceptions, exception expiry, owners, and links to immutable evidence. It should distinguish “not applicable” from “not checked.” Those phrases mean very different things during an incident.
Add one delayed check. Immediate revocation can race with caches, synchronization, or an identity-provider connector. Re-run the high-value queries after the expected propagation window. Search for activity by the removed identity and its known token hashes. An offboarding process that tests only the moment of change can miss a system that quietly restored access on its next synchronization.
Finally, make the receipt fail closed. If a critical system cannot be checked, the departure is not fully closed. Escalate it to a named owner with a deadline and compensating control. If an exception reaches expiry, automatic revocation should happen before the sponsor can request an extension. A new extension should require a new decision.
Here is the sequence in operational terms:
- Freeze the authority map: Capture current memberships, direct grants, tokens, application authorizations, keys, devices, shared secrets, and outside-collaborator access before changing them.
- Classify every path: Mark each path revoke, transfer, or exception. Give every exception a narrow scope, owner, and machine-enforced expiry.
- Revoke the old role: End the person’s normal employment authority on schedule, even when a separate handover path will remain.
- Issue the narrow exception: Use a fresh account or credential where possible. Restrict repositories, actions, device, network source, and lifetime.
- Test denied actions: Prove that old sessions and credentials cannot perform the high-value actions they previously allowed.
- Preserve the receipt: Store non-secret token identifiers, timestamps, effective-access results, exception terms, and evidence where the former account cannot alter them.
- Recheck after propagation: Confirm the state again and search for activity after the departure timestamp.
- Close or escalate: Do not mark the process complete while an unowned exception or untested system remains.
This is longer than clicking “Remove member.” It is still small compared with tracing 170 repositories four months after the fact.
What developers and platform teams should change now
Start with exceptions already in flight. Search current source-control membership for departed staff, contractors whose end dates passed, dormant outside collaborators, and direct repository grants that bypass normal teams. Do not assume the identity provider covers personal accounts or manually granted access.
For each result, ask for the business owner and expiry. If neither exists, remove it. If the access is still needed, replace it with the narrowest supported grant and create a closure test. Friendly history is not a substitute for current ownership.
Check how developer credentials survive device and account changes. GitHub tokens, package credentials, Secure Shell keys, cloud command-line sessions, and secrets stored in configuration files deserve special attention because supply-chain malware searches for exactly those paths. A package install runs with the authority of the machine and user that launched it. The package does not need an exotic exploit when useful credentials are already readable.
Reduce what an install host can expose. Keep production and release credentials away from ordinary developer laptops. Prefer short-lived federation to stored cloud keys. Bind package publication to a dedicated workflow with narrowly scoped identity. Restrict source-control tokens to the repositories and operations they need. These controls cannot stop a malicious package from executing, but they can make the stolen material less useful.
Use managed devices for any post-departure handover. CrowdSec says it did not enforce endpoint detection and response on developer machines in May and has since added endpoint protection for people working with its code or systems. Endpoint software is not a complete answer to malicious dependencies, and CrowdSec says the former employee’s device might no longer have carried company protection anyway. That is another reason not to let old personal-account access linger after the managed-device relationship ends.
Review GitHub audit coverage before you need it. Confirm which Git events your plan records, how long they remain available, whether you can export them, and whether token hashes appear in the data you retain. Run a harmless repository clone with a test token, revoke the token, then prove that your team can still find the event from the preserved identifier.
Exercise the exception path. Create a test leaver, preserve one narrow repository grant for an hour, and verify that ordinary access closes while the exception works only as designed. Let it expire. Confirm the old token and the exception credential both fail afterward. A policy that has never produced a denial is still a theory.
The Secure Harness argues that authority should live inside externally enforced boundaries rather than a person’s memory or a tool’s good intentions. Offboarding is the same engineering problem. The departure workflow requests a safer state. Independent checks must prove that the state exists.
The final door matters most
CrowdSec’s incident was not caused by leaving every system open. Most access had already been removed. That containment appears to be why the actor copied source code without reaching the company’s cloud environment or databases.
The remaining GitHub access still mattered. It held the asset the attacker wanted, and a credential stolen during a separate supply-chain incident could exercise it. Three days later, the account was gone. By then, the repositories had already left.
That is the uncomfortable part of partial revocation: ninety-nine closed doors do not cancel the authority behind the hundredth. The last exception defines the remaining blast radius.
A mature offboarding process does not forbid every handover or treat a departing colleague as an adversary. It removes the old role, rebuilds any genuine exception as narrow temporary access, and proves closure from the outside. The receipt records what ended, what remained, who accepted it, and when the final door actually shut.
CrowdSec’s first account changed as the evidence improved. Its access model had limited the incident, but one justified exception outlived the protection around the employee’s device. Both lessons belong in the record. Good security comes from preserving those distinctions rather than choosing between self-congratulation and panic.
If an account is leaving today, do not ask only whether somebody clicked disable. Ask what the old identity can still read, what its surviving tokens can still do, and which test proves the answer. Offboarding is complete when the last useful path has a denial receipt.
If you want practical security writing without a daily alarm bell, the newsletter is one email per month. The signup lives on this site.
Sources
- CrowdSec: TanStack Supply Chain Attack Analysis, accessed 2026-09-19
- CrowdSec: Statement on Source Code Exposure in May 2026, accessed 2026-09-19
- The Hacker News: CrowdSec Says TanStack npm Attack Led to Copy of 170 Private GitHub Repositories, accessed 2026-09-19
- TanStack: Postmortem, npm Supply-Chain Compromise, accessed 2026-09-19
- GitHub Docs: About User Offboarding on GitHub Enterprise Cloud, accessed 2026-09-19
- GitHub Docs: Removing a Member from Your Organization, accessed 2026-09-19
- GitHub Docs: Identifying Audit Log Events Performed by an Access Token, accessed 2026-09-19