From caa02dfce1204e98709f106174d8d495c272089c Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Tue, 7 Jun 2022 16:06:44 -0400 Subject: [PATCH] keyring command line (#13169) Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com> --- api/keyring.go | 4 +- command/commands.go | 5 + command/operator_secure_variables_keyring.go | 217 +++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 command/operator_secure_variables_keyring.go diff --git a/api/keyring.go b/api/keyring.go index b75cbc681..6b7f5f9d4 100644 --- a/api/keyring.go +++ b/api/keyring.go @@ -80,9 +80,9 @@ func (k *Keyring) Rotate(opts *KeyringRotateOptions, w *WriteOptions) (*RootKeyM qp.Set("full", "true") } } - resp := &RootKeyMeta{} + resp := &struct{ Key *RootKeyMeta }{} wm, err := k.client.write("/v1/operator/keyring/rotate?"+qp.Encode(), nil, resp, w) - return resp, wm, err + return resp.Key, wm, err } // KeyringRotateOptions are parameters for the Rotate API diff --git a/command/commands.go b/command/commands.go index 2776298cf..e3da8dd25 100644 --- a/command/commands.go +++ b/command/commands.go @@ -591,6 +591,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "operator secure-variables keyring": func() (cli.Command, error) { + return &OperatorSecureVariablesKeyringCommand{ + Meta: meta, + }, nil + }, "operator snapshot": func() (cli.Command, error) { return &OperatorSnapshotCommand{ Meta: meta, diff --git a/command/operator_secure_variables_keyring.go b/command/operator_secure_variables_keyring.go new file mode 100644 index 000000000..06bae311f --- /dev/null +++ b/command/operator_secure_variables_keyring.go @@ -0,0 +1,217 @@ +package command + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/posener/complete" + + "github.com/hashicorp/nomad/api" +) + +// OperatorSecureVariablesKeyringCommand is a Command implementation +// that handles querying, installing, and removing secure variables +// encryption keys from a keyring. +type OperatorSecureVariablesKeyringCommand struct { + Meta +} + +func (c *OperatorSecureVariablesKeyringCommand) Help() string { + helpText := ` +Usage: nomad operator secure-variables keyring [options] + + Manages encryption keys used for storing secure variables. This command may be + used to examine active encryption keys in the cluster, rotate keys, add new + keys from backups, or remove unused keys. + + If ACLs are enabled, this command requires a management token. + +General Options: + + ` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + ` + +Keyring Options: + + -install= Install a new encryption key from file. The key file must + be a JSON file previously written by Nomad to the keystore. + + -list List the currently installed keys. This list returns key + metadata and not sensitive key material. + + -remove= Remove the given key from the cluster. This operation may + only be performed on keys that are not the active key. + + -rotate Generate a new encryption key for all future variables. + Allows the use of the -full flag. + + -full When used with -rotate, decrypt all variables and + re-encrypt with the new key. The command will immediately + return and the re-encryption process will run asynchronously + on the leader. + + -verbose Show full information. +` + return strings.TrimSpace(helpText) +} + +func (c *OperatorSecureVariablesKeyringCommand) Synopsis() string { + return "Manages secure variables encryption keys" +} + +func (c *OperatorSecureVariablesKeyringCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-install": complete.PredictFiles("*.json"), + "-list": complete.PredictNothing, + "-remove": complete.PredictAnything, + "-rotate": complete.PredictNothing, + "-full": complete.PredictNothing, + "-verbose": complete.PredictNothing, + }) +} +func (c *OperatorSecureVariablesKeyringCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *OperatorSecureVariablesKeyringCommand) Name() string { return "secure-variables keyring" } + +func (c *OperatorSecureVariablesKeyringCommand) Run(args []string) int { + var installKey, removeKey string + var listKeys, rotateKey, rotateFull, verbose bool + + flags := c.Meta.FlagSet("secure-variables keyring", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + + flags.StringVar(&installKey, "install", "", "install key") + flags.BoolVar(&listKeys, "list", false, "list keys") + flags.StringVar(&removeKey, "remove", "", "remove key") + flags.BoolVar(&rotateKey, "rotate", false, "rotate key") + + flags.BoolVar(&rotateFull, "full", false, "full key rotation") + flags.BoolVar(&verbose, "verbose", false, "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Only accept a single argument + if rotateKey && listKeys { + c.Ui.Error("Only a single action is allowed") + c.Ui.Error(commandErrorText(c)) + return 1 + } + found := listKeys || rotateKey + for _, arg := range []string{installKey, removeKey} { + if found && len(arg) > 0 { + c.Ui.Error("Only a single action is allowed") + c.Ui.Error(commandErrorText(c)) + return 1 + } + found = found || len(arg) > 0 + } + + // Fail fast if no actionable args were passed + if !found { + c.Ui.Error("No actionable argument was passed") + c.Ui.Error("One of '-install', '-list', '-remove' or '-rotate' flags must be set") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error creating nomad cli client: %s", err)) + return 1 + } + + if listKeys { + resp, _, err := client.Keyring().List(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 + } + c.handleKeysResponse(resp, verbose) + return 0 + } + + if rotateKey { + resp, _, err := client.Keyring().Rotate( + &api.KeyringRotateOptions{Full: rotateFull}, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 + } + c.handleKeysResponse([]*api.RootKeyMeta{resp}, verbose) + return 0 + } + + if removeKey != "" { + _, err := client.Keyring().Delete(&api.KeyringDeleteOptions{ + KeyID: removeKey, + }, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 + } + c.Ui.Output(fmt.Sprintf("Removed encryption key %s", removeKey)) + return 0 + } + + if installKey != "" { + + if fi, err := os.Stat(installKey); (installKey == "-" || err == nil) && !fi.IsDir() { + var buf []byte + if installKey == "-" { + buf, err = ioutil.ReadAll(os.Stdin) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err)) + return 1 + } + } else { + buf, err = ioutil.ReadFile(installKey) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err)) + return 1 + } + } + + key := &api.RootKey{} + dec := json.NewDecoder(bytes.NewBuffer(buf)) + if err := dec.Decode(key); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to parse key file: %v", err)) + return 1 + } + + _, err := client.Keyring().Update(key, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 + } + c.Ui.Output(fmt.Sprintf("Installed encryption key %s", key.Meta.KeyID)) + return 0 + } + } + + // Should never make it here + return 0 +} + +func (c *OperatorSecureVariablesKeyringCommand) handleKeysResponse(keys []*api.RootKeyMeta, verbose bool) { + length := fullId + if !verbose { + length = 8 + } + out := make([]string, len(keys)+1) + out[0] = "Key|Active|Create Time" + i := 1 + for _, k := range keys { + out[i] = fmt.Sprintf("%s|%v|%s", + k.KeyID[:length], k.Active, formatTime(k.CreateTime)) + i = i + 1 + } + c.Ui.Output(formatList(out)) +}