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

2FA in CI #282

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
167 changes: 167 additions & 0 deletions docs/drafts/2fa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Baseline practice: using 2FA when publishing

To avoid malicious use of stolen package registry tokens, it is advisable to enable two-factor authentication (2FA). When it is enabled every call to `npm publish` requires a unique one time password (OTP) to be entered.

To make sure that the code is a _second_ factor it should be generated by a human on an independent device. The secret seed of the OTP generator should be protected in such a way, that it cannot be obtained together with the registry token.

When releases are created by humans running `npm publish` (or via tooling such us [np](https://www.npmjs.com/package/np) or [publish-please](https://www.npmjs.com/package/publish-please)) on their local environments, the user is present to manually input the OTP when prompted.

However, some maintainers prefer automating the release process (e.g. by using [semantic-release](https://semantic-release.gitbook.io/semantic-release/) or [merge-release](https://github.com/mikeal/merge-release)) and publishing directly from their Continuous Integration (CI) systems (e.g. Travis). This is at odds with advice to use 2FA, as there usually is no obvious way to provide the OTP for the publish command.

This document outlines possible solutions to bridge this gap with the goal of enabling easy setup of automated releases from CI, using 2FA (human approval), for individuals and for teams.

## Related docs

* [Publish guidelines (draft)](https://github.com/nodejs/package-maintenance/blob/master/docs/drafts/PUBLISH-GUIDELINES.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these all got promoted out of drafts recently, so we may want to double check the link and remove the draft label

* [CI/CD guidelines (draft)](https://github.com/nodejs/package-maintenance/blob/master/docs/drafts/ci-cd-guidelines.md)
* [Testing guidelines (draft)](https://github.com/nodejs/package-maintenance/blob/master/docs/drafts/testing-guidelines.md)

Note: at the time of writing neither documents or gives advice on using two-factor authentication when publishing.
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: at the time of writing neither documents or gives advice on using two-factor authentication when publishing.

Suggest removing or


## Potential solutions for 2FA in CI

All of the listed solutions below have their pros and cons. While the order of desireability of these solutions has been discussed, the list below does not imply any order of preferences.

### Release manager

The release manager could be a place where packages are staged after `npm publish` without an OTP. A human would need to use the manager (CLI/GUI) and could release one or more pending packages, approving the publishing with an OTP. This would need be implemented by the registry (for public packages - npm) or as a separate application, allowing "proxy" publishing.

Such a manager should enable further tooling to integrate notifications and approvals via email or chat systems.

The primary benefit of this is the simplicity of using this for teams - the CI can publish the package under a bot account

Possible complications:

- Are the version numbers reserved once published/staged?
- Are the users allowed to install the "staged" packages by explicitly opting in?
- Existing tooling may already be too dependent on a specific version becoming visible in the registry immediately after publish.
- Can this be standardized enough to work with alternative registries?
- This may required significant effort to build and maintain, incl. by npm Inc.

Poor man's version:

- Allow adding a 2FA approval on specific versions retroactively. This would allow tooling to be built on the consumer side to avoid non-2FA releases, while the maintainers keep the option of "singing off" the releases at their convenience - tooling can be built to help these tasks as well.

### Extension in CI providers

As 2FA in CI is partly a problem because there is no way to enter the OTP, it can be solved by simply adding a way to enter an OTP in CI.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if manual approval, now available in GitHub Actions could be "hacked" to make this work? From what I see in that blog post, folks can leave a comment. Maybe that comment could be the OTP and then the build script could leverage the GitHub API or even the current process / action to grab that and supply it to npm publish?

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting idea but wouldn’t the comment be public?

(time based) OTPs can be used multiple times within valid time window. Bad actors can watch for comments and immediately reuse same OTP for another action.


For maximum usability, the required input should integrate with various ways of notifying the maintainers, possibly by also allowing the input via the notification channels (e.g. chatbots), and should provide for graceful timeouts.

Some CI systems (e.g. Jenkins) already do have some form of user input at build time.

Possible complications:

- Requires a lot of coordination with many different CI providers. While Travis is the de-facto standard in the npm ecosystem and Github Actions have a lot of initial traction, there are many other systems.
- The cost of building such a feature is likely non-negligible for the CI providers (input is harder than output), even if the feature itself may have many other practical applications.
- The setup would vary system by system, which could lead to unnecessary complexity in the ecosystem (esp. for maintainers who contribute to multiple projects).
- Unclear if there's an easy way to make the input work for teams, because the 2FA is tied to a single identity, i.e. who can enter the OTP when the CI is waiting for it?
- Could be solved by configuring multiple publish tokens and selecting the correct one, but this requires configuration complexity.
- Could be solved by using a team account to publish, but this requires sharing a secret across the team.
- Could be solved by using the identity of the person who kicked off the release build (i.e. the person who merged a PR), but this requires interpersonal coordination and couples merge priveleges with publish priveleges.
- Notification fatigue.

### Remote OTP

The below options all center around an idea that a CI job would make a request to a pre-configured somewhat-secret URL, which would trigger a notification to the maintainer, whereby they could enter the OTP.

It's worth noting, that there are already existing commercial system that are used in enterprise settings to provide a second factor via mobile notifications, calls and SMS. However these systems are complex and likely too expensive for small OSS teams and sole maintainers.

The major benefit of this is that this works today, with existing tooling (at least a POC level) and does not depend on any third party action (even if it could potentially be helpful).

Possible complications:

- Similar to the extension in CI providers, there could be complexity in setting things up for teams (i.e. selecting the correct publish tokens and humans for providing the OTP).
- Server costs (all of the below require servers for making requests, which depending on the approach could be long running requests, i.e. they could be costly even on serverless implementations). While it is possible to design a system which relies on a public git repository as a backing store, there may additionaly be database costs.
- Maintenance costs (the software running on the servers needs to be updated, monitored, etc)
- Trust problems (depending on the approach, a third party system might be undesirable, as it may be hard to ensure that such a third party is unable to retrieve the secret OTP generator seed).
- Notification fatigue.

#### Remote OTP entry via mobile

POC: https://github.com/nearform/optic

The POC is a PWA, which can be added to the home screen. It allows scanning a QR code to initialize the OTP generator and can work as a standalone authenticator application. Once a token is configured, it provides a URL which responds with the OTP after the maintainer pushes the "Approve" button on their phone/browser.

The CI system can be configured to wait for a response from this URL just before publishing and to provide the code to npm via `--otp` or an environment variable.

Additional cons:

- Dependency on Firebase to deliver notifications (vendor lock in, possible costs).
- Needs a server.
- Likely should not be trusted to be run by a third party, i.e. a maintainer should run a private instace of this, unless a trustworthy organization would be able to do that (npm Inc.? OpenJS Foundation? One of major Node.js contributor organizations?).

Missing features:

- Impossible to move to another device without resetting the URL, i.e. changing the phone requires re-configuration of CI.
- Possibly scary, as the first thing it asks for is the generator secret (i.e. needs UX and documentation work).

#### Remote OTP entry via chatbot

POC: still on it's way to be OSS - ask https://twitter.com/MarshallOfSound for details

The POC is a chatbot integration. There is an API endpoint which can be trigger a question and another endpoint which can be polled for the response.

The CI system can be configured to use a client module which deals with the API.

Additional cons:

- Dependency on Slack.
- Needs a server.
- Needs the OTP to actually be typed out.

#### Remote OTP entry via authentication SaaS provider

As there are existing commercial systems to provide 2FA APIs to organization, these could potentialy be used for OSS.

Possible providers:

- [Duo](https://duo.com/product/multi-factor-authentication-mfa)
- [Authy](https://authy.com) (with [Twilio](https://www.twilio.com/docs/authy)?)
- [Okta](https://www.okta.com/products/adaptive-multi-factor-authentication/)
- [Auth0](https://auth0.com/docs/multifactor-authentication)

Additional cons:

- Would require reaching out to providers and negotiating to get a free-for-OSS plan.
- Trusting a third party.
- Vendor lock-in.

### Remote decryption service

[krypt.co](https://krypt.co) provides a way to have private keys stored on the mobile phone and use them for remote signing. This means that a CI system can be configured to have the encrypted OTP generator seed on it, and the decryption would be requested via an API. The maintainer can then approve the access to the seed, which can be used to generate the OTP and pass it to `npm publish`.

Alternatively, if package registries accepted package signatures as a second factor, the service could be used to sign the package remotely, using a personal device.

Cons:

- Potentially complicated setup.
- Trusting a third party.
- Vendor lock-in.

### Alternative registries/release repositories for staging

While it is hard to integrate CI systems and the public npm registry _with_ 2FA today, a small scale release manager can be built by using non-npm registries or GitHub Releases as a staging area.

The CI system could be set up to publish packages into an alternative registry or to upload the tarball to a release artifact storage area. Tooling can then be built to query such storage areas for unpublished packages and it could possibly publish things en-masse.

Such tooling can be further extended to e.g. allow pre-publishing unsigned packages under a different scope even on a public npm registry, to allow e.g. easy nightly releases or a way to manage the release of commercially supported versions (see `@commercial` scope of Hapi.js).

The major benefit of this approach is team management - anyone with publish access could run `npm publish [tarball URL]` to unstage a release. The tarball sha hash could also be pre-pushed by the CI system into the release storage area for integrity verification.

Another feature that could be built on top of this is using the tarballs directly from build artifacts as input into CITGM for package ecosystems.

Cons:

- Requires potentially complex setup.
- Requires a manual action on the maintainer's computer, with a non-obvious notification mechanism.
- Requires tooling to be built and maintained.
- GitHub release assets are mutable (?).

## Somewhat related other ideas

- Registries should allow including a meta field to link back to the CI job used to publish a specific version for audit purposes.
- npm client should have an `otpUrl` config option in `.npmrc`. While it is easy enough to provide a `--otp=$(curl http://example.com)`, depending on publishing methods used (e.g. `semantic-release`), such an integration is trickier.
- Managing the publish tokens in CI can be cumbersome, esp. if you have a lot of repos - there is scope for more tooling to deal with Travis, Github Actions APIs to mass-manage multiple repos as a member of multiple organizations and as an organization.
- It's been a long standing request from the community (regardless of whether it solves any problem) to have package signatures. The tooling for staged releases/alternative registries and remote decryption/signing services could be used to also publish the signatures on GitHub releases.
- Package signing from CI might not be good enough, as the CI job is triggered by a merge, which is not protected by 2FA.