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:
James Hunt 2018-04-03 16:42:10 -04:00
parent 36f3b24e0e
commit f8d6b0d5be
5 changed files with 355 additions and 0 deletions

View File

@ -1,5 +1,12 @@
# 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,
for getting target information in a script-parseable format.

166
main.go
View File

@ -6,14 +6,17 @@ import (
"encoding/json"
"errors"
"io/ioutil"
"net"
"net/http/httputil"
"os"
"os/exec"
"os/signal"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"syscall"
"time"
fmt "github.com/jhunt/go-ansi"
@ -82,6 +85,12 @@ type Options struct {
Paste struct{} `cli:"paste"`
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 {
Single bool `cli:"-s, --single"`
NKeys int `cli:"--keys"`
@ -436,6 +445,163 @@ func main() {
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{
Summary: "Initialize a new vault",
Usage: "safe init [--keys #] [--threshold #] [--single] [--json] [--sealed]",

61
names.go Normal file
View File

@ -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))])
}

View File

@ -114,6 +114,10 @@ func (c *Config) Write() error {
if err != nil {
return err
}
if v == nil {
os.Remove(svtoken())
return nil
}
sv := struct {
Vault string `yaml:"vault"` /* this is different than Vault.URL */

117
vault/root.go Normal file
View File

@ -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
}