New `safe local` command
This is awesome. You're going to love this. `safe local` will spin up a Vault, for you, initialize it, unseal it, target it, authenticate to it, and then create a secret/handshake for you. All at almost no cost to you! Closes #134
This commit is contained in:
parent
36f3b24e0e
commit
f8d6b0d5be
|
@ -1,5 +1,12 @@
|
||||||
# New Features
|
# New Features
|
||||||
|
|
||||||
|
- `safe local` will spin a Vault server for you, initialize and
|
||||||
|
unseal it, and target it seamlessly. You can opt for transient
|
||||||
|
local vaults via `safe local --memory` or more durable vaults
|
||||||
|
via `safe local --file path/to/store`. You can name your local
|
||||||
|
vaults, but `safe` took a creative writing course and it itching
|
||||||
|
to use its newfound list of adjectives and nouns!
|
||||||
|
|
||||||
- `safe target` and `safe targets` now support a `--json` flag,
|
- `safe target` and `safe targets` now support a `--json` flag,
|
||||||
for getting target information in a script-parseable format.
|
for getting target information in a script-parseable format.
|
||||||
|
|
||||||
|
|
166
main.go
166
main.go
|
@ -6,14 +6,17 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
fmt "github.com/jhunt/go-ansi"
|
fmt "github.com/jhunt/go-ansi"
|
||||||
|
@ -82,6 +85,12 @@ type Options struct {
|
||||||
Paste struct{} `cli:"paste"`
|
Paste struct{} `cli:"paste"`
|
||||||
Exists struct{} `cli:"exists, check"`
|
Exists struct{} `cli:"exists, check"`
|
||||||
|
|
||||||
|
Local struct {
|
||||||
|
As string `cli:"--as"`
|
||||||
|
File string `cli:"-f, --file"`
|
||||||
|
Memory bool `cli:"-m, --memory"`
|
||||||
|
} `cli:"local"`
|
||||||
|
|
||||||
Init struct {
|
Init struct {
|
||||||
Single bool `cli:"-s, --single"`
|
Single bool `cli:"-s, --single"`
|
||||||
NKeys int `cli:"--keys"`
|
NKeys int `cli:"--keys"`
|
||||||
|
@ -436,6 +445,163 @@ func main() {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Dispatch("local", &Help{
|
||||||
|
Summary: "Run a local vault",
|
||||||
|
Usage: "safe local (--memory|--file path/to/dir) [--as name]",
|
||||||
|
Description: `
|
||||||
|
Spins up a new Vault instance, on an unused port between 8201 and 9999
|
||||||
|
(inclusive). The new Vault will be initialized with a single seal key,
|
||||||
|
targeted with a catchy name, authenticated by the new root token, and
|
||||||
|
populated with a secret/handshake!
|
||||||
|
|
||||||
|
If you just need a transient Vault for testing or experimentation, and
|
||||||
|
don't particularly care about the contents of the Vault, specify the
|
||||||
|
--memory/-m flag and get an in-memory backend.
|
||||||
|
|
||||||
|
If, on the other hand, you want to keep the Vault around, possibly
|
||||||
|
spinning it down when not in use, specify the --file/-f flag, and give it
|
||||||
|
the path to a directory to use for the file backend. The files created
|
||||||
|
by the mechanism will be encrypted. You will be given the seal key for
|
||||||
|
subsequent activations of the Vault.
|
||||||
|
`,
|
||||||
|
Type: AdministrativeCommand,
|
||||||
|
}, func(command string, args ...string) error {
|
||||||
|
if !opt.Local.Memory && opt.Local.File == "" {
|
||||||
|
return fmt.Errorf("Please specify either --memory or --file <path>")
|
||||||
|
}
|
||||||
|
if opt.Local.Memory && opt.Local.File != "" {
|
||||||
|
return fmt.Errorf("Please specify either --memory or --file <path>, but not both")
|
||||||
|
}
|
||||||
|
|
||||||
|
var port int
|
||||||
|
for port = 8201; port < 9999; port++ {
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := ioutil.TempFile("", "kazoo")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(f, `# safe local config
|
||||||
|
disable_mlock = 1
|
||||||
|
listener "tcp" {
|
||||||
|
address = "127.0.0.1:%d"
|
||||||
|
tls_disable = 1
|
||||||
|
}
|
||||||
|
`, port)
|
||||||
|
|
||||||
|
keys := make([]string, 0)
|
||||||
|
if opt.Local.Memory {
|
||||||
|
fmt.Fprintf(f, "storage \"inmem\" {}\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f, "storage \"file\" { path = \"%s\" }\n", opt.Local.File)
|
||||||
|
if _, err := os.Stat(opt.Local.File); err == nil || !os.IsNotExist(err) {
|
||||||
|
keys = append(keys, pr("Unseal Key", false, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echan := make(chan error)
|
||||||
|
cmd := exec.Command("vault", "server", "-config", f.Name())
|
||||||
|
cmd.Start()
|
||||||
|
go func() {
|
||||||
|
echan <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
signal.Ignore(syscall.SIGINT)
|
||||||
|
|
||||||
|
die := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "@R{!! %s}\n", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "@Y{shutting down the Vault...}\n")
|
||||||
|
if err := cmd.Process.Kill(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "@R{NOTE: Unable to terminate the Vault process.}\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "@R{ You may have some environmental cleanup to do.}\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "@R{ Apologies.}\n")
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := rc.Apply("")
|
||||||
|
name := opt.Local.As
|
||||||
|
if name == "" {
|
||||||
|
name = RandomName()
|
||||||
|
var n int
|
||||||
|
for n = 15; n > 0; n-- {
|
||||||
|
if existing, _ := cfg.Vault(name); existing == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name = RandomName()
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
die(fmt.Errorf("I was unable to come up with a cool name for your local Vault. Please try naming it with --as"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if existing, _ := cfg.Vault(name); existing != nil {
|
||||||
|
die(fmt.Errorf("You already have '%s' as a Vault target", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous := cfg.Current
|
||||||
|
|
||||||
|
cfg.SetTarget(name, fmt.Sprintf("http://127.0.0.1:%d", port), false)
|
||||||
|
cfg.Write()
|
||||||
|
|
||||||
|
rc.Apply("")
|
||||||
|
v := connect(false)
|
||||||
|
token := ""
|
||||||
|
if len(keys) == 0 {
|
||||||
|
keys, _, err = v.Init(1, 1)
|
||||||
|
if err != nil {
|
||||||
|
die(fmt.Errorf("Unable to initialize the new (temporary) Vault: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = v.Unseal(keys); err != nil {
|
||||||
|
die(fmt.Errorf("Unable to unseal the new (temporary) Vault: %s", err))
|
||||||
|
}
|
||||||
|
token, err = v.NewRootToken(keys)
|
||||||
|
if err != nil {
|
||||||
|
die(fmt.Errorf("Unable to generate a new root token: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.SetToken(token)
|
||||||
|
os.Setenv("VAULT_TOKEN", token)
|
||||||
|
cfg.Write()
|
||||||
|
v = connect(true)
|
||||||
|
|
||||||
|
s := vault.NewSecret()
|
||||||
|
s.Set("knock", "knock", false)
|
||||||
|
v.Write("secret/handshake", s)
|
||||||
|
|
||||||
|
if !opt.Quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "Now targeting (temporary) @Y{%s} at @C{%s}\n", cfg.Current, cfg.URL())
|
||||||
|
if opt.Local.Memory {
|
||||||
|
fmt.Fprintf(os.Stderr, "@R{This Vault is MEMORY-BACKED!}\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "If you want to @Y{retain your secrets} be sure to @C{safe export}.\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Storing data (encrypted) in @G{%s}\n", opt.Local.File)
|
||||||
|
fmt.Fprintf(os.Stderr, "Your Vault Seal Key is @M{%s}\n", keys[0])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Ctrl-C to shut down the Vault\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = <-echan
|
||||||
|
fmt.Fprintf(os.Stderr, "Vault terminated normally, cleaning up...\n")
|
||||||
|
cfg = rc.Apply("")
|
||||||
|
if cfg.Current == name {
|
||||||
|
if _, found, _ := cfg.Find(previous); found {
|
||||||
|
cfg.Current = previous
|
||||||
|
}
|
||||||
|
cfg.Current = ""
|
||||||
|
}
|
||||||
|
delete(cfg.Vaults, name)
|
||||||
|
cfg.Write()
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
r.Dispatch("init", &Help{
|
r.Dispatch("init", &Help{
|
||||||
Summary: "Initialize a new vault",
|
Summary: "Initialize a new vault",
|
||||||
Usage: "safe init [--keys #] [--threshold #] [--single] [--json] [--sealed]",
|
Usage: "safe init [--keys #] [--threshold #] [--single] [--json] [--sealed]",
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
// not crypto, not significant
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Adjectives []string
|
||||||
|
var Nouns []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Adjectives = []string{
|
||||||
|
"hardened",
|
||||||
|
"toughened",
|
||||||
|
"annealed",
|
||||||
|
"tempered",
|
||||||
|
"fortified",
|
||||||
|
"bastioned",
|
||||||
|
"bolstered",
|
||||||
|
"reinforced",
|
||||||
|
"inviolable",
|
||||||
|
"impregnable",
|
||||||
|
"unassailable",
|
||||||
|
"impervious",
|
||||||
|
"unbreakable",
|
||||||
|
"infrangible",
|
||||||
|
"stalwart",
|
||||||
|
"sturdy",
|
||||||
|
"stouthearted",
|
||||||
|
}
|
||||||
|
|
||||||
|
Nouns = []string{
|
||||||
|
"garrison",
|
||||||
|
"fortress",
|
||||||
|
"castle",
|
||||||
|
"keep",
|
||||||
|
"outpost",
|
||||||
|
"coffer",
|
||||||
|
"zone",
|
||||||
|
"sanctuary",
|
||||||
|
"refuge",
|
||||||
|
"asylum",
|
||||||
|
"hold",
|
||||||
|
"oubliette",
|
||||||
|
"donjon",
|
||||||
|
"dungeon",
|
||||||
|
"gaol",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomName() string {
|
||||||
|
return fmt.Sprintf("%s-%s",
|
||||||
|
Adjectives[rand.Intn(len(Adjectives))],
|
||||||
|
Nouns[rand.Intn(len(Nouns))])
|
||||||
|
}
|
|
@ -114,6 +114,10 @@ func (c *Config) Write() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if v == nil {
|
||||||
|
os.Remove(svtoken())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
sv := struct {
|
sv := struct {
|
||||||
Vault string `yaml:"vault"` /* this is different than Vault.URL */
|
Vault string `yaml:"vault"` /* this is different than Vault.URL */
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *Vault) NewRootToken(keys []string) (string, error) {
|
||||||
|
// cancel any previous generate-root attmempts (get a new nonce!)
|
||||||
|
req, err := http.NewRequest("DELETE", v.url("/v1/sys/generate-root/attempt"), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err := v.request(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 204 {
|
||||||
|
return "", fmt.Errorf("failed to cancel previous generate-root attempt: HTTP %d response", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a 16-byte one-time password, base64-encoded
|
||||||
|
otp := make([]byte, 16)
|
||||||
|
otp64 := make([]byte, 24) // does this need pre-alloc'd?
|
||||||
|
_, err = rand.Read(otp)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to generate a one-time password: %s", err)
|
||||||
|
}
|
||||||
|
base64.StdEncoding.Encode(otp64, otp)
|
||||||
|
|
||||||
|
// initiate a new generate-root attempt, with our one-time password in play
|
||||||
|
req, err = http.NewRequest("PUT", v.url("/v1/sys/generate-root/attempt"), strings.NewReader(`{"otp":"`+string(otp64)+`"}`))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err = v.request(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("failed to start a new generate-root attempt: HTTP %d response", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the nonce for this generate-root attempt go-round
|
||||||
|
var attempt struct {
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &attempt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := ""
|
||||||
|
for _, k := range keys {
|
||||||
|
// for each key, pass back the nonce, provide the key, and go!
|
||||||
|
payload := `{"key":"` + k + `","nonce":"` + attempt.Nonce + `"}`
|
||||||
|
req, err := http.NewRequest("PUT", v.url("/v1/sys/generate-root/update"), strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err := v.request(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("failed to provide seal key to Vault: HTTP %d response", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the response and save the encoded (token^otp) token
|
||||||
|
var out struct {
|
||||||
|
EncodedToken string `json:"encoded_token"`
|
||||||
|
Complete bool `json:"complete"`
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &out)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if out.Complete {
|
||||||
|
encoded = out.EncodedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded == "" {
|
||||||
|
return "", fmt.Errorf("failed to generate new root token")
|
||||||
|
}
|
||||||
|
|
||||||
|
tok64 := []byte(encoded)
|
||||||
|
tok := make([]byte, base64.StdEncoding.DecodedLen(len(tok64)))
|
||||||
|
if len(tok64) != len(otp64) {
|
||||||
|
return "", fmt.Errorf("failed to decode new root token")
|
||||||
|
}
|
||||||
|
|
||||||
|
base64.StdEncoding.Decode(tok, tok64)
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
tok[i] ^= otp[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||||
|
tok[0], tok[1], tok[2], tok[3],
|
||||||
|
tok[4], tok[5],
|
||||||
|
tok[6], tok[7],
|
||||||
|
tok[8], tok[9],
|
||||||
|
tok[10], tok[11], tok[12], tok[13], tok[14], tok[15]), nil
|
||||||
|
}
|
Loading…
Reference in New Issue