From 7ba02a6a4d9419dc1b8d30003c7bd21e15452368 Mon Sep 17 00:00:00 2001 From: Dave Du Cros Date: Wed, 20 Oct 2021 17:29:17 +0100 Subject: [PATCH] operator generate-root -decode: allow token from stdin (#12881) * operator generate-root -decode: allow token from stdin Allow passing "-" as the value for -decode, causing the encoded token to be read from stdin. This is intended to prevent leaking the encoded token + otp into process logs in enterprise environments. * add changelog entry for PR12881 * add check/test for empty decode value passed via stdin --- changelog/12881.txt | 3 + command/operator_generate_root.go | 24 ++++- command/operator_generate_root_test.go | 91 +++++++++++++++++++ .../docs/commands/operator/generate-root.mdx | 1 + 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 changelog/12881.txt diff --git a/changelog/12881.txt b/changelog/12881.txt new file mode 100644 index 0000000000000..3cdf5b4e13f13 --- /dev/null +++ b/changelog/12881.txt @@ -0,0 +1,3 @@ +```release-note:improvement +command: operator generate-root -decode: allow passing encoded token via stdin +``` diff --git a/command/operator_generate_root.go b/command/operator_generate_root.go index 2bbcb11b44043..c0b71e2f85be4 100644 --- a/command/operator_generate_root.go +++ b/command/operator_generate_root.go @@ -130,7 +130,8 @@ func (c *OperatorGenerateRootCommand) Flags() *FlagSets { Default: "", EnvVar: "", Completion: complete.PredictAnything, - Usage: "The value to decode; setting this triggers a decode operation.", + Usage: "The value to decode; setting this triggers a decode operation. " + + " If the value is \"-\" then read the encoded token from stdin.", }) f.BoolVar(&BoolVar{ @@ -328,6 +329,27 @@ func (c *OperatorGenerateRootCommand) decode(client *api.Client, encoded, otp st return 1 } + if encoded == "-" { + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, stdin); err != nil { + c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err)) + return 1 + } + + encoded = buf.String() + + if encoded == "" { + c.UI.Error("Missing encoded value. When using -decode=\"-\" value must be passed via stdin.") + return 1 + } + } + f := client.Sys().GenerateRootStatus switch kind { case generateRootDR: diff --git a/command/operator_generate_root_test.go b/command/operator_generate_root_test.go index c087cacf2b8d4..84ebc3f94de36 100644 --- a/command/operator_generate_root_test.go +++ b/command/operator_generate_root_test.go @@ -1,3 +1,4 @@ +//go:build !race // +build !race package command @@ -158,6 +159,96 @@ func TestOperatorGenerateRootCommand_Run(t *testing.T) { } }) + t.Run("decode_from_stdin", func(t *testing.T) { + t.Parallel() + + encoded := "Bxg9JQQqOCNKBRICNwMIRzo2J3cWCBRi" + otp := "3JhHkONiyiaNYj14nnD9xZQS" + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(encoded)) + stdinW.Close() + }() + + ui, cmd := testOperatorGenerateRootCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + // Simulate piped output to print raw output + old := os.Stdout + _, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + + code := cmd.Run([]string{ + "-decode", "-", // read from stdin + "-otp", otp, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + w.Close() + os.Stdout = old + + expected := "4RUmoevJ3lsLni9sTXcNnRE1" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if combined != expected { + t.Errorf("expected %q to be %q", combined, expected) + } + }) + + t.Run("decode_from_stdin_empty", func(t *testing.T) { + t.Parallel() + + encoded := "" + otp := "3JhHkONiyiaNYj14nnD9xZQS" + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(encoded)) + stdinW.Close() + }() + + ui, cmd := testOperatorGenerateRootCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + // Simulate piped output to print raw output + old := os.Stdout + _, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + + code := cmd.Run([]string{ + "-decode", "-", // read from stdin + "-otp", otp, + }) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + w.Close() + os.Stdout = old + + expected := "Missing encoded value" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + t.Run("cancel", func(t *testing.T) { t.Parallel() diff --git a/website/content/docs/commands/operator/generate-root.mdx b/website/content/docs/commands/operator/generate-root.mdx index 1c7ea6d31544e..796e8eb56c78f 100644 --- a/website/content/docs/commands/operator/generate-root.mdx +++ b/website/content/docs/commands/operator/generate-root.mdx @@ -68,6 +68,7 @@ flags](/docs/commands) included on all commands. - `-decode` `(string: "")` - Decode and output the generated root token. This option requires the `-otp` flag be set to the OTP used during initialization. + If value is "-" then read the encoded token from stdin. - `-generate-otp` `(bool: false)` - Generate and print a high-entropy one-time-password (OTP) suitable for use with the "-init" flag.