keyring CLI: refactor to use subcommands (#13351)

Split the flag options for the `secure-variables keyring` into their
own subcommands. The gossip keyring CLI will be similarly refactored
and the old version will be deprecated.
This commit is contained in:
Tim Gross
2022-06-14 11:22:42 -04:00
parent b5b310a1af
commit 5c6ba1cce6
6 changed files with 432 additions and 153 deletions

View File

@@ -596,6 +596,26 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"operator secure-variables keyring install": func() (cli.Command, error) {
return &OperatorSecureVariablesKeyringInstallCommand{
Meta: meta,
}, nil
},
"operator secure-variables keyring list": func() (cli.Command, error) {
return &OperatorSecureVariablesKeyringListCommand{
Meta: meta,
}, nil
},
"operator secure-variables keyring remove": func() (cli.Command, error) {
return &OperatorSecureVariablesKeyringRemoveCommand{
Meta: meta,
}, nil
},
"operator secure-variables keyring rotate": func() (cli.Command, error) {
return &OperatorSecureVariablesKeyringRotateCommand{
Meta: meta,
}, nil
},
"operator snapshot": func() (cli.Command, error) {
return &OperatorSnapshotCommand{
Meta: meta,

View File

@@ -1,13 +1,10 @@
package command
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/hashicorp/nomad/api"
@@ -28,32 +25,25 @@ Usage: nomad operator secure-variables keyring [options]
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.
If ACLs are enabled, all subcommands requires a management token.
General Options:
Rotate the encryption key:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
$ nomad operator secure-variables keyring rotate
Keyring Options:
List all encryption key metadata:
-install=<file> Install a new encryption key from file. The key file must
be a JSON file previously written by Nomad to the keystore.
$ nomad operator secure-variables keyring list
-list List the currently installed keys. This list returns key
metadata and not sensitive key material.
Remove an encryption key from the keyring:
-remove=<key> Remove the given key from the cluster. This operation may
only be performed on keys that are not the active key.
$ nomad operator secure-variables keyring remove <key ID>
-rotate Generate a new encryption key for all future variables.
Allows the use of the -full flag.
Install an encryption key from backup:
-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.
$ nomad operator secure-variables keyring install <path to .json file>
-verbose Show full information.
Please see individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
@@ -63,144 +53,24 @@ func (c *OperatorSecureVariablesKeyringCommand) Synopsis() string {
}
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,
})
return c.Meta.AutocompleteFlags(FlagSetClient)
}
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) Name() string {
return "secure-variables keyring"
}
func (c *OperatorSecureVariablesKeyringCommand) handleKeysResponse(keys []*api.RootKeyMeta, verbose bool) {
func (c *OperatorSecureVariablesKeyringCommand) Run(args []string) int {
return cli.RunResultHelp
}
// renderSecureVariablesKeysResponse is a helper for formatting the
// keyring API responses
func renderSecureVariablesKeysResponse(keys []*api.RootKeyMeta, verbose bool) string {
length := fullId
if !verbose {
length = 8
@@ -213,5 +83,5 @@ func (c *OperatorSecureVariablesKeyringCommand) handleKeysResponse(keys []*api.R
k.KeyID[:length], k.Active, formatTime(k.CreateTime))
i = i + 1
}
c.Ui.Output(formatList(out))
return formatList(out)
}

View File

@@ -0,0 +1,122 @@
package command
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
// OperatorSecureVariablesKeyringInstallCommand is a Command
// implementation that handles installing secure variables encryption
// keys from a keyring.
type OperatorSecureVariablesKeyringInstallCommand struct {
Meta
}
func (c *OperatorSecureVariablesKeyringInstallCommand) Help() string {
helpText := `
Usage: nomad operator secure-variables keyring install [options] <filepath>
Install a new encryption key used for storage secure variables. The key file
must be a JSON file previously written by Nomad to the keystore. The key
file will be read from stdin by specifying "-", otherwise a path to the file
is expected.
If ACLs are enabled, this command requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Keyring Options:
-verbose
Show full information.
`
return strings.TrimSpace(helpText)
}
func (c *OperatorSecureVariablesKeyringInstallCommand) Synopsis() string {
return "Installs a secure variables encryption key"
}
func (c *OperatorSecureVariablesKeyringInstallCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-verbose": complete.PredictNothing,
})
}
func (c *OperatorSecureVariablesKeyringInstallCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictFiles("*.json")
}
func (c *OperatorSecureVariablesKeyringInstallCommand) Name() string {
return "secure-variables keyring install"
}
func (c *OperatorSecureVariablesKeyringInstallCommand) Run(args []string) int {
var verbose bool
flags := c.Meta.FlagSet("secure-variables keyring install", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
c.Ui.Error("This command requires one argument: <filepath>")
c.Ui.Error(commandErrorText(c))
return 1
}
installKey := args[0]
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating nomad cli client: %s", err))
return 1
}
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
}

View File

@@ -0,0 +1,88 @@
package command
import (
"fmt"
"strings"
"github.com/posener/complete"
)
// OperatorSecureVariablesKeyringListCommand is a Command
// implementation that lists the secure variables encryption keys.
type OperatorSecureVariablesKeyringListCommand struct {
Meta
}
func (c *OperatorSecureVariablesKeyringListCommand) Help() string {
helpText := `
Usage: nomad operator secure-variables keyring list [options]
List the currently installed keys. This list returns key metadata and not
sensitive key material.
If ACLs are enabled, this command requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Keyring Options:
-verbose
Show full information.
`
return strings.TrimSpace(helpText)
}
func (c *OperatorSecureVariablesKeyringListCommand) Synopsis() string {
return "Lists the secure variables encryption keys"
}
func (c *OperatorSecureVariablesKeyringListCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-verbose": complete.PredictNothing,
})
}
func (c *OperatorSecureVariablesKeyringListCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *OperatorSecureVariablesKeyringListCommand) Name() string {
return "secure-variables keyring list"
}
func (c *OperatorSecureVariablesKeyringListCommand) Run(args []string) int {
var verbose bool
flags := c.Meta.FlagSet("secure-variables keyring list", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 0 {
c.Ui.Error("This command requires no arguments.")
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
}
resp, _, err := client.Keyring().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
c.Ui.Output(renderSecureVariablesKeysResponse(resp, verbose))
return 0
}

View File

@@ -0,0 +1,83 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
// OperatorSecureVariablesKeyringRemoveCommand is a Command
// implementation that handles removeing secure variables encryption
// keys from a keyring.
type OperatorSecureVariablesKeyringRemoveCommand struct {
Meta
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) Help() string {
helpText := `
Usage: nomad operator secure-variables keyring remove [options] <key ID>
Remove an encryption key from the cluster. This operation may only be
performed on keys that are not the active key.
If ACLs are enabled, this command requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
return strings.TrimSpace(helpText)
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) Synopsis() string {
return "Removes a secure variables encryption key"
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) AutocompleteFlags() complete.Flags {
return c.Meta.AutocompleteFlags(FlagSetClient)
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) Name() string {
return "secure-variables keyring remove"
}
func (c *OperatorSecureVariablesKeyringRemoveCommand) Run(args []string) int {
var verbose bool
flags := c.Meta.FlagSet("secure-variables keyring remove", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
c.Ui.Error("This command requires one argument: <key ID>")
c.Ui.Error(commandErrorText(c))
return 1
}
removeKey := args[0]
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating nomad cli client: %s", err))
return 1
}
_, 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
}

View File

@@ -0,0 +1,96 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
// OperatorSecureVariablesKeyringRotateCommand is a Command
// implementation that rotates the secure variables encryption key.
type OperatorSecureVariablesKeyringRotateCommand struct {
Meta
}
func (c *OperatorSecureVariablesKeyringRotateCommand) Help() string {
helpText := `
Usage: nomad operator secure-variables keyring rotate [options]
Generate a new encryption key for all future variables.
If ACLs are enabled, this command requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Keyring Options:
-full
Decrypt all existing variables and re-encrypt with the new key. This 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 *OperatorSecureVariablesKeyringRotateCommand) Synopsis() string {
return "Rotates the secure variables encryption key"
}
func (c *OperatorSecureVariablesKeyringRotateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-full": complete.PredictNothing,
"-verbose": complete.PredictNothing,
})
}
func (c *OperatorSecureVariablesKeyringRotateCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *OperatorSecureVariablesKeyringRotateCommand) Name() string {
return "secure-variables keyring rotate"
}
func (c *OperatorSecureVariablesKeyringRotateCommand) Run(args []string) int {
var rotateFull, verbose bool
flags := c.Meta.FlagSet("secure-variables keyring rotate", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&rotateFull, "full", false, "full key rotation")
flags.BoolVar(&verbose, "verbose", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 0 {
c.Ui.Error("This command requires no arguments.")
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
}
resp, _, err := client.Keyring().Rotate(
&api.KeyringRotateOptions{Full: rotateFull}, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
c.Ui.Output(renderSecureVariablesKeysResponse([]*api.RootKeyMeta{resp}, verbose))
return 0
}