Skip to content

Commit bf19fdb

Browse files
woodruffwhugovkdi
authored
[OIDC] User docs (#13285)
* docs/user: OIDC doc skeleton Signed-off-by: William Woodruff <[email protected]> * docs/user: dump the OIDC docs in Signed-off-by: William Woodruff <[email protected]> * OIDC publishing: retitle Signed-off-by: William Woodruff <[email protected]> * docs: recase Signed-off-by: William Woodruff <[email protected]> * docs: remove misbehaving directory Signed-off-by: William Woodruff <[email protected]> * docs: fixup Signed-off-by: William Woodruff <[email protected]> * docs: fix images Signed-off-by: William Woodruff <[email protected]> * user/main: remove unused macro Signed-off-by: William Woodruff <[email protected]> * docs: subpages! Signed-off-by: William Woodruff <[email protected]> * mkdocs: add nav Signed-off-by: William Woodruff <[email protected]> * docs: fix headers, use breakout Signed-off-by: William Woodruff <[email protected]> * docs: add missing preview breakout Signed-off-by: William Woodruff <[email protected]> * Losslessly squash assets by ~40% * Apply suggestions from code review Co-authored-by: Hugo van Kemenade <[email protected]> * Update docs/user/OIDC-publishing/adding-a-publisher.md Co-authored-by: Hugo van Kemenade <[email protected]> * Apply suggestions from code review Co-authored-by: Dustin Ingram <[email protected]> * Apply suggestions from code review Co-authored-by: Dustin Ingram <[email protected]> * Apply suggestions from code review Co-authored-by: Dustin Ingram <[email protected]> * Update docs/user/main.py Co-authored-by: Dustin Ingram <[email protected]> * docs: rename entire tree Signed-off-by: William Woodruff <[email protected]> * docs: remove "above" ref Signed-off-by: William Woodruff <[email protected]> * docs: OIDC -> trusted Signed-off-by: William Woodruff <[email protected]> * docs: emphasize permission preference * docs: add a diff * docs: document risks with untrusted committers Signed-off-by: William Woodruff <[email protected]> * docs: remove ref to beta Unnecessary in this context. Signed-off-by: William Woodruff <[email protected]> * Apply suggestions from code review Co-authored-by: Hugo van Kemenade <[email protected]> * docs: use a canonical PyPI project name Signed-off-by: William Woodruff <[email protected]> * docs: more picture updates Signed-off-by: William Woodruff <[email protected]> * Apply suggestions from code review --------- Signed-off-by: William Woodruff <[email protected]> Co-authored-by: Hugo van Kemenade <[email protected]> Co-authored-by: Dustin Ingram <[email protected]>
1 parent 03fe217 commit bf19fdb

20 files changed

+541
-6
lines changed

docs/mkdocs-user-docs.yml

+22-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ site_dir: user-site
44
plugins:
55
- macros:
66
module_name: user/main
7-
markdown_extensions:
8-
- admonition
9-
- pymdownx.details
7+
markdown_extensions:
8+
- admonition
9+
- pymdownx.details
1010
- pymdownx.superfences
1111
- tables
1212
theme:
@@ -38,3 +38,22 @@ extra:
3838
link: https://twitter.com/pypi
3939
repo_url: https://github.com/pypi/warehouse
4040
edit_uri: blob/main/docs/user/
41+
42+
nav:
43+
- "index.md"
44+
- "Organization Accounts":
45+
- "organization-accounts/index.md"
46+
- "organization-accounts/org-acc-faq.md"
47+
- "organization-accounts/roles-entities.md"
48+
- "Actions":
49+
- "organization-accounts/actions/billing-actions.md"
50+
- "organization-accounts/actions/org-actions.md"
51+
- "organization-accounts/actions/project-actions.md"
52+
- "organization-accounts/actions/team-actions.md"
53+
- "Trusted Publishers":
54+
- "trusted-publishers/index.md"
55+
- "trusted-publishers/adding-a-publisher.md"
56+
- "trusted-publishers/creating-a-project-through-oidc.md"
57+
- "trusted-publishers/using-a-publisher.md"
58+
- "trusted-publishers/security-model.md"
59+
- "trusted-publishers/troubleshooting.md"

docs/user/assets/dropdown.png

39 KB
Loading

docs/user/assets/logo.png

-1.04 KB
Loading

docs/user/assets/manage-link.png

80.7 KB
Loading
Loading
78 KB
Loading
Loading
Loading
73 KB
Loading
16.9 KB
Loading
43.7 KB
Loading

docs/user/assets/publishing-link.png

11.7 KB
Loading
88.6 KB
Loading

docs/user/main.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
from pathlib import Path
2+
3+
4+
OIDC_PUBLISHING = """
5+
!!! info
6+
7+
OpenID Connect publishing functionality is currently in closed beta.
8+
9+
You can request access to the closed beta using
10+
[this form](https://forms.gle/XUsRT8KTKy66TuUp7).
11+
12+
**NOTE**: Access to the OIDC beta is provided on a *per-user* basis: users
13+
can register OIDC publishers against projects once added to the beta, but
14+
other maintainers/owners of the project can't modify OIDC settings unless
15+
they're *also* in the beta.
16+
"""
17+
118
ORG_ACCOUNTS = """
219
!!! info
320
@@ -8,9 +25,9 @@
825
to be one of the first to know how you can begin using them.
926
"""
1027

11-
PREVIEW_FEATURES = {
12-
'org-accounts': ORG_ACCOUNTS
13-
}
28+
PREVIEW_FEATURES = {"oidc-publishing": OIDC_PUBLISHING, "org-accounts": ORG_ACCOUNTS}
29+
30+
_HERE = Path(__file__).parent.resolve()
1431

1532

1633
def define_env(env):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Adding a Trusted Publisher to an Existing PyPI Project
3+
---
4+
5+
{{ preview('oidc-publishing') }}
6+
7+
# Adding a trusted publisher to an existing PyPI project
8+
9+
Adding a trusted publisher to a PyPI project only requires a single setup step.
10+
11+
On the ["Your projects" page](https://pypi.org/manage/projects/), click "Manage" on any project you'd like to
12+
configure:
13+
14+
![](/assets/manage-link.png)
15+
16+
Then, click on "Publishing" in the project's sidebar:
17+
18+
![](/assets/project-publishing-link.png)
19+
20+
That link will take you to the publisher configuration page for the project:
21+
22+
![](/assets/project-publishing.png)
23+
24+
To enable a publisher, you need to tell PyPI how to trust it. For
25+
GitHub Actions (the only currently supported publisher), you do this by
26+
providing the repository owner's name, the repository's name, and the
27+
filename of the GitHub Actions workflow that's authorized to upload to
28+
PyPI.
29+
30+
For example, if you have a project at `https://github.com/pypa/sampleproject`
31+
that uses a publishing workflow defined in `.github/workflows/release.yml`,
32+
then you'd do the following:
33+
34+
![](/assets/project-publishing-form.png)
35+
36+
Once you click "Add", your publisher will be registered and will appear
37+
at the top of the page:
38+
39+
![](/assets/project-publisher-registered.png)
40+
41+
From this point onwards, the `release.yml` workflow on `pypa/sampleproject` will
42+
be able to generate short-lived API tokens from PyPI for the project you've registered
43+
it against.
44+
45+
A publisher can be registered against multiple PyPI projects (e.g. for a
46+
multi-project repository), and a single PyPI project can have multiple
47+
publishers (e.g. for multiple workflows on different architectures, operating
48+
systems).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
title: Creating a PyPI Project with a Trusted Publisher
3+
---
4+
5+
{{ preview('oidc-publishing') }}
6+
7+
# Creating a PyPI project with a trusted publisher
8+
9+
Trusted publishers are not just for pre-existing PyPI projects: you can also use
10+
them to *create* a PyPI project!
11+
12+
This again reduces the number of steps needed to set up a fully automated PyPI
13+
publishing workflow: rather than having to manually upload a first release
14+
to "prime" the project on PyPI, you can configure a "pending" publisher
15+
that will *create* the project when used for the first time. "Pending"
16+
publishers are converted into "normal" publishers on first use, meaning that
17+
no further configuration is required.
18+
19+
The process for configuring a "pending" publisher are similar to those for
20+
a normal publisher, except that the page is under your account sidebar
21+
instead of any project's sidebar (since the project doesn't exist yet):
22+
23+
![](/assets/publishing-link.png)
24+
25+
Clicking on "publishing" will bring you to the following form:
26+
27+
![](/assets/pending-publisher-form.png)
28+
29+
This form behaves the same as with publishers for existing projects, except that you
30+
also need to provide the name of the PyPI project that will be created.
31+
32+
For example, if you have a repository at
33+
`https://github.com/octo-org/sampleproject` with a release workflow at
34+
`release.yml` and you'd like to publish it to PyPI as `sampleproject`, you'd do
35+
the following:
36+
37+
![](/assets/pending-publisher-form-filled.png)
38+
39+
Clicking "Add" will register the "pending" publisher, and show it to you:
40+
41+
![](/assets/pending-publisher-registered.png)
42+
43+
From this point on, the "pending" publisher can be used exactly like a
44+
"normal" publisher, and after first use it will convert it into a "normal"
45+
publisher.

docs/user/trusted-publishers/index.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
title: Getting Started
3+
---
4+
5+
{{ preview('oidc-publishing') }}
6+
7+
# Publishing to PyPI with a Trusted Publisher
8+
9+
## Confirming that you're in the beta
10+
11+
Before we do anything else: let's confirm that you're actually in the
12+
beta! You can do this by [logging into PyPI](https://pypi.org/account/login/)
13+
and clicking on "Account settings" in the dropdown under your profile:
14+
15+
![](/assets/dropdown.png)
16+
17+
On the "Account settings" page, you should see a right-side menu that
18+
contains a "Publishing" link:
19+
20+
![](/assets/publishing-link.png)
21+
22+
If you see that link and can click on it, then you're in the beta group!
23+
24+
## Quick background: Publishing with OpenID Connect
25+
26+
OpenID Connect (OIDC) publishing is a mechanism for uploading packages to PyPI, *complementing*
27+
existing methods (username/password combinations, API tokens).
28+
29+
You don't need to understand OIDC to use OIDC publishing with PyPI, but here's
30+
the TL;DR:
31+
32+
1. Certain CI services (like GitHub Actions) are OIDC *identity providers*, meaning that
33+
they can issue short-lived credentials ("OIDC tokens") that a third party
34+
can **strongly** verify came from the CI service (as well as which user,
35+
repository, etc. actually executed);
36+
1. Projects on PyPI can be configured to trust a particular configuration on
37+
a particular CI service, making that configuration an OIDC publisher
38+
for that project;
39+
1. Release automation (like GitHub Actions) can submit an OIDC token
40+
to PyPI. The token will be matched against configurations trusted by
41+
different projects; if any projects trust the token's configuration,
42+
then PyPI will mint a *short-lived API token* for those projects and
43+
return it;
44+
1. The short-lived API token behaves exactly like a normal project-scoped API
45+
token, except that it's only valid for 15 minutes from time of creation
46+
(enough time for the CI to use it to upload packages).
47+
48+
This confers significant usability and security advantages when compared
49+
to PyPI's traditional authentication methods:
50+
51+
* Usability: with trusted publishing, users no longer need to manually create
52+
API tokens on PyPI and copy-paste them into their CI provider. The only
53+
manual step is configuring the publisher on PyPI.
54+
* Security: PyPI's normal API tokens are long-lived, meaning that an attacker
55+
who compromises a package's release can use it until its legitimate user
56+
notices and manually revokes it. Similarly, uploading with a password means
57+
that an attacker can upload to *any* project associated with the account.
58+
Trusted publishing avoids both of these problems: the tokens minted expire
59+
automatically, and are scoped down to only the packages that they're
60+
authorized to upload to.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: Security Model and Considerations
3+
---
4+
5+
{{ preview('oidc-publishing') }}
6+
7+
# Security model and considerations
8+
9+
## Security model
10+
11+
GitHub Actions' own security model for OpenID Connect tokens is a little subtle:
12+
13+
* Any workflow defined in a repository can request an OIDC token,
14+
*with any audience*, **so long as it has the `id-token: write` permission**.
15+
16+
* The claims defined in an OIDC token are *bound to the workflow*, meaning
17+
that a workflow defined at `foo.yml` in `org/repo` **cannot impersonate**
18+
a workflow defined at `bar.yml` in `org/repo`. However, if `foo.yml` is
19+
*renamed* to `bar.yml`, then the *new* `bar.yml` will be indistinguishable
20+
from the old `bar.yml` **except** for claims that reflect the repository's
21+
state (e.g. `git` ref, branch, etc.).
22+
23+
* *Generally speaking*, "third party" events **cannot** request an OIDC
24+
token: even if they can trigger the workflow that requests the token,
25+
the actual token retrieval step will fail. For example: PRs issued from forks
26+
of a repository **cannot** access the OIDC tokens in the "upstream"
27+
repository's workflows.
28+
29+
* The exception to this is `pull_request_target` events, which are
30+
**[fundamentally dangerous] by design** and should not be used without
31+
careful consideration.
32+
33+
## Considerations
34+
35+
While more secure than passwords and long-lived API tokens, OIDC publishing
36+
is not a panacea. In particular:
37+
38+
* Short-lived API tokens are still sensitive material, and should not be
39+
disclosed (ideally not at all, but certainly not before they expire).
40+
41+
* OIDC tokens themselves are sensitive material, and should not be disclosed.
42+
OIDC tokens are also short-lived, but an attacker who successfully intercepts
43+
one can mint API tokens against it for as long as it lives.
44+
45+
* Configuring an OIDC publisher means establishing trust in a particular piece
46+
of external state; that state **must not** be controllable by untrusted
47+
parties. In particular, for OIDC publishing with GitHub Actions, you **must**:
48+
49+
* Trust the correct username and repository: if you trust a repository
50+
other than one you control and trust, that repository can upload to your
51+
PyPI project.
52+
53+
* Trust the correct workflow: you shouldn't trust every workflow
54+
to upload to PyPI; instead, you should isolate responsibility to the
55+
smallest (and least-privileged) possible separate workflow. We recommend
56+
naming this workflow `release.yml`.
57+
58+
* Take care when merging third-party changes to your code: if you trust
59+
`release.yml`, then you must make sure that third-party changes to that
60+
workflow (or code that runs within that workflow) are not malicious.
61+
62+
* Take care when adding repository contributors, members, and administrators:
63+
by default, anybody who can unconditionally commit to your repository can
64+
also modify your publishing workflow to make it trigger on events you
65+
may not intend (e.g., a manual `workflow_dispatch` trigger).
66+
67+
This particular risk can be mitigated by using a dedicated environment
68+
with manual approvers, as described below.
69+
70+
PyPI has protections in place to make some attacks against OIDC more difficult
71+
(like account resurrection attacks). However, like all forms of authentication,
72+
the end user is **fundamentally responsible** for applying it correctly.
73+
74+
In addition to the requirements above, you can do the following to
75+
"ratchet down" the scope of your OIDC publishing workflows:
76+
77+
* **Use per-job permissions**: The `permissions` key can be defined on the
78+
workflow level or the job level; the job level is **always more secure**
79+
because it limits the number of jobs that receive elevated `GITHUB_TOKEN`
80+
credentials.
81+
82+
* **[Use a dedicated environment]**: GitHub Actions supports "environments,"
83+
which can be used to isolate secrets to specific workflows. OIDC publishing
84+
doesn't use any pre-configured secrets, but a dedicated `publish` or `deploy`
85+
environment is a general best practice.
86+
87+
Dedicated environments allow for additional protections like
88+
[required reviewers], which can be used to require manual approval for a
89+
workflow using the environment.
90+
91+
For example, here is how `pypa/pip-audit`'s `release` environment
92+
restricts reviews to members of the maintenance and admin teams:
93+
94+
![](/assets/required-reviewers.png)
95+
96+
* **[Use tag protection rules]**: if you use a tag-based publishing workflow
97+
(e.g. triggering on tags pushed), then you can limit tag creation and
98+
modification to maintainers and higher (or custom roles) for any tags
99+
that match your release pattern. For example, `v*` will prevent
100+
non-maintainers from creating or modifying tags that match version
101+
strings like `v1.2.3`.
102+
103+
* **Limit the scope of your publishing job**: your publishing job should
104+
(ideally) have only two steps:
105+
106+
1. Retrieve the publishable distribution files from **a separate
107+
build job**;
108+
109+
2. Publish the distributions using `pypa/gh-action-pypi-publish@release/v1`.
110+
111+
By using a separate build job, you keep the number of steps that can
112+
access the OIDC token to a bare minimum. This prevents both accidental
113+
and malicious disclosure.
114+
115+
[fundamentally dangerous]: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
116+
117+
[Use a dedicated environment]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
118+
119+
[Use tag protection rules]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-tag-protection-rules
120+
121+
[required reviewers]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#required-reviewers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: Troubleshooting
3+
---
4+
5+
{{ preview('oidc-publishing') }}
6+
7+
# Troubleshooting
8+
9+
Here's a quick enumeration of errors you might see from the `mint-token`
10+
endpoint:
11+
12+
* `not-enabled`: this indicates that PyPI's backend has
13+
disabled OIDC entirely. You should not see this message during normal
14+
operation, **unless** PyPI's admins have decided to disable OIDC support.
15+
* `invalid-payload`: the OIDC token payload submitted to the `mint-token`
16+
endpoint is not formatted correctly. The payload **must** be a JSON serialized
17+
object, with the following layout:
18+
19+
```json
20+
{
21+
"token": "oidc-token-here"
22+
}
23+
```
24+
25+
No other layouts are supported.
26+
27+
* `invalid-token`: the OIDC token itself is either formatted incorrectly,
28+
has an invalid signature, is expired, etc. This encompasses pretty much
29+
any failure mode that can occur with an OIDC token (which is just a JWT)
30+
*before* it's actually matched against a publisher.
31+
* `invalid-pending-publisher` and `invalid-publisher`: the OIDC token itself
32+
is well-formed (and has a valid signature), but doesn't match any known
33+
(pending) OIDC publisher. This likely indicates a mismatch between the
34+
OIDC publisher specified in the user/project settings and the claims
35+
represented in the actual OIDC token. Check for typos!

0 commit comments

Comments
 (0)