- 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:
parent
7d4c6add13
commit
83a8642f0a
|
@ -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
|
||||
|
|
2
main.go
2
main.go
|
@ -26,7 +26,7 @@ import (
|
|||
|
||||
const (
|
||||
Prog = "vault-sidekick"
|
||||
Version = "v0.0.4"
|
||||
Version = "v0.0.5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
33
utils.go
33
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
|
||||
}
|
||||
|
||||
|
|
29
vault.go
29
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
Reference in a new issue