-
Notifications
You must be signed in to change notification settings - Fork 280
Description
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
stepCLI Version - Smallstep CLI/0.28.7 (linux/amd64)
Expected Behavior
step ca initprints initializes and prints the Helm values.
Actual Behavior
step ca initasks 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:
provisionerPasswordis read from the file - Lines 659-669:
GenerateKeyPairsis called with eitherprovisionerPassword(if set) orpass(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:
- Process substitution file descriptors like
/dev/fd/63created by<(printf "%s\n" "testpassword123") - 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:
- Check if the file is a regular file or a special file (like a pipe/FIFO)
- For pipes and file descriptors, open and read them as a stream using
os.Open+io.ReadAll - For regular files, use
os.ReadFileas 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).