Environment
Device and OS:
App version: uds v0.34.3 (behaviour also present in earlier releases)
Kubernetes distro being used: N/A — reproduces at build time, no cluster involved
Other: cosign key pair generated with an empty passphrase (cosign generate-key-pair + empty prompt). Running non-interactively from a semantic-release prepareCmd in CI.
Steps to reproduce
- Generate a cosign key pair with an empty passphrase:
COSIGN_PASSWORD='' cosign generate-key-pair
- From a directory containing a valid
uds-bundle.yaml, run a fully non-interactive create:
uds create . \
--signing-key ./cosign.key \
--signing-key-password '' \
--confirm --no-color --no-progress
- Repeat with each of the following, all of which fail the same way:
COSIGN_PASSWORD='' uds create . --signing-key ./cosign.key --confirm
- omitting
--signing-key-password entirely
- piping input:
uds create . --signing-key ./cosign.key --confirm < /dev/null
Expected result
With --confirm set (and/or an explicitly-provided empty --signing-key-password, and/or COSIGN_PASSWORD exported), uds create signs the bundle without prompting, and exits non-zero on failure rather than blocking. Bundles signed with an empty-passphrase key should be producible in an unattended pipeline.
Actual Result
uds create blocks on an interactive prompt:
? Private key password (empty for no password):
On a runner with a TTY this hangs until the job times out. Without a TTY, survey errors out instead. Either way there is no flag, env var, or config value that lets you get past it, because an empty passphrase is not expressible.
Visual Proof (screenshots, videos, text, etc)
The relevant code is in [src/pkg/bundle/create.go#L75-L86](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/bundle/create.go#L75-L86):
getSigCreatePassword := func(_ bool) ([]byte, error) {
if b.cfg.CreateOpts.SigningKeyPassword != "" {
return []byte(b.cfg.CreateOpts.SigningKeyPassword), nil
}
return interactive.PromptSigPassword()
}
// sign the bundle
signBlobOptions := signing.DefaultSignBlobOptions()
signBlobOptions.OutputSignature = filepath.Join(b.tmp, config.BundleYAMLSignature)
signBlobOptions.PassFunc = getSigCreatePassword
signBlobOptions.Key = b.cfg.CreateOpts.SigningKeyPath
Three separate consequences, all from these lines:
- An empty passphrase is unrepresentable.
SigningKeyPassword is a plain string and the guard is != "", so "the user explicitly passed an empty password" and "the user passed nothing" collapse into the same case. An empty passphrase is valid for cosign, so this is a genuine gap rather than a missing feature.
COSIGN_PASSWORD is ignored. Assigning signBlobOptions.PassFunc overrides cosign's own env-aware pass function, which would otherwise honour COSIGN_PASSWORD (cosign uses os.LookupEnv, so it handles the empty-string case correctly).
--confirm is not consulted here. [interactive.PromptSigPassword()](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/interactive/interactive.go#L12-L23) uses survey.Password, which requires a TTY. Zarf's equivalent path returns early when confirm is set; UDS CLI does not, so --confirm does not imply non-interactive here even though that is what it implies everywhere else in the command.
For comparison, Zarf exposes zarf package sign ... --signing-key-pass, so a package can be signed after the fact. UDS CLI has no uds sign command, and signing happens mid-create (the signature is written to the temp dir and pushed as an OCI layer by the bundler), so there is no supported way to sign a bundle out of band either.
Severity/Priority
Medium. It does not affect deployed bundles, but it blocks automated signed-bundle releases for anyone using an empty-passphrase key, and the failure mode in CI is a hang rather than a clear error. There is a workaround (re-key with a non-empty passphrase), but it is not discoverable from the CLI output or the docs — the prompt explicitly advertises "empty for no password", which is exactly the case that cannot be automated.
Additional Context
Possible fixes, roughly in increasing order of completeness:
- Honour
--confirm. Return nil, nil (or []byte{}, nil) instead of prompting when config.CommonOptions.Confirm is set. Smallest change; makes --confirm mean the same thing here as elsewhere.
- Fall back to cosign's own pass function. Leave
signBlobOptions.PassFunc unset when no password flag was supplied, so COSIGN_PASSWORD works as users coming from cosign would expect.
- Make "empty" expressible. Use
cmd.Flags().Changed("signing-key-password") rather than a != "" comparison, so an explicit --signing-key-password '' is respected.
- Add
--signing-key-password-file. Avoids passphrases in process args / CI logs, and sidesteps the empty-string ambiguity entirely. Useful independent of this bug.
- Optionally, add a
uds sign command mirroring zarf package sign, for the post-creation signing case.
Happy to open a PR for whichever direction maintainers prefer.
Environment
Device and OS:
App version:
udsv0.34.3 (behaviour also present in earlier releases)Kubernetes distro being used: N/A — reproduces at build time, no cluster involved
Other: cosign key pair generated with an empty passphrase (
cosign generate-key-pair+ empty prompt). Running non-interactively from a semantic-releaseprepareCmdin CI.Steps to reproduce
COSIGN_PASSWORD='' cosign generate-key-pairuds-bundle.yaml, run a fully non-interactive create:COSIGN_PASSWORD='' uds create . --signing-key ./cosign.key --confirm--signing-key-passwordentirelyuds create . --signing-key ./cosign.key --confirm < /dev/nullExpected result
With
--confirmset (and/or an explicitly-provided empty--signing-key-password, and/orCOSIGN_PASSWORDexported),uds createsigns the bundle without prompting, and exits non-zero on failure rather than blocking. Bundles signed with an empty-passphrase key should be producible in an unattended pipeline.Actual Result
uds createblocks on an interactive prompt:On a runner with a TTY this hangs until the job times out. Without a TTY,
surveyerrors out instead. Either way there is no flag, env var, or config value that lets you get past it, because an empty passphrase is not expressible.Visual Proof (screenshots, videos, text, etc)
The relevant code is in
[src/pkg/bundle/create.go#L75-L86](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/bundle/create.go#L75-L86):Three separate consequences, all from these lines:
SigningKeyPasswordis a plainstringand the guard is!= "", so "the user explicitly passed an empty password" and "the user passed nothing" collapse into the same case. An empty passphrase is valid for cosign, so this is a genuine gap rather than a missing feature.COSIGN_PASSWORDis ignored. AssigningsignBlobOptions.PassFuncoverrides cosign's own env-aware pass function, which would otherwise honourCOSIGN_PASSWORD(cosign usesos.LookupEnv, so it handles the empty-string case correctly).--confirmis not consulted here.[interactive.PromptSigPassword()](https://github.com/defenseunicorns/uds-cli/blob/v0.34.3/src/pkg/interactive/interactive.go#L12-L23)usessurvey.Password, which requires a TTY. Zarf's equivalent path returns early when confirm is set; UDS CLI does not, so--confirmdoes not imply non-interactive here even though that is what it implies everywhere else in the command.For comparison, Zarf exposes
zarf package sign ... --signing-key-pass, so a package can be signed after the fact. UDS CLI has nouds signcommand, and signing happens mid-create(the signature is written to the temp dir and pushed as an OCI layer by the bundler), so there is no supported way to sign a bundle out of band either.Severity/Priority
Medium. It does not affect deployed bundles, but it blocks automated signed-bundle releases for anyone using an empty-passphrase key, and the failure mode in CI is a hang rather than a clear error. There is a workaround (re-key with a non-empty passphrase), but it is not discoverable from the CLI output or the docs — the prompt explicitly advertises "empty for no password", which is exactly the case that cannot be automated.
Additional Context
Possible fixes, roughly in increasing order of completeness:
--confirm. Returnnil, nil(or[]byte{}, nil) instead of prompting whenconfig.CommonOptions.Confirmis set. Smallest change; makes--confirmmean the same thing here as elsewhere.signBlobOptions.PassFuncunset when no password flag was supplied, soCOSIGN_PASSWORDworks as users coming from cosign would expect.cmd.Flags().Changed("signing-key-password")rather than a!= ""comparison, so an explicit--signing-key-password ''is respected.--signing-key-password-file. Avoids passphrases in process args / CI logs, and sidesteps the empty-string ambiguity entirely. Useful independent of this bug.uds signcommand mirroringzarf package sign, for the post-creation signing case.Happy to open a PR for whichever direction maintainers prefer.