diff --git a/auth_token.go b/auth_token.go index e97c9bd..42ac9ae 100644 --- a/auth_token.go +++ b/auth_token.go @@ -40,7 +40,6 @@ func NewUserTokenPlugin(client *api.Client) AuthInterface { func (r authTokenPlugin) Create(cfg map[string]string) (string, error) { filename, _ := cfg["filename"] if filename != "" { - content, err := readConfigFile(filename) if err != nil { return "", err diff --git a/main.go b/main.go index 5766952..a3467eb 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ import ( const ( Prog = "vault-sidekick" - Version = "v0.0.4" + Version = "v0.0.5" ) func main() { diff --git a/utils.go b/utils.go index ae594c1..1cfd2ba 100644 --- a/utils.go +++ b/utils.go @@ -127,7 +127,8 @@ func readYAMLFile(filename string) (map[string]string, error) { // min : the smallest number we can accept // max : the largest number we can accept func getDurationWithin(min, max int) time.Duration { - return time.Duration(rand.Intn(max-min)+min) * time.Second + duration := rand.Intn(max-min) + min + return time.Duration(duration) * time.Second } // getEnv checks to see if an environment variable exists otherwise uses the default @@ -158,9 +159,6 @@ func fileExists(filename string) (bool, error) { // rn : a point to the vault resource // data : a map of the related secret associated to the resource func writeResource(rn *VaultResource, data map[string]interface{}) error { - var content []byte - var err error - // step: determine the resource path resourcePath := rn.GetFilename() if !strings.HasPrefix(resourcePath, "/") { @@ -171,7 +169,8 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { if rn.format == "yaml" { // marshall the content to yaml - if content, err = yaml.Marshal(data); err != nil { + content, err := yaml.Marshal(data) + if err != nil { return err } @@ -181,11 +180,10 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { if rn.format == "ini" { var buf bytes.Buffer for key, val := range data { - buf.WriteString(fmt.Sprintf("%s = %s\n", key, val)) + buf.WriteString(fmt.Sprintf("%s = %v\n", key, val)) } - content = buf.Bytes() - return writeFile(resourcePath, content) + return writeFile(resourcePath, buf.Bytes()) } if rn.format == "bundle" { @@ -205,6 +203,15 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { } } + if rn.format == "env" { + var buf bytes.Buffer + for key, val := range data { + buf.WriteString(fmt.Sprintf("%s=%v\n", strings.ToUpper(key), val)) + } + + return writeFile(resourcePath, buf.Bytes()) + } + if rn.format == "cert" { files := map[string]string{ "certificate": "crt", @@ -232,11 +239,10 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { if rn.format == "csv" { var buf bytes.Buffer for key, val := range data { - buf.WriteString(fmt.Sprintf("%s,%s\n", key, val)) + buf.WriteString(fmt.Sprintf("%s,%v\n", key, val)) } - content = buf.Bytes() - return writeFile(resourcePath, content) + return writeFile(resourcePath, buf.Bytes()) } if rn.format == "txt" { @@ -256,14 +262,15 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { // step: we only have the one key, so will write plain value, _ := data[keys[0]] - content = []byte(fmt.Sprintf("%s", value)) + content := []byte(fmt.Sprintf("%s", value)) return writeFile(resourcePath, content) } if rn.format == "json" { - if content, err = json.MarshalIndent(data, "", " "); err != nil { + content, err := json.MarshalIndent(data, "", " ") + if err != nil { return err } diff --git a/vault.go b/vault.go index c9fcc3a..8862610 100644 --- a/vault.go +++ b/vault.go @@ -27,6 +27,7 @@ import ( "github.com/golang/glog" "github.com/hashicorp/vault/api" + "strings" ) const ( @@ -314,22 +315,35 @@ func (r VaultService) revoke(lease string) error { func (r VaultService) get(rn *watchedResource) (err error) { var secret *api.Secret glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource) - + // step: perform a request to vault switch rn.resource.resource { case "pki": secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), map[string]interface{}{ "common_name": rn.resource.options[optionCommonName], }) + case "transit": + secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), + map[string]interface{}{ + "cipertext": rn.resource.options[optionCiphertext], + }) case "aws": - secret, err = r.client.Logical().Read(rn.resource.path) + fallthrough + case "cubbyhole": + fallthrough case "mysql": - secret, err = r.client.Logical().Read(rn.resource.path) + fallthrough + case "postgres": + fallthrough case "secret": secret, err = r.client.Logical().Read(rn.resource.path) } // step: return on error if err != nil { + if strings.Contains(err.Error(), "missing client token") { + // decision: until the rewrite, lets just exit for now + glog.Fatalf("the vault token is no longer valid, exitting, error: %s", err) + } return err } if secret == nil && err != nil { @@ -380,6 +394,9 @@ func newVaultClient(opts *config) (*api.Client, error) { default: return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin) } + if err != nil { + return nil, err + } // step: set the token for the client client.SetToken(token) @@ -393,19 +410,17 @@ func buildHTTPTransport(opts *config) (*http.Transport, error) { transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: 10 * time.Second, + KeepAlive: 10 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, } - // step: are we skip the tls verify? if options.tlsVerify { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } - // step: are we loading a CA file if opts.vaultCaFile != "" { // step: load the ca file diff --git a/vault_resource.go b/vault_resource.go index f228a3c..8ab860e 100644 --- a/vault_resource.go +++ b/vault_resource.go @@ -40,10 +40,12 @@ const ( optionsRevokeDelay = "delay" // optionUpdate overrides the lease of the resource optionUpdate = "update" + // optionCiphertext + optionCiphertext = "ciphertext" ) var ( - resourceFormatRegex = regexp.MustCompile("^(yaml|json|ini|txt|cert|bundle|csv)$") + resourceFormatRegex = regexp.MustCompile("^(yaml|json|env|ini|txt|cert|bundle|csv)$") // a map of valid resource to retrieve from vault validResources = map[string]bool{ @@ -53,6 +55,8 @@ var ( "mysql": true, "tpl": true, "postgres": true, + "transit": true, + "cubbyhole": true, "cassandra": true, } ) @@ -82,6 +86,8 @@ type VaultResource struct { revokeDelay time.Duration // the lease duration update time.Duration + // the cipertext for transit + ciphertext string // additional options to the resource options map[string]string } @@ -123,6 +129,10 @@ func (r *VaultResource) isValidResource() error { if _, found := r.options[optionCommonName]; !found { return fmt.Errorf("pki resource requires a common name specified") } + case "transit": + if _, found := r.options[optionCiphertext]; !found { + return fmt.Errorf("transit requires a ciphertext option") + } case "tpl": if _, found := r.options[optionTemplatePath]; !found { return fmt.Errorf("template resource requires a template path option") @@ -166,6 +176,8 @@ func (r *VaultResource) isValidOptions() error { return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", val) } r.renewable = choice + case optionCiphertext: + r.ciphertext = val case optionFilename: // @TODO need to check it's valid filename / path case optionCommonName: diff --git a/vault_resources.go b/vault_resources.go index 4f87e35..9f42399 100644 --- a/vault_resources.go +++ b/vault_resources.go @@ -18,15 +18,9 @@ package main import ( "fmt" - "regexp" "strings" ) -var ( - resourceRegex = regexp.MustCompile("^([\\w]+):([\\w\\\\/\\-_\\.]+):?(.*)") - resourceOptionsRegex = regexp.MustCompile("([\\w\\d]{2,6})=([\\w\\d\\/\\.\\-_]+)[,]?") -) - // VaultResources is a collection of type resource type VaultResources struct { // an array of resource to retrieve @@ -34,31 +28,39 @@ type VaultResources struct { } // Set is the implementation for the parser +// secret:test:file=filename.test,fmt=yaml func (r *VaultResources) Set(value string) error { rn := defaultVaultResource() - // step: extract the resource type and name - if matched := resourceRegex.MatchString(value); !matched { - return fmt.Errorf("invalid resource specification, should be TYPE:NAME:?(OPTION_NAME=VALUE,)") + // step: split on the ':' + items := strings.Split(value, ":") + if len(items) < 2 { + return fmt.Errorf("invalid resource, must have at least two sections TYPE:PATH") + } + if len(items) > 3 { + return fmt.Errorf("invalid resource, can only has three sections, TYPE:PATH[:OPTIONS]") + } + if items[0] == "" || items[1] == "" { + return fmt.Errorf("invalid resource, neither type or path can be empty") } - // step: extract the matches - matches := resourceRegex.FindAllStringSubmatch(value, -1) - rn.resource = matches[0][1] - rn.path = matches[0][2] + // step: extract the elements + rn.resource = items[0] + rn.path = items[1] rn.options = make(map[string]string, 0) - // step: do we have any options for the resource? - if len(matches[0]) == 4 { - opts := matches[0][3] - for len(opts) > 0 { - if matched := resourceOptionsRegex.MatchString(opts); !matched { - return fmt.Errorf("invalid resource options specified: '%s', please check usage", opts) + // step: extract any options + if len(items) > 2 { + for _, x := range strings.Split(items[2], ",") { + kp := strings.Split(x, "=") + if len(kp) != 2 { + return fmt.Errorf("invalid resource option: %s, must be KEY=VALUE", x) + } + if kp[1] == "" { + return fmt.Errorf("invalid resource option: %s, must have a value", x) } - matches := resourceOptionsRegex.FindAllStringSubmatch(opts, -1) - rn.options[matches[0][1]] = matches[0][2] - opts = strings.TrimPrefix(opts, matches[0][0]) + rn.options[kp[0]] = kp[1] } } // step: append to the list of resources diff --git a/vault_resources_test.go b/vault_resources_test.go index 0f3e16c..6398615 100644 --- a/vault_resources_test.go +++ b/vault_resources_test.go @@ -26,10 +26,10 @@ func TestSetResources(t *testing.T) { var items VaultResources assert.Nil(t, items.Set("secret:test:file=filename.test,fmt=yaml")) - assert.Nil(t, items.Set("secret:test:file=filename.test,")) + assert.Nil(t, items.Set("secret:test:file=filename.test")) assert.Nil(t, items.Set("secret:/db/prod/username")) assert.Nil(t, items.Set("secret:/db/prod:file=filename.test,fmt=yaml")) - assert.Nil(t, items.Set("secret:test:fn=filename.test,")) + assert.Nil(t, items.Set("secret:test:fn=filename.test")) assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com")) assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,file=/etc/certs/ssl/blah.example.com")) assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,renew=10s")) diff --git a/watched_resource.go b/watched_resource.go index e1a5232..c2c6e6d 100644 --- a/watched_resource.go +++ b/watched_resource.go @@ -19,6 +19,7 @@ package main import ( "time" + "fmt" "github.com/golang/glog" "github.com/hashicorp/vault/api" ) @@ -51,6 +52,7 @@ func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) { // step: if the answer is no, we set the notification between 80-95% of the lease time of the secret if r.renewalTime <= 0 { r.renewalTime = r.calculateRenewal() + fmt.Printf("seconds: %s", r.renewalTime) } glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime) // step: wait for the duration @@ -64,5 +66,5 @@ func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) { func (r watchedResource) calculateRenewal() time.Duration { return time.Duration(getDurationWithin( int(float64(r.secret.LeaseDuration)*renewalMinimum), - int(float64(r.secret.LeaseDuration)*renewalMaximum))) * time.Second + int(float64(r.secret.LeaseDuration)*renewalMaximum))) }