diff --git a/Makefile b/Makefile index 9f2d5f6..11993ef 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ token: ## copies vault token in clipboard buffer .PHONY: clean clean: ## clean the development vault - @rm -rf cmd/vops.yml cmd/cluster-1* snapshots/ coverage.out dist/ $(projectname) manpages/ dist/ completions/ assets/raft/* || true + @rm -rf cluster-1.* cmd/vops.yml cmd/cluster-1* snapshots/ coverage.out dist/ $(projectname) manpages/ dist/ completions/ assets/raft/* || true @kill -9 $(shell pgrep -x vault) 2> /dev/null || true .PHONY: vhs diff --git a/README.md b/README.md index 2da4ab9..23fc052 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ vops generate-root -c vops rekey -c # save/restory snapshots vops snapshot save -c -vops snapshot restore -c +vops snapshot restore -c -s -f # custom commands vops custom -c -x # adhoc commands @@ -92,10 +92,6 @@ CustomCmds: drawing - -***`vops` is in early stage and is likely to change*** - - # Background I automate, develop and maintain a lot of Vault cluster for different clients. When automating Vault using tools such as `terraform` and `ansible` I was missing a small utility that allows me to quickly perform certain operations like generate a new root token or create a snapshot. Thus I came up with `vops`, which stands for **v**ault-**op**eration**s** @@ -258,7 +254,25 @@ cluster "cluster-1" sealed ``` ## Rekey -tbd. +> generates new unseal/recover keys +```bash +> vops rekey -c +[ Rekey ] +reading ./assets/vops.yaml + +[ cluster-1 ] +performing a rekey for cluster-1 with 5 shares and a threshold of 3 +applying VAULT_SKIP_VERIFY +using keyfile "cluster-1.json" +initialized rekey process +[01/03] successfully entered key +[02/03] successfully entered key +[03/03] successfully entered key +rekeying successfully completed +renamed keyfile "cluster-1.json" for cluster "cluster-1" to "cluster-1_2023-03-10-16:05:16.json". +Hint: snapshots depend on the unseal/recovery keys from the moment the snapshot has been created. +This way you always have the matching unseal/recovery keys for the specific snapshot if needed ready. +``` ## Generate Root @@ -278,19 +292,32 @@ new root token: "hvs.dmhO9aVPT0aBB1G7nrj3UdDh" (make sure to update your token e ## Snapshots ### Snapshot save +> creates a snapshot and stores the corresponding keyfile with it (only integrated storage) ```bash > vops snapshot save -c -[ Snapshot Save ] -using vops.yaml +[ Save ] +reading ./assets/vops.yaml [ cluster-1 ] -applying VAULT_TLS_SKIP_VERIFY +applying VAULT_SKIP_VERIFY executed token exec command -created snapshot file "cluster-1/20230216155514" for cluster "cluster-1" +created snapshot file "snapshots/cluster-1_2023-03-10-16:03:38.gz" for cluster "cluster-1" +created snapshot keyfile "snapshots/cluster-1_2023-03-10-16:03:38_keyfile.json" for cluster "cluster-1" ``` ### Snapshot Restore -tbd. +> restores a snapshot (only integrated storage) +```bash +> vos snapshot restore -c -s +[ Restore ] +reading ./assets/vops.yaml + +[ cluster-1 ] +applying VAULT_SKIP_VERIFY +executed token exec command +restrored snapshot for cluster-1 +Remember to use the root token und unseal/recovery keys from the snapshot you just restored +``` ## Custom Commands You can run any defined custom commands: diff --git a/cmd/generateroot.go b/cmd/generateroot.go index 8202827..12a881e 100644 --- a/cmd/generateroot.go +++ b/cmd/generateroot.go @@ -57,7 +57,7 @@ func generateRoot(cluster config.Cluster) error { return err } - keys, err := cluster.GetKeyFile() + keyFile, err := cluster.GetKeyFile() if err != nil { return err } @@ -81,7 +81,13 @@ func generateRoot(cluster config.Cluster) error { fmt.Println("started root token generation process") - for _, key := range keys.Keys { + keys := keyFile.Keys + + if cluster.Keys.Autounseal { + keys = keyFile.RecoveryKeys + } + + for _, key := range keys { resp, err := v.GenerateRootUpdate(key, regenRoot.Nonce) if err != nil { return err diff --git a/cmd/rekey.go b/cmd/rekey.go index 0ab9160..d5ea190 100644 --- a/cmd/rekey.go +++ b/cmd/rekey.go @@ -2,6 +2,8 @@ package cmd import ( "fmt" + "path/filepath" + "strings" "github.com/FalcoSuessgott/vops/pkg/config" "github.com/FalcoSuessgott/vops/pkg/fs" @@ -45,19 +47,16 @@ func rekeyCmd() *cobra.Command { return cmd } +// nolint: cyclop func rekeyCluster(cluster config.Cluster) error { fmt.Printf("\n[ %s ]\n", cluster.Name) fmt.Printf("performing a rekey for %s with %d shares and a threshold of %d\n", cluster.Name, cluster.Keys.Shares, cluster.Keys.Threshold) - if cluster.Keys.Path == "" { - return fmt.Errorf("a key file containing unseal/recovery keys for that cluster is required") - } - if err := cluster.ApplyEnvironmentVariables(cluster.ExtraEnv); err != nil { return err } - keys, err := cluster.GetKeyFile() + keyFile, err := cluster.GetKeyFile() if err != nil { return err } @@ -74,14 +73,24 @@ func rekeyCluster(cluster config.Cluster) error { return err } + fmt.Println("initialized rekey process") + var newKeys *api.RekeyUpdateResponse - for _, key := range keys.Keys { - resp, err := v.RekeyUpdate(key, rekeyInit.Nonce) + keys := keyFile.Keys + + if cluster.Keys.Autounseal { + keys = keyFile.RecoveryKeys + } + + for i, key := range keys { + resp, err := v.RekeyUpdate(key, rekeyInit.Nonce, cluster.Keys.Autounseal) if err != nil { return err } + fmt.Printf("[%02d/%02d] successfully entered key\n", i+1, cluster.Keys.Threshold) + if resp.Complete { fmt.Println("rekeying successfully completed") @@ -91,18 +100,31 @@ func rekeyCluster(cluster config.Cluster) error { } } - newName := fmt.Sprintf("%s_%s", cluster.Keys.Path, utils.GetCurrentDate()) + fileName := strings.TrimSuffix(cluster.Keys.Path, filepath.Ext(cluster.Keys.Path)) + newName := fmt.Sprintf("%s_%s%s", fileName, utils.GetCurrentDate(), filepath.Ext(cluster.Keys.Path)) fs.RenameFile(cluster.Keys.Path, newName) fmt.Printf( - "renamed keyfile \"%s\" for cluster \"%s\" to \"%s\""+ - "(snapshots depend on the unseal/recovery keys from the moment the snapshot has been created. "+ - "This way you always have the matching unseal/recovery keys ready.\n", + "renamed keyfile \"%s\" for cluster \"%s\" to \"%s\".\n"+ + "Hint: snapshots depend on the unseal/recovery keys from the moment the snapshot has been created.\n"+ + "This way you always have the matching unseal/recovery keys for the specific snapshot if needed ready.\n", cluster.Keys.Path, cluster.Name, newName, ) - if err := fs.WriteToFile(utils.ToJSON(newKeys), cluster.Keys.Path); err != nil { + newKeyfile := &api.InitResponse{ + RootToken: keyFile.RootToken, + } + + if cluster.Keys.Autounseal { + newKeyfile.RecoveryKeys = newKeys.Keys + newKeyfile.RecoveryKeysB64 = newKeys.KeysB64 + } else { + newKeyfile.Keys = newKeys.Keys + newKeyfile.KeysB64 = newKeys.KeysB64 + } + + if err := fs.WriteToFile(utils.ToJSON(newKeyfile), cluster.Keys.Path); err != nil { return err } diff --git a/cmd/seal.go b/cmd/seal.go index dbfec23..33df0ab 100644 --- a/cmd/seal.go +++ b/cmd/seal.go @@ -35,8 +35,6 @@ func sealCmd() *cobra.Command { return err } - fmt.Printf("cluster \"%s\" sealed\n", cluster.Name) - return nil }, } diff --git a/cmd/snapshot.go b/cmd/snapshot.go index 87d0872..c38ce22 100644 --- a/cmd/snapshot.go +++ b/cmd/snapshot.go @@ -1,8 +1,8 @@ package cmd import ( - "bytes" "fmt" + "os" "path" "github.com/FalcoSuessgott/vops/pkg/config" @@ -12,6 +12,11 @@ import ( "github.com/spf13/cobra" ) +var ( + snapshotFile string + force bool +) + func snapshotCmd() *cobra.Command { cmd := &cobra.Command{ Use: "snapshot", @@ -71,13 +76,7 @@ func snapRestoreCmd() *cobra.Command { SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { if allCluster { - for _, cluster := range cfg.Cluster { - if err := restoreSnapshot(cluster); err != nil { - return err - } - } - - return nil + return fmt.Errorf("cannot restore a snapshot for all cluster") } cluster, err := cfg.GetCluster(cluster) @@ -85,10 +84,13 @@ func snapRestoreCmd() *cobra.Command { return err } - return restoreSnapshot(*cluster) + return restoreSnapshot(*cluster, snapshotFile) }, } + cmd.Flags().StringVarP(&snapshotFile, "snapshot", "s", adhocCommand, "the cluster snapshot file") + cmd.Flags().BoolVarP(&force, "force", "f", force, "bypasses checks ensuring the Autounseal or shamir keys are consistent with the snapshot data.") + return cmd } @@ -121,7 +123,8 @@ func saveSnapshot(cluster config.Cluster) error { fs.CreateDirIfNotExist(cluster.SnapshotDir) - snapshotName := path.Join(cluster.SnapshotDir, utils.GetCurrentDate()) + timestamp := utils.GetCurrentDate() + snapshotName := path.Join(cluster.SnapshotDir, fmt.Sprintf("%s_%s.gz", cluster.Name, timestamp)) if fs.WriteToFile(w.Bytes(), snapshotName) != nil { return err @@ -129,10 +132,23 @@ func saveSnapshot(cluster config.Cluster) error { fmt.Printf("created snapshot file \"%s\" for cluster \"%s\"\n", snapshotName, cluster.Name) + keyFile, err := cluster.GetKeyFile() + if err != nil { + return err + } + + keyFileName := path.Join(cluster.SnapshotDir, fmt.Sprintf("%s_%s_keyfile.json", cluster.Name, timestamp)) + + if fs.WriteToFile(utils.ToJSON(keyFile), keyFileName) != nil { + return err + } + + fmt.Printf("created snapshot keyfile \"%s\" for cluster \"%s\"\n", keyFileName, cluster.Name) + return nil } -func restoreSnapshot(cluster config.Cluster) error { +func restoreSnapshot(cluster config.Cluster, snapshotFile string) error { fmt.Printf("\n[ %s ]\n", cluster.Name) if err := cluster.ApplyEnvironmentVariables(cluster.ExtraEnv); err != nil { @@ -150,13 +166,20 @@ func restoreSnapshot(cluster config.Cluster) error { return err } - var b bytes.Reader + reader, err := os.Open(snapshotFile) + if err != nil { + return err + } + + defer reader.Close() - if err := v.SnapshotRestore(&b, true); err != nil { + if err := v.SnapshotRestore(reader, force); err != nil { return err } - fmt.Printf("restrored snapshot for %s\n", cluster.Name) + fmt.Printf("restrored snapshot for %s\n"+ + "Remember to use the root token und unseal/recovery keys from the snapshot you just restored\n", + cluster.Name) return nil } diff --git a/cmd/vops.yml b/cmd/vops.yml deleted file mode 100644 index 15daeda..0000000 --- a/cmd/vops.yml +++ /dev/null @@ -1,17 +0,0 @@ -Cluster: - - Name: cluster-1 - Addr: http://127.0.0.1:8200 - TokenExecCmd: jq -r '.root_token' {{ .Keys.Path }} - Keys: - Path: '{{ .Name }}.json' - Shares: 1 - Threshold: 1 - SnapshotDirectory: '{{ .Name }}/' - Nodes: - - '{{ .Addr }}' - ExtraEnv: - VAULT_TLS_SKIP_VERIFY: true -CustomCmds: - list-peers: vault operator raft list-peers - status: vault status - diff --git a/pkg/config/config.go b/pkg/config/config.go index 2e7ec53..9a2eaa7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,7 +12,7 @@ import ( const ( clusterEnvVar = "VOPS_CLUSTER" defaultKeyShares = 5 - defaultKeyThreshold = 5 + defaultKeyThreshold = 3 ) var ( diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2913e11..4739e4a 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -79,7 +79,7 @@ func ToYAML(m interface{}) []byte { return b.Bytes() } -// GetCurrentDate returns the current date in YYYYDDMMHHss format. +// GetCurrentDate returns the current date in YYYY-DD-MM-HH:mm:ss format. func GetCurrentDate() string { - return time.Now().Format("20060102150405") + return time.Now().Format("2006-01-02-15:04:05") } diff --git a/pkg/vault/rekey.go b/pkg/vault/rekey.go index ba82d5e..67b7fff 100644 --- a/pkg/vault/rekey.go +++ b/pkg/vault/rekey.go @@ -4,7 +4,7 @@ import "github.com/hashicorp/vault/api" // RekeyInit inits a rekeying of a vault server. func (v *Vault) RekeyInit(shares, threshold int, recoveryKeys bool) (*api.RekeyStatusResponse, error) { - var err error + var fn func(config *api.RekeyInitRequest) (*api.RekeyStatusResponse, error) opts := &api.RekeyInitRequest{ SecretShares: shares, @@ -12,15 +12,12 @@ func (v *Vault) RekeyInit(shares, threshold int, recoveryKeys bool) (*api.RekeyS } if recoveryKeys { - resp, err := v.Client.Sys().RekeyRecoveryKeyInit(opts) - if err != nil { - return nil, err - } - - return resp, nil + fn = v.Client.Sys().RekeyRecoveryKeyInit + } else { + fn = v.Client.Sys().RekeyInit } - resp, err := v.Client.Sys().RekeyInit(opts) + resp, err := fn(opts) if err != nil { return nil, err } @@ -29,8 +26,16 @@ func (v *Vault) RekeyInit(shares, threshold int, recoveryKeys bool) (*api.RekeyS } // RekeyUpdate rekeys a vault server. -func (v *Vault) RekeyUpdate(key, nonce string) (*api.RekeyUpdateResponse, error) { - resp, err := v.Client.Sys().RekeyUpdate(key, nonce) +func (v *Vault) RekeyUpdate(key, nonce string, recoveryKeys bool) (*api.RekeyUpdateResponse, error) { + var fn func(shard, nonce string) (*api.RekeyUpdateResponse, error) + + if recoveryKeys { + fn = v.Client.Sys().RekeyRecoveryKeyUpdate + } else { + fn = v.Client.Sys().RekeyUpdate + } + + resp, err := fn(key, nonce) if err != nil { return nil, err }