From 4df8bcb1f52f96f8aa9a545dd26a7b15500b5e23 Mon Sep 17 00:00:00 2001 From: Hal Martin <1230969+halmartin@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:41:08 +0100 Subject: [PATCH] nebula-cert: support reading CA passphrase from env (#1421) * nebula-cert: support reading CA passphrase from env This patch extends the `nebula-cert` command to support reading the CA passphrase from the environment variable `CA_PASSPHRASE`. Currently `nebula-cert` depends in an interactive session to obtain the CA passphrase. This presents a challenge for automation tools like ansible. With this change, ansible can store the CA passphrase in a vault and supply it to `nebula-cert` via the `CA_PASSPHRASE` environment variable for non-interactive signing. Signed-off-by: Hal Martin <1230969+halmartin@users.noreply.github.com> * name the variable NEBULA_CA_PASSPHRASE --------- Signed-off-by: Hal Martin <1230969+halmartin@users.noreply.github.com> Co-authored-by: JackDoan --- cmd/nebula-cert/ca.go | 35 +++++++++++++++++++---------------- cmd/nebula-cert/ca_test.go | 11 +++++++++++ cmd/nebula-cert/sign.go | 36 +++++++++++++++++++----------------- cmd/nebula-cert/sign_test.go | 20 ++++++++++++++++++++ 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/cmd/nebula-cert/ca.go b/cmd/nebula-cert/ca.go index f83c94f..cd9b82f 100644 --- a/cmd/nebula-cert/ca.go +++ b/cmd/nebula-cert/ca.go @@ -173,23 +173,26 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error var passphrase []byte if !isP11 && *cf.encryption { - for i := 0; i < 5; i++ { - out.Write([]byte("Enter passphrase: ")) - passphrase, err = pr.ReadPassword() - - if err == ErrNoTerminal { - return fmt.Errorf("out-key must be encrypted interactively") - } else if err != nil { - return fmt.Errorf("error reading passphrase: %s", err) - } - - if len(passphrase) > 0 { - break - } - } - + passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE")) if len(passphrase) == 0 { - return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext") + for i := 0; i < 5; i++ { + out.Write([]byte("Enter passphrase: ")) + passphrase, err = pr.ReadPassword() + + if err == ErrNoTerminal { + return fmt.Errorf("out-key must be encrypted interactively") + } else if err != nil { + return fmt.Errorf("error reading passphrase: %s", err) + } + + if len(passphrase) > 0 { + break + } + } + + if len(passphrase) == 0 { + return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext") + } } } diff --git a/cmd/nebula-cert/ca_test.go b/cmd/nebula-cert/ca_test.go index b1cbde9..cd3f0bf 100644 --- a/cmd/nebula-cert/ca_test.go +++ b/cmd/nebula-cert/ca_test.go @@ -171,6 +171,17 @@ func Test_ca(t *testing.T) { assert.Equal(t, pwPromptOb, ob.String()) assert.Empty(t, eb.String()) + // test encrypted key with passphrase environment variable + os.Remove(keyF.Name()) + os.Remove(crtF.Name()) + ob.Reset() + eb.Reset() + args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()} + os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase)) + require.NoError(t, ca(args, ob, eb, testpw)) + assert.Empty(t, eb.String()) + os.Setenv("NEBULA_CA_PASSPHRASE", "") + // read encrypted key file and verify default params rb, _ = os.ReadFile(keyF.Name()) k, _ := pem.Decode(rb) diff --git a/cmd/nebula-cert/sign.go b/cmd/nebula-cert/sign.go index ebcb592..3631c50 100644 --- a/cmd/nebula-cert/sign.go +++ b/cmd/nebula-cert/sign.go @@ -116,26 +116,28 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) // 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) { - // ask for a passphrase until we get one var passphrase []byte - for i := 0; i < 5; i++ { - out.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 - } - } + passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE")) if len(passphrase) == 0 { - return fmt.Errorf("cannot open encrypted ca-key without passphrase") - } + // ask for a passphrase until we get one + for i := 0; i < 5; i++ { + out.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) diff --git a/cmd/nebula-cert/sign_test.go b/cmd/nebula-cert/sign_test.go index b2bba76..12eddf6 100644 --- a/cmd/nebula-cert/sign_test.go +++ b/cmd/nebula-cert/sign_test.go @@ -379,6 +379,15 @@ func Test_signCert(t *testing.T) { assert.Equal(t, "Enter passphrase: ", ob.String()) assert.Empty(t, eb.String()) + // test with the proper password in the environment + os.Remove(crtF.Name()) + os.Remove(keyF.Name()) + args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} + os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase)) + require.NoError(t, signCert(args, ob, eb, testpw)) + assert.Empty(t, eb.String()) + os.Setenv("NEBULA_CA_PASSPHRASE", "") + // test with the wrong password ob.Reset() eb.Reset() @@ -389,6 +398,17 @@ func Test_signCert(t *testing.T) { assert.Equal(t, "Enter passphrase: ", ob.String()) assert.Empty(t, eb.String()) + // test with the wrong password in environment + ob.Reset() + eb.Reset() + + os.Setenv("NEBULA_CA_PASSPHRASE", "invalid password") + args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} + require.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing encrypted ca-key: invalid passphrase or corrupt private key") + assert.Empty(t, ob.String()) + assert.Empty(t, eb.String()) + os.Setenv("NEBULA_CA_PASSPHRASE", "") + // test with the user not entering a password ob.Reset() eb.Reset()