Skip to content

Commit cd0c0a5

Browse files
authored
Publish blog regarding leaked personal access token (#16232)
1 parent ee0a453 commit cd0c0a5

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
title: "Incident Report: Leaked GitHub Personal Access Token"
3+
description: We responded to an incident related to a leaked
4+
GitHub Personal Access Token for a PyPI administrator.
5+
authors:
6+
- ewdurbin
7+
date: 2024-07-08
8+
tags:
9+
- transparency
10+
- security
11+
---
12+
13+
On June 28, 2024 [email protected] and I (Ee Durbin) were notified of
14+
a leaked GitHub Personal Access Token for my GitHub user account, `ewdurbin`.
15+
This token was immediately revoked,
16+
and a review of my GitHub account and activity was performed.
17+
No indicators of malicious activity were found.
18+
19+
<!-- more -->
20+
21+
## Timeline of events
22+
23+
- 2023-MM-DD[^1]:
24+
A GitHub Personal Access Token was created for `ewdurbin`[^2].
25+
This token had access to all the organizations and repositories as my User,
26+
including `pypi`, `python`, `psf`, and `pypa`.
27+
- 2023-MM-DD[^3]:
28+
`cabotage/cabotage-app:v3.0.0b35` pushed to hub.docker.com
29+
containing GitHub Personal Access Token for `ewdurbin` in a `.pyc` file.
30+
- 2023-MM-DD[^3]:
31+
`cabotage/cabotage-app:v3.0.0b110` pushed to hub.docker.com
32+
containing GitHub Personal Access Token for `ewdurbin` in a `.pyc` file.
33+
- 2024-06-21:
34+
`cabotage/cabotage-app:v3.0.0b35` and `cabotage/cabotage-app:v3.0.0b110`
35+
removed from hub.docker.com for reasons unrelated to this report[^4].
36+
- 2024-06-28 7:09 AM Eastern:
37+
Brian Moussalli of JFrog reports their finding of the
38+
GitHub Personal Access Token for `ewdurbin` to [email protected] and Ee's
39+
personal email address.
40+
- 2024-06-28 7:26 AM Eastern:
41+
GitHub Personal Access Token for `ewdurbin` destroyed.
42+
43+
## How did this happen
44+
45+
While developing `cabotage-app`[^5] locally, working on the build portion
46+
of the codebase, I was consistently running into GitHub API rate limits.
47+
These rate limits apply to anonymous access. While in production the system
48+
is configured as a GitHub App, I modified my local files to include my own access
49+
token in an act of laziness, rather than configure a `localhost` GitHub App.
50+
These changes were never intended to be pushed remotely.
51+
52+
```diff
53+
diff --git a/cabotage/celery/tasks/build.py b/cabotage/celery/tasks/build.py
54+
index 0f58158..3b88b5d 100644
55+
--- a/cabotage/celery/tasks/build.py
56+
+++ b/cabotage/celery/tasks/build.py
57+
@@ -395,7 +395,10 @@ def build_release_buildkit(release):
58+
59+
60+
def _fetch_github_file(
61+
- github_repository="owner/repo", ref="main", access_token=None, filename="Dockerfile"
62+
+ github_repository="owner/repo",
63+
+ ref="main",
64+
+ access_token="0d6a9bb5af126f73350a2afc058492765446aaad",
65+
+ filename="Dockerfile",
66+
):
67+
g = Github(access_token)
68+
try:
69+
@@ -407,7 +410,13 @@ def _fetch_github_file(
70+
return None
71+
```
72+
73+
While I was keenly aware of the risk of leaking the token on `.py` files, `.pyc`
74+
files containing the compiled bytecode weren't considered.
75+
76+
At the time, staging deployments were performed using the following script which
77+
_attempted_ but failed to remove temporarily applied changes including the
78+
hardcoded secret.
79+
80+
```bash
81+
#!/bin/bash
82+
83+
generation=$(cat generation)
84+
git stash
85+
docker buildx build --platform linux/amd64,linux/arm64 -t cabotage/cabotage-app:v3.0.0b${generation} --push .
86+
kubectl -n cabotage set image deployment/cabotage-app cabotage-app=cabotage/cabotage-app:v3.0.0b${generation} cabotage-app-worker=cabotage/cabotage-app:v3.0.0b${generation} cabotage-app-beat=cabotage/cabotage-app:v3.0.0b${generation}
87+
git stash pop
88+
echo $((generation + 1)) > generation
89+
```
90+
91+
As the application had been running locally in Docker on a shared volume,
92+
`.pyc` files containing local and uncommitted changes were still present.
93+
A minimal [`.dockerignore` file](https://github.com/cabotage/cabotage-app/blob/c412d71b6b0ad45b7cd55d41800bf75bb0e0ea9f/.dockerignore)
94+
at the time did not exclude `__pycache__` or `*.pyc` files from the build,
95+
leaking the secret.
96+
97+
## Response
98+
99+
Aside from revoking the token and reviewing more or less all GitHub audit logs
100+
and account activity available to me for possible malicious usage of the token,
101+
a few changes have been made to mitigate future risk of this kind of leak.
102+
103+
Cabotage is now entirely self-hosting, which means that builds of the cabotage-app
104+
no longer utilize a public registry[^6] and deployment builds are initiated from
105+
clean checkouts of source only.
106+
This mitigates the scenario of local edits making it into an image build outside
107+
of development environments,
108+
as well as removing the need to publish to public registries.
109+
110+
I have revoked the one and only GitHub access token related to my account,
111+
and will avoid creating one in the future unless absolutely necessary,
112+
moreover ensuring that they have a built in expiration.
113+
Aside from this spurious usecase I cannot recall another time where a long-lived
114+
token for my user has been helpful and having one around to potentially
115+
leverage when I'm lazy seems risky rather then helpful.
116+
117+
## Takeaways
118+
119+
This is a great reminder
120+
to set aggressive expiration dates for API tokens (If you need them at all),
121+
treat `.pyc` files as if they were source code,
122+
and perform builds on automated systems from clean source only[^7].
123+
124+
## Thanks
125+
126+
First and foremost, thanks to JFrog's team for finding and reporting this leak.
127+
We are grateful for the entire community of security researchers
128+
undertaking scanning of public repositories like Docker Hub and PyPI itself.
129+
PyPI relies on the efforts of this community for detecting
130+
[Malware uploaded to PyPI](/posts/2024-03-06-malware-reporting-evolved/)
131+
and
132+
[integrates with GitHub](https://github.blog/changelog/2021-03-22-the-python-package-index-is-now-a-github-secret-scanning-integrator/)
133+
to automatically handle leaked PyPI credentials in commits as well as
134+
[in public issues](/posts/2023-08-17-github-token-scanning-for-public-repos/).
135+
Cooperation between all parties helps to improve the security of open source,
136+
and none of us could do it alone.
137+
138+
---
139+
140+
_Ee Durbin is the Director of Infrastructure at
141+
the Python Software Foundation.
142+
They have been contributing to keeping PyPI online, available, and
143+
secure since 2013._
144+
145+
146+
[^1]:
147+
Exact date is unknown, as
148+
[GitHub Account Security Logs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-security-log#accessing-your-security-log)
149+
are not available beyond 90 days.
150+
[^2]:
151+
Specific permissions for this token are not known as they are not retained
152+
in the GitHub Account Security Log
153+
and were not noted before the token was destroyed.
154+
We've asked JFrog if their findings include the token level permissions
155+
and will update this post if they provide them.
156+
[^3]:
157+
Publication dates are not known because hub.docker.com does not retain any
158+
history for images which have been removed.
159+
We've asked JFrog if their findings include the publication dates
160+
and will update this post if they provide them.
161+
[^4]:
162+
`cabotage-app` had moved its builds to an automated system. The images on
163+
hub.docker.com were no longer necessary and were removed proactively.
164+
[^5]:
165+
cabotage is the codebase that deploys the
166+
[warehouse codebase](https://github.com/pypi/warehouse)
167+
as well as related services that compose PyPI.
168+
You can read more about cabotage in our blog post from our
169+
[security audit of the codebase](/posts/2023-11-14-3-security-audit-remediation-cabotage/).
170+
[^6]:
171+
Better still, the internal private registry used for cabotage builds uses
172+
[fine-grained Docker authentication](https://github.com/cabotage/cabotage-app/blob/0c960f9a6683d0ced7a3be7a757edf87aff5695b/cabotage/server/models/projects.py#L586-L601)
173+
to allow only designated Kubernetes ServiceAccounts access to images.
174+
[^7]:
175+
PyPI for instance supports
176+
[Trusted Publishers](https://docs.pypi.org/trusted-publishers/).

0 commit comments

Comments
 (0)