- 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) {
|
func (r authTokenPlugin) Create(cfg map[string]string) (string, error) {
|
||||||
filename, _ := cfg["filename"]
|
filename, _ := cfg["filename"]
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
|
|
||||||
content, err := readConfigFile(filename)
|
content, err := readConfigFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
2
main.go
2
main.go
|
@ -26,7 +26,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Prog = "vault-sidekick"
|
Prog = "vault-sidekick"
|
||||||
Version = "v0.0.4"
|
Version = "v0.0.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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
|
// min : the smallest number we can accept
|
||||||
// max : the largest number we can accept
|
// max : the largest number we can accept
|
||||||
func getDurationWithin(min, max int) time.Duration {
|
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
|
// 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
|
// rn : a point to the vault resource
|
||||||
// data : a map of the related secret associated to the resource
|
// data : a map of the related secret associated to the resource
|
||||||
func writeResource(rn *VaultResource, data map[string]interface{}) error {
|
func writeResource(rn *VaultResource, data map[string]interface{}) error {
|
||||||
var content []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// step: determine the resource path
|
// step: determine the resource path
|
||||||
resourcePath := rn.GetFilename()
|
resourcePath := rn.GetFilename()
|
||||||
if !strings.HasPrefix(resourcePath, "/") {
|
if !strings.HasPrefix(resourcePath, "/") {
|
||||||
|
@ -171,7 +169,8 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error {
|
||||||
|
|
||||||
if rn.format == "yaml" {
|
if rn.format == "yaml" {
|
||||||
// marshall the content to yaml
|
// marshall the content to yaml
|
||||||
if content, err = yaml.Marshal(data); err != nil {
|
content, err := yaml.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,11 +180,10 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error {
|
||||||
if rn.format == "ini" {
|
if rn.format == "ini" {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for key, val := range data {
|
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" {
|
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" {
|
if rn.format == "cert" {
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"certificate": "crt",
|
"certificate": "crt",
|
||||||
|
@ -232,11 +239,10 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error {
|
||||||
if rn.format == "csv" {
|
if rn.format == "csv" {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for key, val := range data {
|
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" {
|
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
|
// step: we only have the one key, so will write plain
|
||||||
value, _ := data[keys[0]]
|
value, _ := data[keys[0]]
|
||||||
content = []byte(fmt.Sprintf("%s", value))
|
content := []byte(fmt.Sprintf("%s", value))
|
||||||
|
|
||||||
return writeFile(resourcePath, content)
|
return writeFile(resourcePath, content)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rn.format == "json" {
|
if rn.format == "json" {
|
||||||
if content, err = json.MarshalIndent(data, "", " "); err != nil {
|
content, err := json.MarshalIndent(data, "", " ")
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
vault.go
29
vault.go
|
@ -27,6 +27,7 @@ import (
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -314,22 +315,35 @@ func (r VaultService) revoke(lease string) error {
|
||||||
func (r VaultService) get(rn *watchedResource) (err error) {
|
func (r VaultService) get(rn *watchedResource) (err error) {
|
||||||
var secret *api.Secret
|
var secret *api.Secret
|
||||||
glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource)
|
glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource)
|
||||||
|
// step: perform a request to vault
|
||||||
switch rn.resource.resource {
|
switch rn.resource.resource {
|
||||||
case "pki":
|
case "pki":
|
||||||
secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path),
|
secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path),
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"common_name": rn.resource.options[optionCommonName],
|
"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":
|
case "aws":
|
||||||
secret, err = r.client.Logical().Read(rn.resource.path)
|
fallthrough
|
||||||
|
case "cubbyhole":
|
||||||
|
fallthrough
|
||||||
case "mysql":
|
case "mysql":
|
||||||
secret, err = r.client.Logical().Read(rn.resource.path)
|
fallthrough
|
||||||
|
case "postgres":
|
||||||
|
fallthrough
|
||||||
case "secret":
|
case "secret":
|
||||||
secret, err = r.client.Logical().Read(rn.resource.path)
|
secret, err = r.client.Logical().Read(rn.resource.path)
|
||||||
}
|
}
|
||||||
// step: return on error
|
// step: return on error
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
if secret == nil && err != nil {
|
if secret == nil && err != nil {
|
||||||
|
@ -380,6 +394,9 @@ func newVaultClient(opts *config) (*api.Client, error) {
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin)
|
return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// step: set the token for the client
|
// step: set the token for the client
|
||||||
client.SetToken(token)
|
client.SetToken(token)
|
||||||
|
@ -393,19 +410,17 @@ func buildHTTPTransport(opts *config) (*http.Transport, error) {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
Dial: (&net.Dialer{
|
Dial: (&net.Dialer{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 10 * time.Second,
|
||||||
}).Dial,
|
}).Dial,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// step: are we skip the tls verify?
|
// step: are we skip the tls verify?
|
||||||
if options.tlsVerify {
|
if options.tlsVerify {
|
||||||
transport.TLSClientConfig = &tls.Config{
|
transport.TLSClientConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// step: are we loading a CA file
|
// step: are we loading a CA file
|
||||||
if opts.vaultCaFile != "" {
|
if opts.vaultCaFile != "" {
|
||||||
// step: load the ca file
|
// step: load the ca file
|
||||||
|
|
|
@ -40,10 +40,12 @@ const (
|
||||||
optionsRevokeDelay = "delay"
|
optionsRevokeDelay = "delay"
|
||||||
// optionUpdate overrides the lease of the resource
|
// optionUpdate overrides the lease of the resource
|
||||||
optionUpdate = "update"
|
optionUpdate = "update"
|
||||||
|
// optionCiphertext
|
||||||
|
optionCiphertext = "ciphertext"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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
|
// a map of valid resource to retrieve from vault
|
||||||
validResources = map[string]bool{
|
validResources = map[string]bool{
|
||||||
|
@ -53,6 +55,8 @@ var (
|
||||||
"mysql": true,
|
"mysql": true,
|
||||||
"tpl": true,
|
"tpl": true,
|
||||||
"postgres": true,
|
"postgres": true,
|
||||||
|
"transit": true,
|
||||||
|
"cubbyhole": true,
|
||||||
"cassandra": true,
|
"cassandra": true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -82,6 +86,8 @@ type VaultResource struct {
|
||||||
revokeDelay time.Duration
|
revokeDelay time.Duration
|
||||||
// the lease duration
|
// the lease duration
|
||||||
update time.Duration
|
update time.Duration
|
||||||
|
// the cipertext for transit
|
||||||
|
ciphertext string
|
||||||
// additional options to the resource
|
// additional options to the resource
|
||||||
options map[string]string
|
options map[string]string
|
||||||
}
|
}
|
||||||
|
@ -123,6 +129,10 @@ func (r *VaultResource) isValidResource() error {
|
||||||
if _, found := r.options[optionCommonName]; !found {
|
if _, found := r.options[optionCommonName]; !found {
|
||||||
return fmt.Errorf("pki resource requires a common name specified")
|
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":
|
case "tpl":
|
||||||
if _, found := r.options[optionTemplatePath]; !found {
|
if _, found := r.options[optionTemplatePath]; !found {
|
||||||
return fmt.Errorf("template resource requires a template path option")
|
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)
|
return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", val)
|
||||||
}
|
}
|
||||||
r.renewable = choice
|
r.renewable = choice
|
||||||
|
case optionCiphertext:
|
||||||
|
r.ciphertext = val
|
||||||
case optionFilename:
|
case optionFilename:
|
||||||
// @TODO need to check it's valid filename / path
|
// @TODO need to check it's valid filename / path
|
||||||
case optionCommonName:
|
case optionCommonName:
|
||||||
|
|
|
@ -18,15 +18,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
resourceRegex = regexp.MustCompile("^([\\w]+):([\\w\\\\/\\-_\\.]+):?(.*)")
|
|
||||||
resourceOptionsRegex = regexp.MustCompile("([\\w\\d]{2,6})=([\\w\\d\\/\\.\\-_]+)[,]?")
|
|
||||||
)
|
|
||||||
|
|
||||||
// VaultResources is a collection of type resource
|
// VaultResources is a collection of type resource
|
||||||
type VaultResources struct {
|
type VaultResources struct {
|
||||||
// an array of resource to retrieve
|
// an array of resource to retrieve
|
||||||
|
@ -34,31 +28,39 @@ type VaultResources struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set is the implementation for the parser
|
// Set is the implementation for the parser
|
||||||
|
// secret:test:file=filename.test,fmt=yaml
|
||||||
func (r *VaultResources) Set(value string) error {
|
func (r *VaultResources) Set(value string) error {
|
||||||
rn := defaultVaultResource()
|
rn := defaultVaultResource()
|
||||||
|
|
||||||
// step: extract the resource type and name
|
// step: split on the ':'
|
||||||
if matched := resourceRegex.MatchString(value); !matched {
|
items := strings.Split(value, ":")
|
||||||
return fmt.Errorf("invalid resource specification, should be TYPE:NAME:?(OPTION_NAME=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
|
// step: extract the elements
|
||||||
matches := resourceRegex.FindAllStringSubmatch(value, -1)
|
rn.resource = items[0]
|
||||||
rn.resource = matches[0][1]
|
rn.path = items[1]
|
||||||
rn.path = matches[0][2]
|
|
||||||
rn.options = make(map[string]string, 0)
|
rn.options = make(map[string]string, 0)
|
||||||
|
|
||||||
// step: do we have any options for the resource?
|
// step: extract any options
|
||||||
if len(matches[0]) == 4 {
|
if len(items) > 2 {
|
||||||
opts := matches[0][3]
|
for _, x := range strings.Split(items[2], ",") {
|
||||||
for len(opts) > 0 {
|
kp := strings.Split(x, "=")
|
||||||
if matched := resourceOptionsRegex.MatchString(opts); !matched {
|
if len(kp) != 2 {
|
||||||
return fmt.Errorf("invalid resource options specified: '%s', please check usage", opts)
|
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[kp[0]] = kp[1]
|
||||||
rn.options[matches[0][1]] = matches[0][2]
|
|
||||||
opts = strings.TrimPrefix(opts, matches[0][0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// step: append to the list of resources
|
// step: append to the list of resources
|
||||||
|
|
|
@ -26,10 +26,10 @@ func TestSetResources(t *testing.T) {
|
||||||
var items VaultResources
|
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,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/username"))
|
||||||
assert.Nil(t, items.Set("secret:/db/prod:file=filename.test,fmt=yaml"))
|
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"))
|
||||||
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,file=/etc/certs/ssl/blah.example.com"))
|
||||||
assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,renew=10s"))
|
assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,renew=10s"))
|
||||||
|
|
|
@ -19,6 +19,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"github.com/hashicorp/vault/api"
|
"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
|
// step: if the answer is no, we set the notification between 80-95% of the lease time of the secret
|
||||||
if r.renewalTime <= 0 {
|
if r.renewalTime <= 0 {
|
||||||
r.renewalTime = r.calculateRenewal()
|
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)
|
glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime)
|
||||||
// step: wait for the duration
|
// step: wait for the duration
|
||||||
|
@ -64,5 +66,5 @@ func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) {
|
||||||
func (r watchedResource) calculateRenewal() time.Duration {
|
func (r watchedResource) calculateRenewal() time.Duration {
|
||||||
return time.Duration(getDurationWithin(
|
return time.Duration(getDurationWithin(
|
||||||
int(float64(r.secret.LeaseDuration)*renewalMinimum),
|
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