I’ve been using the Unix command line (CLI) for longer than I care to admit. Let’s just say I’m officially a “greybeard” and, as of this writing, literally have a grey beard. But thankfully, the command line is as popular as ever.

Passing secrets on the command line has many sharp edges. I recently read the Command Line Interface Guidelines, which is sort of like a version of Apple’s Human Interface Guidelines (HIG), but for command line programs. If you write command line programs and utilities, the whole thing is an essential read. But there is a section on handling secrets, like passwords or API tokens, that deserves special attention.

Here are the two guidelines on secrets, written from the perspective of CLI authors:

  • Do not read secrets directly from flags.
  • Do not read secrets from environment variables.

In this post, I explore what this actually means for both users and authors of command line programs. In a followup post, I show an example using cURL, as many, MANY examples show using curl in insecure ways to pass API tokens.

For Users

Avoid Passing Secrets as Command Line Arguments

As a user, it is important to use the proper method to avoid unintentionally leaking your secrets. First and foremost, avoid passing secrets directly in command line arguments, often via flags. Even if a program offers more secure methods, it still often offers an insecure flag:

> cli --password abc123

This should be avoided for two reasons:

  1. This will leak into the history for most shells, like Zsh or Bash.
  2. It shows up in ps output.

Here’s a demonstration of Zsh shell history:

> cli --password abc123
> history -1 # In Bash: `history 1`
66681  cli --password abc123

As you can see, the history command shows the password in plaintext. Worse, shells usually store their history on disk, so this password will be visible, in plaintext, to anything that can access this file. Worse still, the shell history file could end up in backups that are preserved for years.

The ps output is not as bad, but it does mean your password is visible in plaintext to anyone (or any process) with access to the machine. Say the command takes a long time to run, for example, while doing network requests. The password will be visible in plaintext while the command is running. For example, if you run this command:

> slow-cli --password abc123

The password is visible via ps (or pgrep):

> pgrep -fl slow-cli
45320 /bin/sh /Users/dave/bin/slow-cli --password abc123

Avoid Passing Secrets in Environment Variables

A very common recommendation to avoid leaking secrets into shell history is to store the secret in an environment variable instead. This does hide the secret from shell history, as the history will only contain the environment variable name:

> slow-cli --password $SLOW_CLI_PASSWORD
> history -1
66710  slow-cli --password $SLOW_CLI_PASSWORD

But you do have to be careful how you set the environment variable. The naive method still ends up in history:

> export SLOW_CLI_PASSWORD=abc123
> history -1
66712  export SLOW_CLI_PASSWORD=abc123

This can be resolved a number of ways, but the best is to use shell command substitution. For demonstration purposes, I will use cat, but this is less than ideal since it stores the password in plaintext on a file on disk. However, you can (and should) use a password manager CLI in its place, like op, for 1Password, pass, or gopass:

> export SLOW_CLI_PASSWORD=$(cat password.txt)
> slow-cli --password $SLOW_CLI_PASSWORD
> history -2
66719  export SLOW_CLI_PASSWORD=$(cat password.txt)
66720  slow-cli --password $SLOW_CLI_PASSWORD

This prevents the password from leaking into shell history, but it still runs into the ps issue, even when using a password manager. This is because the $SLOW_CLI_PASSWORD environment variable is expanded by the shell before it executes the program, so the real arguments it passes include the plaintext password.

Side note: Please use $(...) for command substitution rather than backticks, for the simple reason that it nests better. Both Zsh and Bash support this.

Another downside to storing passwords in environment variables is that they are also prone to leakage. Read the CLI Guidelines for more details, but environment variables are accessible to any subprocess and are often included in logs. Some programs even read secrets directly from an environment variable, without requiring a flag, but the secret is still in the environment.

Use an Option to Read Passwords from a File

So what can you do to make this secure? One method to look for is the ability to read secrets from a file:

> slow-cli --password-file password.txt

You might think this is not great, because it requires storing the plaintext password in a file. This is true, however, most shells support process substitution (which is similar to command substitution) that can be coupled with a password manager’s CLI:

> slow-cli --password-file <(cat password.txt)

Obviously you need to replace cat, but if you do, then this is your most secure option. The password is never leaked to shell history, ps, or the environment. This is because <(...) substitution uses a pipe exposed as a temporary file name:

> echo <(cat password.txt)
/dev/fd/16

A similar method that some CLI programs support is to read from a command:

> slow-cli --password-command "cat password.txt"

This method is best for configuration files, though, where shell substitution is unavailable. But either way, these are the most secure methods.

Read Passwords from Standard Input

Many programs have an option to read secrets from standard input or stdin. This is just as secure as the file method above, and leaks nothing:

> cat password.txt | slow-cli --password-stdin

But I find this has worse ergonomics, since you need to put the command to read the secret first and pipe it to the program. This breaks searching through shell history by prefix.

stdin can also only be used once. For example, maybe you want to pipe some content to the program through stdin, but then it cannot also be used for the secret.

For Authors

The most secure methods require the command to be able to read secrets from a file or command. And unfortunately, not all commands support this. So if you write command line programs, please add an option to read from a file or command. And if you support configuration files, please support reading secrets from a command.

Finally, instead of only supporting reading secrets from stdin, support reading from a file, and then use - to indicate stdin:

> cat password.txt | slow-cli --password-file -

Next

Please read the followup post to see an example using cURL and password manager CLIs.