Skip to content

Commit

Permalink
docs: add per-publisher content tabs (pypi#15173)
Browse files Browse the repository at this point in the history
* docs: add per-publisher content tabs

Signed-off-by: William Woodruff <william@trailofbits.com>

* docs: remove unused pictures

Signed-off-by: William Woodruff <william@trailofbits.com>

* docs/user: language

Signed-off-by: William Woodruff <william@trailofbits.com>

* docs/user: sectionify another page

Signed-off-by: William Woodruff <william@trailofbits.com>

* docs/user: tabify security-model

Signed-off-by: William Woodruff <william@trailofbits.com>

* Update docs/user/trusted-publishers/security-model.md

---------

Signed-off-by: William Woodruff <william@trailofbits.com>
Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
  • Loading branch information
woodruffw and di authored Jan 10, 2024
1 parent 4023254 commit ee973b3
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 307 deletions.
42 changes: 22 additions & 20 deletions docs/mkdocs-user-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ site_dir: user-site
plugins:
- macros:
module_name: user/main
j2_block_start_string: '<!--[[%'
j2_block_end_string: '%]]-->'
j2_variable_start_string: '<!--[['
j2_variable_end_string: ']]-->'
j2_block_start_string: "<!--[[%"
j2_block_end_string: "%]]-->"
j2_variable_start_string: "<!--[["
j2_variable_end_string: "]]-->"
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- tables
theme:
name: material
Expand Down Expand Up @@ -46,20 +48,20 @@ edit_uri: blob/main/docs/user/
nav:
- "index.md"
- "Organization Accounts":
- "organization-accounts/index.md"
- "organization-accounts/org-acc-faq.md"
- "organization-accounts/roles-entities.md"
- "Actions":
- "organization-accounts/actions/billing-actions.md"
- "organization-accounts/actions/org-actions.md"
- "organization-accounts/actions/project-actions.md"
- "organization-accounts/actions/team-actions.md"
- "organization-accounts/pricing-and-payments.md"
- "organization-accounts/index.md"
- "organization-accounts/org-acc-faq.md"
- "organization-accounts/roles-entities.md"
- "Actions":
- "organization-accounts/actions/billing-actions.md"
- "organization-accounts/actions/org-actions.md"
- "organization-accounts/actions/project-actions.md"
- "organization-accounts/actions/team-actions.md"
- "organization-accounts/pricing-and-payments.md"
- "Trusted Publishers":
- "trusted-publishers/index.md"
- "trusted-publishers/adding-a-publisher.md"
- "trusted-publishers/creating-a-project-through-oidc.md"
- "trusted-publishers/using-a-publisher.md"
- "trusted-publishers/security-model.md"
- "trusted-publishers/troubleshooting.md"
- "trusted-publishers/internals.md"
- "trusted-publishers/index.md"
- "trusted-publishers/adding-a-publisher.md"
- "trusted-publishers/creating-a-project-through-oidc.md"
- "trusted-publishers/using-a-publisher.md"
- "trusted-publishers/security-model.md"
- "trusted-publishers/troubleshooting.md"
- "trusted-publishers/internals.md"
Binary file removed docs/user/assets/pending-publisher-form.png
Binary file not shown.
Binary file removed docs/user/assets/project-publishing.png
Binary file not shown.
38 changes: 22 additions & 16 deletions docs/user/trusted-publishers/adding-a-publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,34 @@ Then, click on "Publishing" in the project's sidebar:

![](/assets/project-publishing-link.png)

That link will take you to the publisher configuration page for the project:
That link will take you to the publisher configuration page for the project,
which will allow you to configure trusted publishers for the different
platforms supported by PyPI (such as GitHub Actions).

![](/assets/project-publishing.png)
To enable a publisher, you need to tell PyPI how to trust it. Each trusted
publisher has its own configuration requirements; click the tabs below to see
each.

To enable a publisher, you need to tell PyPI how to trust it. For
GitHub Actions (the only currently supported publisher), you do this by
providing the repository owner's name, the repository's name, and the
filename of the GitHub Actions workflow that's authorized to upload to
PyPI.
=== "GitHub Actions"

For example, if you have a project at `https://github.com/octo-org/sampleproject`
that uses a publishing workflow defined in `.github/workflows/release.yml`
and a custom environment named `release`, then you'd do the following:
For GitHub Actions, you **must** provide the repository owner's name, the
repository's name, and the filename of the GitHub Actions workflow that's
authorized to upload to PyPI. In addition, you may **optionally**
provide the name of a [GitHub Actions environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment).

![](/assets/project-publishing-form.png)
For example, if you have a project at `https://github.com/octo-org/sampleproject`
that uses a publishing workflow defined in `.github/workflows/release.yml`
and a custom environment named `release`, then you'd do the following:

!!! note
![](/assets/project-publishing-form.png)

!!! note

Configuring an environment is optional, but **strongly** recommended:
with a GitHub environment, you can apply additional restrictions to
your trusted workflow, such as requiring manual approval on each run
by a trusted subset of repository maintainers.

Configuring an environment is optional, but **strongly** recommended:
with a GitHub environment, you can apply additional restrictions to
your trusted workflow, such as requiring manual approval on each run
by a trusted subset of repository maintainers.

Once you click "Add", your publisher will be registered and will appear
at the top of the page:
Expand Down
26 changes: 16 additions & 10 deletions docs/user/trusted-publishers/creating-a-project-through-oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,31 @@ that will *create* the project when used for the first time. "Pending"
publishers are converted into "normal" publishers on first use, meaning that
no further configuration is required.

The process for configuring a "pending" publisher are similar to those for
The steps for configuring a "pending" publisher are similar to those for
a normal publisher, except that the page is under your account sidebar
instead of any project's sidebar (since the project doesn't exist yet):

![](/assets/publishing-link.png)

Clicking on "publishing" will bring you to the following form:
Clicking on "publishing" will bring you to a page with different potential
trusted publishers. The forms on this page behave
the same as with publishers for existing projects, except that you also need to
provide the name of the PyPI project that will be created.

![](/assets/pending-publisher-form.png)
=== "GitHub Actions"

This form behaves the same as with publishers for existing projects, except that you
also need to provide the name of the PyPI project that will be created.
If you have a repository at
`https://github.com/octo-org/sampleproject` with a release workflow at
`release.yml` and an environment named `release` that you would like to publish
to PyPI as `sampleproject`, then you would do the following:

For example, if you have a repository at
`https://github.com/octo-org/sampleproject` with a release workflow at
`release.yml` and an environment named `release` that you would like to publish
to PyPI as `sampleproject`, then you would do the following:
![](/assets/pending-publisher-form-filled.png)

!!! note

Like with "normal" trusted publishers, configuring a GitHub Actions
environment is **optional but strongly recommended**.

![](/assets/pending-publisher-form-filled.png)

Clicking "Add" will register the "pending" publisher, and show it to you:

Expand Down
177 changes: 96 additions & 81 deletions docs/user/trusted-publishers/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,7 @@ title: Security Model and Considerations

# Security model and considerations

## Security model

GitHub Actions' own security model for OpenID Connect tokens is a little subtle:

* Any workflow defined in a repository can request an OIDC token,
*with any audience*, **so long as it has the `id-token: write` permission**.

* The claims defined in an OIDC token are *bound to the workflow*, meaning
that a workflow defined at `foo.yml` in `org/repo` **cannot impersonate**
a workflow defined at `bar.yml` in `org/repo`. However, if `foo.yml` is
*renamed* to `bar.yml`, then the *new* `bar.yml` will be indistinguishable
from the old `bar.yml` **except** for claims that reflect the repository's
state (e.g. `git` ref, branch, etc.).

* *Generally speaking*, "third party" events **cannot** request an OIDC
token: even if they can trigger the workflow that requests the token,
the actual token retrieval step will fail. For example: PRs issued from forks
of a repository **cannot** access the OIDC tokens in the "upstream"
repository's workflows.

* The exception to this is `pull_request_target` events, which are
**[fundamentally dangerous] by design** and should not be used without
careful consideration.

## Considerations
## General considerations

While more secure than passwords and long-lived API tokens, OIDC publishing
is not a panacea. In particular:
Expand All @@ -41,80 +17,119 @@ is not a panacea. In particular:
one can mint API tokens against it for as long as it lives.

* Configuring a trusted publisher means establishing trust in a particular piece
of external state; that state **must not** be controllable by untrusted
parties. In particular, for trusted publishing with GitHub Actions, you
**must**:
of external state (such as a GitHub Actions workflow); that state **must not**
be controllable by untrusted parties.

In summary: treat your trusted publishers *as if* they were API tokens. If you
wouldn't let a user or piece of code access your API token, then they shouldn't
be able to invoke your trusted publisher.

## Provider-specific considerations

Each trusted publishing provider is its own OIDC identity provider, with its
own security model and considerations.

=== "GitHub Actions"

## Security model

GitHub Actions' own security model for OpenID Connect tokens is a little subtle:

* Any workflow defined in a repository can request an OIDC token,
*with any audience*, **so long as it has the `id-token: write` permission**.

* The claims defined in an OIDC token are *bound to the workflow*, meaning
that a workflow defined at `foo.yml` in `org/repo` **cannot impersonate**
a workflow defined at `bar.yml` in `org/repo`. However, if `foo.yml` is
*renamed* to `bar.yml`, then the *new* `bar.yml` will be indistinguishable
from the old `bar.yml` **except** for claims that reflect the repository's
state (e.g. `git` ref, branch, etc.).

* *Generally speaking*, "third party" events **cannot** request an OIDC
token: even if they can trigger the workflow that requests the token,
the actual token retrieval step will fail. For example: PRs issued from forks
of a repository **cannot** access the OIDC tokens in the "upstream"
repository's workflows.

* The exception to this is `pull_request_target` events, which are
**[fundamentally dangerous] by design** and should not be used without
careful consideration.

## Considerations

* In particular, for trusted publishing with GitHub Actions, you
**must**:

* Trust the correct username and repository: if you trust a repository
other than one you control and trust, that repository can upload to your
PyPI project.
* Trust the correct username and repository: if you trust a repository
other than one you control and trust, that repository can upload to your
PyPI project.

* Trust the correct workflow: you shouldn't trust every workflow
to upload to PyPI; instead, you should isolate responsibility to the
smallest (and least-privileged) possible separate workflow. We recommend
naming this workflow `release.yml`.
* Trust the correct workflow: you shouldn't trust every workflow
to upload to PyPI; instead, you should isolate responsibility to the
smallest (and least-privileged) possible separate workflow. We recommend
naming this workflow `release.yml`.

* Take care when merging third-party changes to your code: if you trust
`release.yml`, then you must make sure that third-party changes to that
workflow (or code that runs within that workflow) are not malicious.
* Take care when merging third-party changes to your code: if you trust
`release.yml`, then you must make sure that third-party changes to that
workflow (or code that runs within that workflow) are not malicious.

* Take care when adding repository contributors, members, and administrators:
by default, anybody who can unconditionally commit to your repository can
also modify your publishing workflow to make it trigger on events you
may not intend (e.g., a manual `workflow_dispatch` trigger).
* Take care when adding repository contributors, members, and administrators:
by default, anybody who can unconditionally commit to your repository can
also modify your publishing workflow to make it trigger on events you
may not intend (e.g., a manual `workflow_dispatch` trigger).

This particular risk can be mitigated by using a dedicated environment
with manual approvers, as described below.
This particular risk can be mitigated by using a dedicated environment
with manual approvers, as described below.

* Trusted publishers are registered to projects, not to users. This means that
removing a user from a PyPI project does **not** remove any trusted publishers
that they might have registered, and that you should include a review
of any/all trusted publishers as part of "offboarding" a project maintainer.
* Trusted publishers are registered to projects, not to users. This means that
removing a user from a PyPI project does **not** remove any trusted publishers
that they might have registered, and that you should include a review
of any/all trusted publishers as part of "offboarding" a project maintainer.

PyPI has protections in place to make some attacks against OIDC more difficult
(like [account resurrection attacks]). However, like all forms of authentication,
the end user is **fundamentally responsible** for applying it correctly.
PyPI has protections in place to make some attacks against OIDC more difficult
(like [account resurrection attacks]). However, like all forms of authentication,
the end user is **fundamentally responsible** for applying it correctly.

In addition to the requirements above, you can do the following to
"ratchet down" the scope of your trusted publishing workflows:
In addition to the requirements above, you can do the following to
"ratchet down" the scope of your trusted publishing workflows:

* **Use per-job permissions**: The `permissions` key can be defined on the
workflow level or the job level; the job level is **always more secure**
because it limits the number of jobs that receive elevated `GITHUB_TOKEN`
credentials.
* **Use per-job permissions**: The `permissions` key can be defined on the
workflow level or the job level; the job level is **always more secure**
because it limits the number of jobs that receive elevated `GITHUB_TOKEN`
credentials.

* **[Use a dedicated environment]**: GitHub Actions supports "environments,"
which can be used to isolate secrets to specific workflows. OIDC publishing
doesn't use any pre-configured secrets, but a dedicated `publish` or `deploy`
environment is a general best practice.
* **[Use a dedicated environment]**: GitHub Actions supports "environments,"
which can be used to isolate secrets to specific workflows. OIDC publishing
doesn't use any pre-configured secrets, but a dedicated `publish` or `deploy`
environment is a general best practice.

Dedicated environments allow for additional protections like
[required reviewers], which can be used to require manual approval for a
workflow using the environment.
Dedicated environments allow for additional protections like
[required reviewers], which can be used to require manual approval for a
workflow using the environment.

For example, here is how `pypa/pip-audit`'s `release` environment
restricts reviews to members of the maintenance and admin teams:
For example, here is how `pypa/pip-audit`'s `release` environment
restricts reviews to members of the maintenance and admin teams:

![](/assets/required-reviewers.png)
![](/assets/required-reviewers.png)

* **[Use tag protection rules]**: if you use a tag-based publishing workflow
(e.g. triggering on tags pushed), then you can limit tag creation and
modification to maintainers and higher (or custom roles) for any tags
that match your release pattern. For example, `v*` will prevent
non-maintainers from creating or modifying tags that match version
strings like `v1.2.3`.
* **[Use tag protection rules]**: if you use a tag-based publishing workflow
(e.g. triggering on tags pushed), then you can limit tag creation and
modification to maintainers and higher (or custom roles) for any tags
that match your release pattern. For example, `v*` will prevent
non-maintainers from creating or modifying tags that match version
strings like `v1.2.3`.

* **Limit the scope of your publishing job**: your publishing job should
(ideally) have only two steps:
* **Limit the scope of your publishing job**: your publishing job should
(ideally) have only two steps:

1. Retrieve the publishable distribution files from **a separate
build job**;
1. Retrieve the publishable distribution files from **a separate
build job**;

2. Publish the distributions using `pypa/gh-action-pypi-publish@release/v1`.
2. Publish the distributions using `pypa/gh-action-pypi-publish@release/v1`.

By using a separate build job, you keep the number of steps that can
access the OIDC token to a bare minimum. This prevents both accidental
and malicious disclosure.
By using a separate build job, you keep the number of steps that can
access the OIDC token to a bare minimum. This prevents both accidental
and malicious disclosure.

[fundamentally dangerous]: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

Expand Down
Loading

0 comments on commit ee973b3

Please sign in to comment.