Summary

Add a cargo setting to fetch registry authentication tokens by calling an external process.

Motivation

Some interactions with a registry require an authentication token, and Cargo currently stores such token in plaintext in the .cargo/credentials file. While Cargo properly sets permissions on that file to only allow the current user to read it, that’s not enough to prevent other processes ran by the same user from reading the token.

This RFC aims to provide a way to configure Cargo to instead fetch the token from any secrets storage system, for example a password manager or the system keyring.

Guide-level explanation

Suppose a user has their authentication token stored in a password manager, and the password manager provides a command, /usr/bin/cargo-creds, to decrypt and print that token in a secure way. Instead of storing the token in plaintext, the user can add this snippet to their own Cargo config to authenticate with crates.io:

[registry]
credential-process = "/usr/bin/cargo-creds"

When authentication is required, Cargo will execute the command to acquire the token, which will never be stored by Cargo on disk.

It will be possible to use credential-process on both crates.io and alternative registries.

Reference-level explanation

A new key, credential-process, will be added to the [registry] and [registries.NAME] sections of the configuration file. When a token key is also present, the latter will take precedence over credential-process to maintain backward compatibility, and a warning will be issued to let the user know about that.

The registry.credential-process value will be used for all registries. If a specific registry specifies the value in the registries table, then that will take precedence.

The credential-process key accepts either a string containing the executable and arguments or an array containing the executable name and the arguments. This follows Cargo’s convention for executables defined in config.

There are special strings in the credential-process that Cargo will replace with a given value:

  • {name} — Name of the registry.
  • {api_url} — The API URL.
  • {action} — The authentication action (described below).
[registry]
credential-process = 'cargo osxkeychain {action}'

[registries.my-registry]
credential-process = ['/path/to/myscript', '{name}']

There are two different kinds of token processes that Cargo supports. The simple “basic” kind will only be called by Cargo when it needs a token. This is intended for simple and easy integration with password managers, that can often use pre-existing tooling. The more advanced “Cargo” kind supports different actions passed as a command-line argument. This is intended for more pleasant integration experience, at the expense of requiring a Cargo-specific process to glue to the password manager. Cargo will determine which kind is supported by the credential-process definition. If it contains the {action} argument, then it uses the advanced style, otherwise it assumes it only supports the “basic” kind.

Basic authenticator

A basic authenticator is a process that returns a token on stdout. Newlines will be trimmed. The process inherits the user’s stdin and stderr. It should exit 0 on success, and nonzero on error.

With this form, cargo login and cargo logout are not supported and return an error if used.

Cargo authenticator

The protocol between the Cargo and the process is very basic, intended to ensure the credential process is kept as simple as possible. Cargo will execute the process with the {action} argument indicating which action to perform:

  • store — Store the given token in secure storage.
  • get — Get a token from storage.
  • erase — Remove a token from storage.

The cargo login command will use store to save a token. Commands that require authentication, like cargo publish, will use get to retrieve a token. A new command, cargo logout will be added which will use the erase command to remove a token.

The process inherits the user’s stderr, so the process can display messages. Some values are passed in via environment variables (see below). The expected interactions are:

  • store — The token is sent to the process’s stdin, terminated by a newline. The process should store the token keyed off the registry name. If the process fails, it should exit with a nonzero exit status.

  • get — The process should send the token to its stdout (trailing newline will be trimmed). The process inherits the user’s stdin, should it need to receive input.

    If the process is unable to fulfill the request, it should exit with a nonzero exit code.

  • erase — The process should remove the token associated with the registry name. If the token is not found, the process should exit with a 0 exit status.

Environment

The following environment variables will be provided to the executed command:

  • CARGO — Path to the cargo binary executing the command.
  • CARGO_REGISTRY_NAME — Name of the registry the authentication token is for.
  • CARGO_REGISTRY_API_URL — The URL of the registry API.

Drawbacks

No known drawbacks yet.

Rationale and alternatives

The solution proposed by this RFC isn’t tied to any secret storage services and can be adapted to work with virtually any secret storage the user might rely on, while being relatively easy to understand and use.

Prior art

Multiple command line tools implement this system or a similar one to retrieve authentication tokens or other secrets:

  • awscli includes the credentials_process setting which calls a process with arguments provided by the user. The process is expected to emit JSON that contains the access key.
  • Docker CLI offers “credential stores”, programs the Docker CLI calls with specific arguments expecting JSON output. Implementations are provided for common storage systems, and the protocol is documented for users who want to integrate with their custom system.
  • Ansible Vault allows to specify an executable file as the decryption password, executing it when needed.
  • Git has a credential mechanism using store/get/erase arguments, and key=value parameters send and received with the process.

Unresolved questions

No known unresolved questions yet.

Future possibilities

To allow for a better user experience for users of popular secret storages, Cargo can provide built-in support for common systems. It is proposed that a credential-process with a cargo: prefix will use some internal support. For example, credential-process = 'cargo:system-keychain'.

Additionally, the community could create Cargo plugins that implement different storage systems. For example, a hypothetical Cargo plugin could be specified as credential-process = 'cargo credential-1password {action}'.

Encrypting the stored tokens or alternate authentication methods are out of the scope of this RFC, but could be proposed in the future to provide additional security for our users.

Future RFCs introducing new kinds of secrets used by Cargo (i.e. 2FA codes) could also add support for fetching those secrets from a process, in a similar way to this RFC. Defining how that should work is outside the scope of this RFC though.