Repository: hashicorp/vault (Go)

Issue:
operator generate-root -decode: allow passing encoded token via stdin
**Is your feature request related to a problem? Please describe.**
In our environment processes and their arguments are logged. Passing both the encoded token and the otp as command line arguments would allow anyone with access to these logs to decode our Vault root token.

**Describe the solution you'd like**
The solution proposed in #12881 allows passing the encoded token via stdin rather than a command line argument. Passing the otp via stdin was another option but it's used across a few commands.



Discussion:
Hi @davidducros, thank you for your submission! I have provided feedback from my review of the referenced pull request.



Patch:
diff --git a/changelog/12881.txt b/changelog/12881.txt
new file mode 100644
index 00000000000..3cdf5b4e13f
--- /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 2bbcb11b440..c0b71e2f85b 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/website/content/docs/commands/operator/generate-root.mdx b/website/content/docs/commands/operator/generate-root.mdx
index 1c7ea6d3154..796e8eb56c7 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.


Test patch:
diff --git a/command/operator_generate_root_test.go b/command/operator_generate_root_test.go
index c087cacf2b8..84ebc3f94de 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()