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
|
||||
|
||||
- `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
166
main.go
|
@ -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]",
|
||||
|
|
|
@ -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 {
|
||||
return err
|
||||
}
|
||||
if v == nil {
|
||||
os.Remove(svtoken())
|
||||
return nil
|
||||
}
|
||||
|
||||
sv := struct {
|
||||
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