- fixing the durationWithIn method, which causes a invalid renewel time

- added a env format which will generate enviroment variables
- added the postgres, cubbyhole and transit backend
- fixed a number of bugs
- shifting to version v0.0.5
This commit is contained in:
Rohith 2016-03-12 18:49:57 +00:00
parent 7d4c6add13
commit 83a8642f0a
8 changed files with 85 additions and 48 deletions

View file

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

View file

@ -26,7 +26,7 @@ import (
const (
Prog = "vault-sidekick"
Version = "v0.0.4"
Version = "v0.0.5"
)
func main() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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"))

View file

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