diff --git a/README.md b/README.md index b004043..ef5a648 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ bundle format is very similar in the sense it similar takes the private key and **Resource Options** - **file**: (filaname) by default all file are relative to the output directory specified and will have the name NAME.RESOURCE; the fn options allows you to switch names and paths to write the files +- **create**: (create) create the resource - **update**: (update) override the lease time of this resource and get/renew a secret on the specified duration e.g 1m, 2d, 5m10s - **renew**: (renewal) override the default behavour on this resource, renew the resource when coming close to expiration e.g true, TRUE - **delay**: (renewal-delay) delay the revoking the lease of a resource for x period once time e.g 1m, 1h20s diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..b32de34 --- /dev/null +++ b/generate.go @@ -0,0 +1,53 @@ +/* +Copyright 2015 Home Office All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "crypto/rand" + +) + +var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~") + +func NewPassword(length int) string { + return rand_char(length, StdChars) +} + +func rand_char(length int, chars []byte) string { + new_pword := make([]byte, length) + random_data := make([]byte, length+(length/4)) // storage for random bytes. + clen := byte(len(chars)) + maxrb := byte(256 - (256 % len(chars))) + i := 0 + for { + if _, err := io.ReadFull(rand.Reader, random_data); err != nil { + panic(err) + } + for _, c := range random_data { + if c >= maxrb { + continue + } + new_pword[i] = chars[c%clen] + i++ + if i == length { + return string(new_pword) + } + } + } + panic("unreachable") +} diff --git a/main.go b/main.go index cfd5bc6..2d13fc4 100644 --- a/main.go +++ b/main.go @@ -26,11 +26,12 @@ import ( const ( Prog = "vault-sidekick" - Version = "v0.0.8" + Version = "v0.0.9" ) func main() { // step: parse and validate the command line / environment options + if err := parseOptions(); err != nil { showUsage("invalid options, %s", err) } diff --git a/vault.go b/vault.go index 4d0457a..7f04726 100644 --- a/vault.go +++ b/vault.go @@ -138,7 +138,6 @@ func (r *VaultService) vaultServiceProcessor() { glog.V(10).Infof("resource: %s has a previous lease: %s", x.resource, leaseID) } - // step: retrieve the resource from vault err := r.get(x) if err != nil { glog.Errorf("failed to retrieve the resource: %s from vault, error: %s", x.resource, err) @@ -147,7 +146,7 @@ func (r *VaultService) vaultServiceProcessor() { break } - glog.V(4).Infof("successfully retrieved resournce: %s, leaseID: %s", x.resource, x.secret.LeaseID) + glog.V(4).Infof("successfully retrieved resource: %s, leaseID: %s", x.resource, x.secret.LeaseID) // step: if we had a previous lease and the option is to revoke, lets throw into the revoke channel if leaseID != "" && x.resource.revoked { @@ -315,6 +314,7 @@ func (r VaultService) revoke(lease string) error { func (r VaultService) get(rn *watchedResource) (err error) { var secret *api.Secret // step: not sure who to cast map[string]string to map[string]interface{} doesn't like it anyway i try and do it + params := make(map[string]interface{}, 0) for k, v := range rn.resource.options { params[k] = interface{}(v) @@ -366,6 +366,17 @@ func (r VaultService) get(rn *watchedResource) (err error) { fallthrough case "secret": secret, err = r.client.Logical().Read(rn.resource.path) + // We must generate the secret if we have the create flag + if rn.resource.create && secret == nil && err == nil { + glog.V(3).Infof("Create param specified, creating resource: %s", rn.resource.path) + params["value"] = NewPassword(int(rn.resource.size)) + secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), params) + glog.V(3).Infof("Secret created: %s", rn.resource.path) + if err == nil { + // Populate the secret data as stored in Vault... + secret, err = r.client.Logical().Read(rn.resource.path) + } + } } // step: check the error if any if err != nil { @@ -375,7 +386,7 @@ func (r VaultService) get(rn *watchedResource) (err error) { } return err } - if secret == nil && err != nil { + if secret == nil && err == nil { return fmt.Errorf("the resource does not exist") } diff --git a/vault_resource.go b/vault_resource.go index e22aeb2..f09e648 100644 --- a/vault_resource.go +++ b/vault_resource.go @@ -39,6 +39,12 @@ const ( optionUpdate = "update" // optionsExec executes something on a change optionExec = "exec" + // optionCreate creates a secret if it doesn't exist + optionCreate = "create" + // optionSize sets the initial size of a password secret + optionSize = "size" + // defaultSize sets the default size of a generic secret + defaultSize = 20 ) var ( @@ -65,6 +71,7 @@ func defaultVaultResource() *VaultResource { renewable: false, revoked: false, options: make(map[string]string, 0), + size: defaultSize, } } @@ -84,6 +91,10 @@ type VaultResource struct { revokeDelay time.Duration // the lease duration update time.Duration + // whether the resource should be created? + create bool + // the size of a secret to create + size int64 // the filename to save the secret filename string // the template file diff --git a/vault_resources.go b/vault_resources.go index 4f0f5a1..5ee96df 100644 --- a/vault_resources.go +++ b/vault_resources.go @@ -65,7 +65,7 @@ func (r *VaultResources) Set(value string) error { name := kp[0] value := strings.Replace(kp[1], "|", ",", -1) - // step: extract the control options from the path resource parameteres + // step: extract the control options from the path resource parameters switch name { case optionFormat: if matched := resourceFormatRegex.MatchString(value); !matched { @@ -96,6 +96,21 @@ func (r *VaultResources) Set(value string) error { return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", value) } rn.renewable = choice + case optionCreate: + choice, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("the create option: %s is invalid, should be a boolean", value) + } + if rn.resource != "secret" { + return fmt.Errorf("the create option is only supported for 'cn=secret' at this time") + } + rn.create = choice + case optionSize: + size, err := strconv.ParseInt(value, 10, 16) + if err != nil { + return fmt.Errorf("the size option: %s is invalid, should be an integer", value) + } + rn.size = size case optionExec: rn.execPath = value case optionFilename: