Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trusted Publishing Support on Crates.io #3691

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

mdtro
Copy link

@mdtro mdtro commented Sep 10, 2024

/cc @rust-lang/crates-io

A big thank you to @woodruffw for co-authoring, providing prior art through PyPi's implementation, and all of the expert advice. 🙏

Rendered

@mdtro mdtro marked this pull request as ready for review September 10, 2024 17:39
@Turbo87 Turbo87 added the T-crates-io Relevant to the crates.io team, which will review and decide on the RFC. label Sep 10, 2024
@woodruffw
Copy link

Thank you so much for your hard work authoring this @mdtro! It was my honor and pleasure to be able to help.

As one of the people who built the equivalent Trusted Publishing feature on PyPI, I'm more than happy to answer any technical or policy questions the Rust community has, as well as offer insight into PyPI's experience (which IMO has been extremely successful) over the past 18 months of having Trusted Publishing deployed.

@programmerjake
Copy link
Member

programmerjake commented Sep 10, 2024

I think we should try to support 3rd-party websites that have their own gitlab/forgejo/gitea/etc. instances, so e.g. gitlab.example.com could publish to whatever crates they own even though they aren't using gitlab.com's CI and instead are running their own CI infrastructure.

this could perhaps be done by, when CI asks crates.io for an OIDC token, having crates.io use oauth/oidc to check that gitlab.example.com grants permission for CI uploads through a token provided to crates.io by CI

@woodruffw
Copy link

I think we should try to support 3rd-party websites that have their own gitlab/forgejo/gitea/etc. instances, so e.g. gitlab.example.com could publish to whatever crates they own even though they aren't using gitlab.com's CI and instead are running their own CI infrastructure.

It's ultimately up to each index to decide a subjective cutoff for IdP "popularity," but I would caution against this: the main security benefit of trusted publishing versus an API token is that large CI/CD providers have dedicated OIDC IdP maintenance and operation teams that handle the burden of maintaining an OIDC PKI. For one-off instances, the benefits of a PKI versus ordinary API tokens are marginal and may even invert, since maintaining a PKI is significantly more operationally complicated than securing a single API token.

(For PyPI, this is one of the reasons we started with GitHub, and then moved to support GitLab, Google Cloud Build, etc., but haven't yet moved to support third-party instances of GH or GL.)

@di
Copy link

di commented Sep 11, 2024

(For PyPI, this is one of the reasons we started with GitHub, and then moved to support GitLab, Google Cloud Build, etc., but haven't yet moved to support third-party instances of GH or GL.)

+1 to what @woodruffw said, also to add to this: PyPI has a notion of "organizations", and one thing we are considering is for PyPI is to permit self-hosted IdPs 1:1 with organizations on a case by case basis.

@lyphyser
Copy link

I think it should really be implemented so that you need BOTH a crates.io API token AND the OpenID Connect identity token.

Otherwise, if there is a bug in the OpenID Connect implementation by GitHub/Google/etc., someone exploiting it could take over all crates using it without having to take over the actual developer machines or CI systems where the API token would be stored; this also guarantees that security is strictly improved since even if the OpenID Connect implementation on crates.io's side were totally broken, it would still be as secure as the current system.

The best way to do this seems to be to change the crates.io API token creation UI to have the option to also require an OpenID Connect identity to be provided to accept requests using that token.

@woodruffw
Copy link

Otherwise, if there is a bug in the OpenID Connect implementation by GitHub/Google/etc., someone exploiting it could take over all crates using it without having to take over the actual developer machines or CI systems where the API token would be stored; this also guarantees that security is strictly improved since even if the OpenID Connect implementation on crates.io's side were totally broken, it would still be as secure as the current system.

Could you elaborate on the threat model you're envisioning here? We considered similar scenarios when building out the threat model for trusted publishing on PyPI, and ultimately came to the conclusion that an attacker who is sufficiently powerful to control a major OIDC IdP (like Google's or GitHub's) would almost certainly also have sufficient power to control CI-side user-configured credentials.

Or in other words: we couldn't think of an internally coherent threat model in which an attacker is simultaneously strong enough to take over a major OIDC IdP but too weak to compromise an individual CI process on that IdP's platform (and thereby exfiltrate a manually-configured crates.io API token).

(More broadly, I think Trusted Publishing's security and usability benefits become moot if they require two credentials - one manual - instead of just an automatic one: the goal is to remove error prone manual steps and opportunities for over-scoping/accidental disclosure, both of which would still exist if the user would still need to configure a crates.io API token.)

Copy link
Member

@Turbo87 Turbo87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good work @mdtro and everyone involved! I'm excited for this to land in crates.io :)


These tokens have some security flaws:

1. By default, they are long-lived and do not expire.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very minor nitpick, we've actually changed the default last week to expire in 90 days 😄


1. (required) The owning GitHub username or organization
2. (required) The repository name
3. (required) The workflow file name (must be located in `.github/workflows/`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the rationale for requiring this instead of making it optional?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not strictly required, but I would strongly recommend requiring it: making this claim optional would mean that any workflow in the source repository could generate an OIDC token with the ability publish. Requiring it to be scoped to a single workflow significantly reduces the opportunity for compromise here.

Additionally, the use case of wanting to publish from multiple workflows with a single configuration is not a strong one, I think. Usually there is just a single workflow which can publish, and if a user really needs to publish from multiple workflows, they can always configure multiple publishers with varying workflow filenames instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring it to be scoped to a single workflow significantly reduces the opportunity for compromise here.

I'm not sure what the threat model here is. If an attacker is able to add a new workflow to the repository then wouldn't they also be able to overwrite the existing publish workflow?

the use case of wanting to publish from multiple workflows with a single configuration is not a strong one, I think

yeah, I agree with that. I was thinking more about an unknowing dev renaming a workflow file in some refactoring without realizing that this breaks the publish pipeline.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The threat would be third-party code that is necessary for the workflow to function being compromised to steal OIDC credentials (or API tokens, for that matter).

Consider two separate workflows: one that does testing & linting, another that handles publishing on release. The former may introduce a lot of dependencies that aren't required for publishing, but are required for testing/linting. If this test/lint workflow also has the ability to generate OIDC credentials that can be used for publishing, this unnecessarily increases the surface area that the OIDC token is exposed to.

(Note that for GitHub as an IdP specifically, this may be sort of a moot point, because providing the token to the workflow requires an explicit configuration in the workflow with id-token: write, however this is fairly easy to over-configure.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, what you're saying is that the actions used inside of the workflow might get compromised. makes sense, thanks for the clarification. I wonder if we should include a sentence about it in the RFC so that it's documented.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And specifically, actions used inside of workflows not named release.yml being compromised is a lot easier to contemplate. This functionally reduces the scope of your audit to just the release workflow.

uses: actions/checkout@v4

- name: Install dependencies
run: cargo build --release
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo publish should already download everything that is needed. do we need this additional step for anything?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure the crate successfully builds before acquiring the token. If the crate takes 1+ hour to build, the token might expire during the cargo publish verification checks.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note some related discussion below, where one idea was cargo publish itself could manage token provisioning and do it immediately before uploading an already built crate to avoid this problem

}
```

In addition to the above parameters, the configuration will be linked to a particular crates.io user or team to administer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct. It may be a leftover from the "pending state" idea in an earlier draft. The configs discussed here should be in a one-to-one or many-to-one relationship with a crate, but not a user or team.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it makes more sense to make the relationship between the project and the publisher, and confer ability to modify the configuration on any user who can otherwise modify the project.

[unresolved-questions]: #unresolved-questions

- Should crate owners be able to configure the allowed token scopes for a Trusted Publisher configuration?
- We could default to `publish-new` and `publish-update`, but maybe it's best to allow this to be configurable?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the config in this RFC can only be added to existing crates I don't think this the token scopes question applies here


- Should crate owners be able to configure the allowed token scopes for a Trusted Publisher configuration?
- We could default to `publish-new` and `publish-update`, but maybe it's best to allow this to be configurable?
- How long should an access token derived from the ID token exchange be valid for?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we know what the other registries have chosen and the reasons for their choices?

I guess an "issue" is that cargo by default builds the crate before it is sent to crates.io. Depending on how long this build process takes, the token may have already expired by the time cargo sends the crate to crates.io.

I wonder how viable it would be to provide a config option for the token lifetime.

Alternatively: cargo provides support for other authentication methods these days, e.g. to integrate with 1Password. Would it be viable to integrate the authentication flow through that? It would presumably allow us to request the token from crates.io just in time before the upload happens. Admittedly I don't know at what point cargo talks to the auth provider process. It might happen before the build process too. Might be worth exploring though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like integrating this into Cargo's default set of auth providers also avoids the separate "auth" repository/action maintenance, which feels like more of a "new thing" in our release processes etc. It also seems like native integration in Cargo is probably better for users -- the risk of using the wrong repository (or an outdated version) is probably reduced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's up to the cargo team whether they would want to make this a built-in thing. It looks like they have quite a long to-do list already though 😅

I don't think the action would have to be integrated with the Rust release process though. All it needs is a repository with some git tags, which shouldn't be too hard to set up an maintain.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PyPI, the token is valid for 15 minutes from the time that it is minted. Our reasoning for this is that 15 minutes is generally enough time to perform any uploading needed, and anyone that needed more time could always re-request a token.

I guess an "issue" is that cargo by default builds the crate before it is sent to crates.io. Depending on how long this build process takes, the token may have already expired by the time cargo sends the crate to crates.io.

I'm a little unfamiliar with the general workflow here, but it might not be necessary to exchange an OIDC token for an upload token prior to the build, i.e. this could happen after a build completes, but just before an upload will happen instead, so your only concern is the length of time of the upload.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 minutes is generally enough time to perform any uploading needed

if I understand correctly this kind of originates in PyPI allowing file uploads after a version was created? (vs. crates.io where you need to upload your one crate file upfront)

this could happen after a build completes, but just before an upload will happen instead, so your only concern is the length of time of the upload.

the short version is cargo publish performs both the build and the upload and there isn't really a step in between that we could hook into from the outside (except maybe with the auth providers that I mentioned above). the uploads are currently limited to 30sec anyway due to hosting provider limitations.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I understand correctly this kind of originates in PyPI allowing file uploads after a version was created? (vs. crates.io where you need to upload your one crate file upfront)

Yea, kind of, more like it originates from a PyPI release consisting of multiple files, which are uploaded in multiple separate requests rather than a single request, and that some of those files can be huge, which takes some time to upload.

the short version is cargo publish performs both the build and the upload and there isn't really a step in between that we could hook into from the outside

I guess the question is whether the OIDC token exchange needs to happen externally (and prior to) cargo publish being invoked, or whether the token exchange is the responsibility of cargo publish, in which case it could happen between the build & publish steps.

For PyPI, we chose to do the exchange in our canonical upload workflow prior to invoking twine upload, but we have plans to make twine upload support handling the exchange directly: pypa/twine#999. In either case, the build has already happened though, so we can keep this window relatively small.

- Support providing a list of crate names in the crates.io UI to simplify configuration for monorepos that publish multiple crates.
- Support for additional trusted publishers (i.e. GitLab, CircleCI).
- Support for custom assertions on OIDC ID token claims.
- [Additional claims supported by GitHub Actions OIDC ID tokens](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some discussion of machine-accessible API to provision the configuration here? In particular, for rust-lang/* repositories, I'd love to control this access via rust-lang/team, rather than the current manual configuration process. For some repositories manual configuration (even with this feature) would still mean that we're regularly asking humans to access highly privileged accounts (rust-lang-owner) to update settings, and in a way that likely lacks good review + audit trail (e.g., no 2 person review on the actual policy set).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will definitely be an API for it since that is currently required by the crates.io user interface anyway. I would expect us to only enable it for cookie-based auth at first though until we're happy with the API and consider it stable.

1. (required) The owning GitHub username or organization
2. (required) The repository name
3. (required) The workflow file name (must be located in `.github/workflows/`)
4. (optional) The [GitHub Actions environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to treat an empty environment string as mandating that the publishing is not done from an environment? This may help users fill in which environment allows publishing if they use environments at all on their CI rather than forgetting to fill it in implicitly acting as wildcard and thus accidentally allow publishing outside of an environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-crates-io Relevant to the crates.io team, which will review and decide on the RFC.
Projects
Development

Successfully merging this pull request may close these issues.

10 participants