mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-06 06:37:02 +02:00
We have many hand-written String() methods (and similar) for enums. These require more maintenance and are more error-prone than using automatically generated methods. In addition, the auto-generated versions can be more efficient. Here, we switch to using https://github.com/loggerhead/enumer, itself a fork of https://github.com/diegostamigni/enumer, no longer maintained, and a fork of the mostly standard tool https://pkg.go.dev/golang.org/x/tools/cmd/stringer. We use this fork of enumer for Go 1.20+ compatibility and because we require the `-transform` flag to be able to generate constants that match our current code base. Some enums were not targeted for this change:
630 lines
18 KiB
Go
630 lines
18 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/cli"
|
|
"github.com/hashicorp/go-secure-stdlib/password"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
|
"github.com/hashicorp/vault/sdk/helper/roottoken"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
var (
|
|
_ cli.Command = (*OperatorGenerateRootCommand)(nil)
|
|
_ cli.CommandAutocomplete = (*OperatorGenerateRootCommand)(nil)
|
|
)
|
|
|
|
//go:generate enumer -type=generateRootKind -trimprefix=generateRoot
|
|
type generateRootKind int
|
|
|
|
const (
|
|
generateRootRegular generateRootKind = iota
|
|
generateRootDR
|
|
generateRootRecovery
|
|
)
|
|
|
|
type OperatorGenerateRootCommand struct {
|
|
*BaseCommand
|
|
|
|
flagInit bool
|
|
flagCancel bool
|
|
flagStatus bool
|
|
flagDecode string
|
|
flagOTP string
|
|
flagPGPKey string
|
|
flagNonce string
|
|
flagGenerateOTP bool
|
|
flagDRToken bool
|
|
flagRecoveryToken bool
|
|
|
|
testStdin io.Reader // for tests
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) Synopsis() string {
|
|
return "Generates a new root, DR operation, or recovery token"
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) Help() string {
|
|
helpText := `
|
|
Usage: vault operator generate-root [options] -init [-otp=...] [-pgp-key=...]
|
|
vault operator generate-root [options] [-nonce=... KEY]
|
|
vault operator generate-root [options] -decode=... -otp=...
|
|
vault operator generate-root [options] -generate-otp
|
|
vault operator generate-root [options] -status
|
|
vault operator generate-root [options] -cancel
|
|
|
|
Generates a new root token by combining a quorum of share holders.
|
|
|
|
This command is unusual, as it is effectively six separate subcommands,
|
|
selected via the options -init, -decode, -generate-otp, -status, -cancel,
|
|
or the absence of any of the previous five options (which selects the
|
|
"provide a key share" form).
|
|
|
|
With the -dr-token or -recovery-token options, a DR operation token or a
|
|
recovery token is generated instead of a root token - the relevant option
|
|
must be included in every form of the generate-root command.
|
|
|
|
Form 1 (-init) - Start a token generation:
|
|
|
|
When starting a root or privileged operation token generation, you must
|
|
choose one of the following protection methods for how the token will be
|
|
returned:
|
|
|
|
- A base64-encoded one-time-password (OTP). The resulting token is XORed
|
|
with this value when it is returned. Use the "-decode" form of this
|
|
command to output the final value.
|
|
|
|
The Vault server will generate a suitable OTP for you, and return it:
|
|
|
|
$ vault operator generate-root -init
|
|
|
|
Vault versions before 0.11.2, released in 2018, required you to
|
|
generate your own OTP (see the "-generate-otp" form) and pass it in,
|
|
but this is no longer necessary. The command is still supported for
|
|
compatibility, though:
|
|
|
|
$ vault operator generate-root -init -otp="..."
|
|
|
|
- A PGP key. The resulting token is encrypted with this public key.
|
|
The key may be specified as a path to a file, or a string of the
|
|
form "keybase:<username>" to fetch the key from the keybase.io API.
|
|
|
|
$ vault operator generate-root -init -pgp-key="..."
|
|
|
|
Form 2 (no option) - Enter an unseal key to progress root token generation:
|
|
|
|
In the sub-form intended for interactive use, the command will
|
|
automatically look up the nonce of the currently active generation
|
|
operation, and will prompt for the key to be entered:
|
|
|
|
$ vault operator generate-root
|
|
|
|
In the sub-form intended for automation, the operation nonce must be
|
|
explicitly provided, and the key is provided directly on the command line
|
|
|
|
$ vault operator generate-root -nonce=... KEY
|
|
|
|
If key is specified as "-", the command will read from stdin.
|
|
|
|
Form 3 (-decode) - Decode a generated token protected with an OTP:
|
|
|
|
$ vault operator generate-root -decode=ENCODED_TOKEN -otp=OTP
|
|
|
|
If encoded token is specified as "-", the command will read from stdin.
|
|
|
|
Form 4 (-generate-otp) - Generate an OTP code for the final token:
|
|
|
|
$ vault operator generate-root -generate-otp
|
|
|
|
Since changes in Vault 0.11.2 in 2018, there is no longer any reason to
|
|
use this form, as a suitable OTP will be returned as part of the "-init"
|
|
command.
|
|
|
|
Form 5 (-status) - Get the status of a token generation that is in progress:
|
|
|
|
$ vault operator generate-root -status
|
|
|
|
This form also returns the length of the a correct OTP, for the running
|
|
version and configuration of Vault.
|
|
|
|
Form 6 (-cancel) - Cancel a token generation that is in progress:
|
|
|
|
This would be used to remove an in progress generation operation, so that
|
|
a new one can be started with different parameters.
|
|
|
|
$ vault operator generate-root -cancel
|
|
|
|
` + c.Flags().Help()
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) Flags() *FlagSets {
|
|
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
|
|
|
f := set.NewFlagSet("Command Options")
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "init",
|
|
Target: &c.flagInit,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Start a root token generation. This can only be done if " +
|
|
"there is not currently one in progress.",
|
|
})
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "cancel",
|
|
Target: &c.flagCancel,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Reset the root token generation progress. This will discard any " +
|
|
"submitted unseal keys or configuration.",
|
|
})
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "status",
|
|
Target: &c.flagStatus,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Print the status of the current attempt without providing an " +
|
|
"unseal key.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "decode",
|
|
Target: &c.flagDecode,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
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{
|
|
Name: "generate-otp",
|
|
Target: &c.flagGenerateOTP,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Generate and print a high-entropy one-time-password (OTP) " +
|
|
"suitable for use with the \"-init\" flag.",
|
|
})
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "dr-token",
|
|
Target: &c.flagDRToken,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Set this flag to do generate root operations on DR operation " +
|
|
"tokens.",
|
|
})
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "recovery-token",
|
|
Target: &c.flagRecoveryToken,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Set this flag to do generate root operations on recovery " +
|
|
"tokens.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "otp",
|
|
Target: &c.flagOTP,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "OTP code to use with \"-decode\" or \"-init\".",
|
|
})
|
|
|
|
f.VarFlag(&VarFlag{
|
|
Name: "pgp-key",
|
|
Value: (*pgpkeys.PubKeyFileFlag)(&c.flagPGPKey),
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "Path to a file on disk containing a binary or base64-encoded " +
|
|
"public PGP key. This can also be specified as a Keybase username " +
|
|
"using the format \"keybase:<username>\". When supplied, the generated " +
|
|
"root token will be encrypted and base64-encoded with the given public " +
|
|
"key. Must be used with \"-init\".",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "nonce",
|
|
Target: &c.flagNonce,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "Nonce value returned at initialization. The same nonce value " +
|
|
"must be provided with each unseal or recovery key. Only needed " +
|
|
"when providing an unseal or recovery key.",
|
|
})
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) AutocompleteArgs() complete.Predictor {
|
|
return nil
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) AutocompleteFlags() complete.Flags {
|
|
return c.Flags().Completions()
|
|
}
|
|
|
|
func (c *OperatorGenerateRootCommand) Run(args []string) int {
|
|
f := c.Flags()
|
|
|
|
if err := f.Parse(args); err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
args = f.Args()
|
|
if len(args) > 1 {
|
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args)))
|
|
return 1
|
|
}
|
|
|
|
if c.flagDRToken && c.flagRecoveryToken {
|
|
c.UI.Error("Both -recovery-token and -dr-token flags are set")
|
|
return 1
|
|
}
|
|
|
|
client, err := c.Client()
|
|
if err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 2
|
|
}
|
|
|
|
kind := generateRootRegular
|
|
switch {
|
|
case c.flagDRToken:
|
|
kind = generateRootDR
|
|
case c.flagRecoveryToken:
|
|
kind = generateRootRecovery
|
|
}
|
|
|
|
switch {
|
|
case c.flagGenerateOTP:
|
|
otp, code := c.generateOTP(client, kind)
|
|
if code == 0 {
|
|
switch Format(c.UI) {
|
|
case "", "table":
|
|
return PrintRaw(c.UI, otp)
|
|
default:
|
|
status := map[string]interface{}{
|
|
"otp": otp,
|
|
"otp_length": len(otp),
|
|
}
|
|
return OutputData(c.UI, status)
|
|
}
|
|
}
|
|
return code
|
|
case c.flagDecode != "":
|
|
return c.decode(client, c.flagDecode, c.flagOTP, kind)
|
|
case c.flagCancel:
|
|
return c.cancel(client, kind)
|
|
case c.flagInit:
|
|
return c.init(client, c.flagOTP, c.flagPGPKey, kind)
|
|
case c.flagStatus:
|
|
return c.status(client, kind)
|
|
default:
|
|
// If there are no other flags, prompt for an unseal key.
|
|
key := ""
|
|
if len(args) > 0 {
|
|
key = strings.TrimSpace(args[0])
|
|
}
|
|
return c.provide(client, key, kind)
|
|
}
|
|
}
|
|
|
|
// generateOTP generates a suitable OTP code for generating a root token.
|
|
func (c *OperatorGenerateRootCommand) generateOTP(client *api.Client, kind generateRootKind) (string, int) {
|
|
f := client.Sys().GenerateRootStatus
|
|
switch kind {
|
|
case generateRootDR:
|
|
f = client.Sys().GenerateDROperationTokenStatus
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenStatus
|
|
}
|
|
|
|
status, err := f()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
|
|
return "", 2
|
|
}
|
|
|
|
otp, err := roottoken.GenerateOTP(status.OTPLength)
|
|
var retCode int
|
|
if err != nil {
|
|
retCode = 2
|
|
c.UI.Error(err.Error())
|
|
} else {
|
|
retCode = 0
|
|
}
|
|
return otp, retCode
|
|
}
|
|
|
|
// decode decodes the given value using the otp.
|
|
func (c *OperatorGenerateRootCommand) decode(client *api.Client, encoded, otp string, kind generateRootKind) int {
|
|
if encoded == "" {
|
|
c.UI.Error("Missing encoded value: use -decode=<string> to supply it")
|
|
return 1
|
|
}
|
|
if otp == "" {
|
|
c.UI.Error("Missing otp: use -otp to supply it")
|
|
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:
|
|
f = client.Sys().GenerateDROperationTokenStatus
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenStatus
|
|
}
|
|
|
|
status, err := f()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
|
|
return 2
|
|
}
|
|
|
|
token, err := roottoken.DecodeToken(encoded, otp, status.OTPLength)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error decoding root token: %s", err))
|
|
return 1
|
|
}
|
|
|
|
switch Format(c.UI) {
|
|
case "", "table":
|
|
return PrintRaw(c.UI, token)
|
|
default:
|
|
tokenJSON := map[string]interface{}{
|
|
"token": token,
|
|
}
|
|
return OutputData(c.UI, tokenJSON)
|
|
}
|
|
}
|
|
|
|
// init is used to start the generation process
|
|
func (c *OperatorGenerateRootCommand) init(client *api.Client, otp, pgpKey string, kind generateRootKind) int {
|
|
// Validate incoming fields. Either OTP OR PGP keys must be supplied.
|
|
if otp != "" && pgpKey != "" {
|
|
c.UI.Error("Error initializing: cannot specify both -otp and -pgp-key")
|
|
return 1
|
|
}
|
|
|
|
// Start the root generation
|
|
f := client.Sys().GenerateRootInit
|
|
switch kind {
|
|
case generateRootDR:
|
|
f = client.Sys().GenerateDROperationTokenInit
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenInit
|
|
}
|
|
status, err := f(otp, pgpKey)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error initializing root generation: %s", err))
|
|
return 2
|
|
}
|
|
|
|
switch Format(c.UI) {
|
|
case "table":
|
|
return c.printStatus(status)
|
|
default:
|
|
return OutputData(c.UI, status)
|
|
}
|
|
}
|
|
|
|
// provide prompts the user for the seal key and posts it to the update root
|
|
// endpoint. If this is the last unseal, this function outputs it.
|
|
func (c *OperatorGenerateRootCommand) provide(client *api.Client, key string, kind generateRootKind) int {
|
|
f := client.Sys().GenerateRootStatus
|
|
switch kind {
|
|
case generateRootDR:
|
|
f = client.Sys().GenerateDROperationTokenStatus
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenStatus
|
|
}
|
|
status, err := f()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
|
|
return 2
|
|
}
|
|
|
|
// Verify a root token generation is in progress. If there is not one in
|
|
// progress, return an error instructing the user to start one.
|
|
if !status.Started {
|
|
c.UI.Error(wrapAtLength(
|
|
"No root generation is in progress. Start a root generation by " +
|
|
"running \"vault operator generate-root -init\"."))
|
|
c.UI.Warn(wrapAtLength(fmt.Sprintf(
|
|
"If starting root generation using the OTP method and generating "+
|
|
"your own OTP, the length of the OTP string needs to be %d "+
|
|
"characters in length.", status.OTPLength)))
|
|
return 1
|
|
}
|
|
|
|
var nonce string
|
|
|
|
switch key {
|
|
case "-": // Read from stdin
|
|
nonce = c.flagNonce
|
|
|
|
// 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
|
|
}
|
|
|
|
key = buf.String()
|
|
case "": // Prompt using the tty
|
|
// Nonce value is not required if we are prompting via the terminal
|
|
nonce = status.Nonce
|
|
|
|
w := getWriterFromUI(c.UI)
|
|
fmt.Fprintf(w, "Operation nonce: %s\n", nonce)
|
|
fmt.Fprintf(w, "Unseal Key (will be hidden): ")
|
|
key, err = password.Read(os.Stdin)
|
|
fmt.Fprintf(w, "\n")
|
|
if err != nil {
|
|
if err == password.ErrInterrupted {
|
|
c.UI.Error("user canceled")
|
|
return 1
|
|
}
|
|
|
|
c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
|
|
"ask for the unseal key. The raw error message is shown below, but "+
|
|
"usually this is because you attempted to pipe a value into the "+
|
|
"command or you are executing outside of a terminal (tty). If you "+
|
|
"want to pipe the value, pass \"-\" as the argument to read from "+
|
|
"stdin. The raw error was: %s", err)))
|
|
return 1
|
|
}
|
|
default: // Supplied directly as an arg
|
|
nonce = c.flagNonce
|
|
}
|
|
|
|
// Trim any whitespace from they key, especially since we might have prompted
|
|
// the user for it.
|
|
key = strings.TrimSpace(key)
|
|
|
|
// Verify we have a nonce value
|
|
if nonce == "" {
|
|
c.UI.Error("Missing nonce value: specify it via the -nonce flag")
|
|
return 1
|
|
}
|
|
|
|
// Provide the key, this may potentially complete the update
|
|
fUpd := client.Sys().GenerateRootUpdate
|
|
switch kind {
|
|
case generateRootDR:
|
|
fUpd = client.Sys().GenerateDROperationTokenUpdate
|
|
case generateRootRecovery:
|
|
fUpd = client.Sys().GenerateRecoveryOperationTokenUpdate
|
|
}
|
|
status, err = fUpd(key, nonce)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err))
|
|
return 2
|
|
}
|
|
switch Format(c.UI) {
|
|
case "table":
|
|
return c.printStatus(status)
|
|
default:
|
|
return OutputData(c.UI, status)
|
|
}
|
|
}
|
|
|
|
// cancel cancels the root token generation
|
|
func (c *OperatorGenerateRootCommand) cancel(client *api.Client, kind generateRootKind) int {
|
|
f := client.Sys().GenerateRootCancel
|
|
switch kind {
|
|
case generateRootDR:
|
|
f = client.Sys().GenerateDROperationTokenCancel
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenCancel
|
|
}
|
|
if err := f(); err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error canceling root token generation: %s", err))
|
|
return 2
|
|
}
|
|
c.UI.Output("Success! Root token generation canceled (if it was started)")
|
|
return 0
|
|
}
|
|
|
|
// status is used just to fetch and dump the status
|
|
func (c *OperatorGenerateRootCommand) status(client *api.Client, kind generateRootKind) int {
|
|
f := client.Sys().GenerateRootStatus
|
|
switch kind {
|
|
case generateRootDR:
|
|
f = client.Sys().GenerateDROperationTokenStatus
|
|
case generateRootRecovery:
|
|
f = client.Sys().GenerateRecoveryOperationTokenStatus
|
|
}
|
|
|
|
status, err := f()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
|
|
return 2
|
|
}
|
|
switch Format(c.UI) {
|
|
case "table":
|
|
return c.printStatus(status)
|
|
default:
|
|
return OutputData(c.UI, status)
|
|
}
|
|
}
|
|
|
|
// printStatus dumps the status to output
|
|
func (c *OperatorGenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int {
|
|
out := []string{}
|
|
out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce))
|
|
out = append(out, fmt.Sprintf("Started | %t", status.Started))
|
|
out = append(out, fmt.Sprintf("Progress | %d/%d", status.Progress, status.Required))
|
|
out = append(out, fmt.Sprintf("Complete | %t", status.Complete))
|
|
if status.PGPFingerprint != "" {
|
|
out = append(out, fmt.Sprintf("PGP Fingerprint | %s", status.PGPFingerprint))
|
|
}
|
|
switch {
|
|
case status.EncodedToken != "":
|
|
out = append(out, fmt.Sprintf("Encoded Token | %s", status.EncodedToken))
|
|
case status.EncodedRootToken != "":
|
|
out = append(out, fmt.Sprintf("Encoded Root Token | %s", status.EncodedRootToken))
|
|
}
|
|
if status.OTP != "" {
|
|
c.UI.Warn(wrapAtLength("A One-Time-Password has been generated for you and is shown in the OTP field. You will need this value to decode the resulting root token, so keep it safe."))
|
|
out = append(out, fmt.Sprintf("OTP | %s", status.OTP))
|
|
}
|
|
if status.OTPLength != 0 {
|
|
out = append(out, fmt.Sprintf("OTP Length | %d", status.OTPLength))
|
|
}
|
|
|
|
output := columnOutput(out, nil)
|
|
c.UI.Output(output)
|
|
return 0
|
|
}
|