-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: Store registry tokens in the OS credential store by default #3981
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| - Feature Name: `keyring_credential_default` | ||
| - Start Date: 2026-07-02 | ||
| - RFC PR: [rust-lang/rfcs#3981](https://github.com/rust-lang/rfcs/pull/3981) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| # Summary | ||
| [summary]: #summary | ||
|
|
||
| Cargo should stop writing registry tokens to a plaintext file by default. Instead, `cargo login` should put the token in the operating system's credential store: Keychain on macOS, Credential Manager on Windows, Secret Service on Linux and the BSDs. Any command that reads a token, `cargo publish` included, should migrate an existing plaintext token into the store when one is found. Cargo should fall back to the current file-based storage only when no such store is available. | ||
|
|
||
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Today, `cargo login` writes your token to `~/.cargo/credentials.toml` in plaintext. This file gets swept up in dotfile repos, backups, and misconfigured shared home directories, and it's a well-known target for token-stealing malware. A stolen crates.io token means someone can publish under your name. And this class of attack is getting cheaper by the month: with AI-assisted tooling, writing and deploying a credential-harvesting script that knows exactly where every ecosystem keeps its plaintext tokens is now a trivial exercise, so "it's just a file in the home directory" is a worse bet than it was even a couple of years ago. | ||
|
|
||
| The Rust Maintainers actually solved most of this problem already. Since 1.74, Cargo has pluggable credential providers, and it even ships secure ones: `cargo:macos-keychain`, `cargo:wincred`, and `cargo:libsecret`. The problem is that they're opt-in: a user only gets secure storage if they know the feature exists and edit their config to enable it, which is exactly the population least at risk in the first place. The plaintext file remains what every `cargo login` produces out of the box. Meanwhile `gh`, Docker, and pip all moved to OS credential stores by default years ago, and the sky did not fall. | ||
|
|
||
| This RFC proposes flipping the default. | ||
|
|
||
| # Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| For most users, nothing visibly changes. You run `cargo login`, paste your token, and publish as usual. The difference is where the token lives: it goes into your OS keychain rather than a file. On macOS you might see a one-time Keychain prompt. On Linux, whatever implements the Secret Service API on your desktop (GNOME Keyring, KWallet, or `pass` via the `pass-secret-service` bridge) holds it. `cargo logout` removes it. | ||
|
|
||
| If you already have a plaintext token from before the upgrade, you don't need to log in again to get the benefit. The next time any command needs your credential, whether that's `cargo login`, `cargo publish`, `cargo yank`, or `cargo owner`, Cargo migrates the token into the keyring, deletes it from `credentials.toml`, and prints a notice saying it did so. In practice this means most users are moved to secure storage the first time they publish after upgrading. | ||
|
|
||
| If you're on a headless server or in CI, nothing changes at all. `CARGO_REGISTRY_TOKEN` still takes precedence and is never migrated anywhere, and if there's no credential store to talk to, Cargo quietly falls back to the file just like today. Nobody's build breaks because D-Bus isn't running. | ||
|
|
||
| # Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| Rather than maintaining three separate built-in providers, Cargo's default provider would be a single `cargo:keyring` provider built on the `keyring` crate, which already abstracts over the macOS Security Framework, Windows Credential Manager, and Secret Service (with a kernel keyutils fallback on Linux, and Secret Service coverage on FreeBSD and OpenBSD). The default provider list becomes `["cargo:keyring", "cargo:token"]`: try the keyring, fall back to the file. | ||
|
|
||
| Migration happens in the credential-resolution path, not just in `login`. When resolution falls through to the file provider and finds a token, and the keyring provider reported itself available, Cargo writes the token to the keyring, verifies the write by reading it back, and only then removes it from `credentials.toml`. If the keyring write or read-back fails, the file is left untouched and the token is used as before. This ordering means an interrupted migration can duplicate a credential but never lose one. Tokens supplied via `CARGO_REGISTRY_TOKEN` or `--token` bypass migration entirely, since they were never Cargo's to store. | ||
|
|
||
| Entries are keyed by service `cargo-registry:<index-url>` so alternative registries each get their own credential. On Linux the Secret Service connection is made over D-Bus at runtime, not linked at build time, so a missing D-Bus daemon just means the provider reports that no store is available and the chain moves on to the file provider. | ||
|
|
||
| The existing three per-platform providers stay for a deprecation period so nobody's explicit config breaks, but they'd eventually become aliases for `cargo:keyring`. | ||
|
|
||
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another possible drawback is that the file no longer exists in the filesystem (or at least, it's not in the same place). This has multiple drawbacks:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, only crates.io credentials can be re-created via GitHub. if this change applies to other registry credentials, then moving those credentials into personal keychains could potentially violate corporate asset or security policies.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the corporate registry mandates use of plaintext tokens they can specify it in the config: # .cargo/config.toml
[registries.example]
index = 'https://example.com/index'
credential-provider = ['cargo:token']also i don't think that a threat model that - (1) saving valuable corporate asset (the registry login token) into
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to worry about corporate access policies being an issue. Prior art (Github CLI, pip/uv opt-in, etc) have already made the change and there isn't much covered engineering-news wise about lashing back from a corporate policy standpoint. In my 5 last years, moves to secured credentials via company-owned password vaults and mandating things like 2FA, Passkeys on personal devices, etc, has been the norm, not the outlier. |
||
|
|
||
| Keychain access can prompt the user, which is surprising the first time it happens mid-script, though the env-var and file fallbacks mean scripted environments never hit this in practice. Migrating during `publish` makes that first prompt more likely to appear in the middle of a workflow the user thinks of as non-interactive, which is why the notice needs to be loud about what just happened. It also adds the `keyring` crate (and its platform dependencies) to Cargo's dependency tree, which is real supply-chain surface for a security-sensitive code path and would need the usual vetting. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be a timeout for the initial transfer to the keychain, in case the user is running non-interactively, but with a GUI/terminal keychain prompt enabled? Without a timeout, the build process will appear to freeze, and if the user can't see the reason, it will look like a bug. For example, the prompt could appear on a different (virtual/invisible) screen or terminal. Similar unexpected prompts have led to denial of service issues, although this one isn't remotely triggerable. |
||
|
|
||
| # Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| The obvious alternative is the status quo: the secure providers exist, just turn them on yourself. But a security measure that requires discovering a config key does not protect the default install, and the default install is the threat model here. Defaults matter precisely because most users never change them, and that cuts both ways. Right now it cuts toward plaintext. | ||
|
|
||
| Migrating only on `cargo login` would be simpler, but it leaves every existing user in plaintext indefinitely, since a working token means there's no reason to ever run `login` again. Publish-time migration is what actually moves the installed base. | ||
|
|
||
| The Rust Maintainers could also keep the three separate per-platform providers and just change the default list per platform. That works, but it's three implementations to keep behaviorally in sync where one crate already does the job, and it leaves the BSDs out. | ||
|
Comment on lines
+45
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another alternative could be to print a warning/notice when To aid manual migration, maybe something like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a manual migration command would be useful whatever we do here, and also for testing.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would agree for testing but I don't agree for the full practice. Too many people, in my opinion, are going to just never do the migration if it's allowed to be manual. The purpose of the RFC is a forced stance on moving to a safety first approach to benefit users who are less savey about proper security procedures.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear, I think people should be able to manually migrate before they publish. |
||
|
|
||
| # Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| Cargo's own credential-process RFC ([RFC 2730](https://rust-lang.github.io/rfcs/2730-cargo-token-from-process.html)) and the 1.74 stabilization laid all the groundwork here. This RFC is really just about the default. Outside Rust: `docker-credential-helpers`, `gh auth`, and Python's `keyring` package all made the same move, and their experience (including the headless-fallback problem) is directly applicable. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're absolutely right, The other languages and Tools Migrations would be extremely helpful in understanding where the pitfalls are. What did they do? What worked? What didn't? Where can I read more? Are there other tools that did different migrations or made different decisions? If so what decisions did they do and why? |
||
|
|
||
| # Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| Whether the deprecation period for the old provider names needs a formal timeline or can just ride the normal edition cadence. And whether publish-time migration should ask for confirmation on its first run or just proceed with a notice, given that a confirmation prompt in a previously non-interactive flow is its own kind of breakage. | ||
|
|
||
| # Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| Once tokens live behind a provider interface by default, the same path could carry asymmetric tokens ([RFC 3231](https://rust-lang.github.io/rfcs/3231-cargo-asymmetric-tokens.html)) without users ever handling a bearer secret directly. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cargo team rarely specifies exactly what dependencies we're going to use. I suspect we will be more comfortable merging this RFC if it directly specified the semantics; justifying that those semantics are possible to implement by referencing your recommended implementation.
Something like:
If we do take your advice and use the
keyringcrate. We will also need to have a deep discussion about what infrastructure supports the ongoing maintenance of that crate. Who is on call for any security problems found? Will they still be around and available in three years or five years?View changes since the review