Skip to content

[Bug]: utils/read.go ReadPasswordFromFile does not support pipes and file descriptors #1503

@cwegener

Description

@cwegener

Steps to Reproduce

Run step ca init using a file descriptor as a password file:

step ca init --kms azurekms \
--helm \
--deployment-type=standalone \
--name=Smallstep \                                                                                                                                                                                                                                             --kms-root='azurekms:name=my-root-key;vault=my-kv' \                                                                                                                                                                                                           --kms-intermediate='azurekms:name=my-intermediate-key;vault=my-kv' \
--dns=lima-default \
--dns=localhost \
--dns=127.0.0.1 \
--address=:8443 \
--provisioner=admin \
--provisioner-password-file=<(printf "%s\n" "testpassword123")

Your Environment

  • OS - Linux
  • step CLI Version - Smallstep CLI/0.28.7 (linux/amd64)

Expected Behavior

  • step ca init prints initializes and prints the Helm values.

Actual Behavior

  • step ca init asks for the Provisioner password interactively

Additional Context

Perfect! Now I've found the issue. The problem is on line 62-64 of the jose/generate.go file:

func GenerateDefaultKeyPair(passphrase []byte) (*JSONWebKey, *JSONWebEncryption, error) {
	if len(passphrase) == 0 {
		return nil, nil, errors.New("step-jose: password cannot be empty when encryptying a JWK")
	}

Now let me trace back to the init.go code to understand the flow:

Looking at command/ca/init.go:

  • Lines 291-297: provisionerPassword is read from the file
  • Lines 659-669: GenerateKeyPairs is called with either provisionerPassword (if set) or pass (the interactively prompted password)

The issue is that when you use process substitution --provisioner-password-file=<(printf "%s\n" "testpassword123"), Bash creates a file descriptor like /dev/fd/63. However, ReadPasswordFromFile uses os.ReadFile which may not work correctly with process substitution file descriptors.

Analysis Summary

I've identified the root cause of the issue. The problem is in ./cli/utils/read.go at lines 55-62:

The Issue:

The ReadPasswordFromFile function uses os.ReadFile(filename) directly, which doesn't properly handle:

  1. Process substitution file descriptors like /dev/fd/63 created by <(printf "%s\n" "testpassword123")
  2. Named pipes (FIFOs) that process substitution creates

Why it fails:

When you use --provisioner-password-file=<(printf "%s\n" "testpassword123"), Bash creates a process substitution that appears as a file descriptor path like /dev/fd/63. While os.ReadFile can sometimes read from these, it's not reliable for all file descriptor types, especially on different systems or when the file descriptor is a pipe that needs to be read as a stream rather than seeked.

The Solution:

The ReadPasswordFromFile function should be updated to handle file descriptors and pipes properly, similar to how the ReadFile function (lines 91-107 in the same file) handles stdin. It should:

  1. Check if the file is a regular file or a special file (like a pipe/FIFO)
  2. For pipes and file descriptors, open and read them as a stream using os.Open + io.ReadAll
  3. For regular files, use os.ReadFile as it currently does

Proposed Fix:

Modify ReadPasswordFromFile to use os.Open and io.ReadAll instead of os.ReadFile, which properly handles both regular files and file descriptors/pipes:

func ReadPasswordFromFile(filename string) ([]byte, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, errs.FileError(err, filename)
	}
	defer f.Close()
	
	password, err := io.ReadAll(f)
	if err != nil {
		return nil, errs.FileError(err, filename)
	}
	password = bytes.TrimRightFunc(password, unicode.IsSpace)
	return password, nil
}

This change would make the function work correctly with:

  • Regular files: /tmp/password.txt
  • Process substitution: <(printf "password")
  • Named pipes (FIFOs)
  • File descriptors: /dev/fd/63

The fix is minimal and maintains backward compatibility while adding support for process substitution.

Contributing

Vote on this issue by adding a 👍 reaction.
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugneeds triageWaiting for discussion / prioritization by team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions