slackhq/nebula · error

ca-key is encrypted and must be decrypted interactively

Error message

ca-key is encrypted and must be decrypted interactively

What it means

When the CA key PEM is encrypted (UnmarshalSigningPrivateKeyFromPEM returns cert.ErrPrivateKeyEncrypted), signCert prompts up to 5 times for a passphrase. If the password reader reports ErrNoTerminal (stdin/stdout is not an interactive TTY, e.g. piped input or a CI job), it cannot prompt and returns this error immediately instead of looping.

Source

Thrown at cmd/nebula-cert/sign.go:154

		var rawCAKey []byte
		rawCAKey, err = readInput("ca-key", *sf.caKeyPath, &claims)
		if err != nil {
			return fmt.Errorf("error while reading ca-key: %s", err)
		}

		// naively attempt to decode the private key as though it is not encrypted
		caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
		if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
			var passphrase []byte
			passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
			if len(passphrase) == 0 {
				// ask for a passphrase until we get one
				for i := 0; i < 5; i++ {
					errOut.Write([]byte("Enter passphrase: "))
					passphrase, err = pr.ReadPassword()

					if errors.Is(err, ErrNoTerminal) {
						return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
					} else if err != nil {
						return fmt.Errorf("error reading password: %s", err)
					}

					if len(passphrase) > 0 {
						break
					}
				}
				if len(passphrase) == 0 {
					return fmt.Errorf("cannot open encrypted ca-key without passphrase")
				}
			}
			curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
			if err != nil {
				return fmt.Errorf("error while parsing encrypted ca-key: %s", err)
			}
		} else if err != nil {
			return fmt.Errorf("error while parsing ca-key: %s", err)

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Decrypt the CA key once, interactively, and sign with the plaintext key file
  2. Provide a pty: run under `ssh -t` or use a terminal for the command
  3. Use a version/mechanism that accepts the passphrase non-interactively (e.g. environment/flag supported by your build) rather than prompting
  4. Store an unencrypted CA key in a secret manager and mount it for automated signing

Example fix

// before
nebula-cert sign -ca-key encrypted-ca.key < input.txt   # no TTY, cannot prompt
// after
nebula-cert sign -ca-key encrypted-ca.key               # run in an interactive terminal, enter passphrase when prompted
Defensive patterns

Strategy: fallback

Validate before calling

data, _ := os.ReadFile(caKeyPath)
if _, _, _, err := cert.UnmarshalSigningPrivateKeyFromPEM(data); errors.Is(err, cert.ErrPrivateKeyEncrypted) && !isATTY(os.Stdin) {
    return fmt.Errorf("encrypted CA key but no TTY; decrypt the key first")
}

Type guard

func requiresInteractiveDecrypt(err error) bool {
    return strings.Contains(err.Error(), "must be decrypted interactively")
}

Try / catch

if err := signCert(args, out, errOut, StdinPasswordReader{}); err != nil {
    if strings.Contains(err.Error(), "must be decrypted interactively") {
        log.Fatalf("allocate a TTY (e.g. ssh -t) or use an unencrypted CA key for automation")
    }
}

Prevention

When it happens

Trigger: signing with an encrypted CA key while stdin is not a terminal: `cat pass.txt | nebula-cert sign -ca-key encrypted-ca.key ...`, running from a systemd unit/CI runner with no TTY, or invoking via exec without a pty

Common situations: automation scripts feeding input via pipes; containerized signing jobs; SSH sessions without -t so no pty is allocated; scheduled jobs attempting interactive decryption

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/e2cff91fbe9a2d4e. Report an issue: GitHub.