Skip to content

Commit

Permalink
Merge pull request kelseyhightower#383 from odedlaz/vault
Browse files Browse the repository at this point in the history
Added support for Vault backend
  • Loading branch information
kelseyhightower committed Feb 18, 2016
2 parents 071e6ab + 12eae23 commit d133c25
Show file tree
Hide file tree
Showing 189 changed files with 17,295 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ language: go
go:
- 1.4
- tip
env:
- VAULT_ADDR='http://127.0.0.1:8200'
services:
- redis
before_install:
Expand All @@ -24,6 +26,11 @@ before_install:
- mkdir -p ./rancher-metadata
- tar xzf rancher-metadata.tar.gz --strip-components=1 -C ./rancher-metadata
- sudo mv ./rancher-metadata/bin/rancher-metadata /bin/
# Install vault
- wget https://releases.hashicorp.com/vault/0.4.1/vault_0.4.1_linux_amd64.zip
- unzip vault_0.4.1_linux_amd64.zip
- sudo mv vault /bin/
- vault server -dev &
install:
- sudo pip install awscli
- go get github.com/constabulary/gb/...
Expand All @@ -36,3 +43,4 @@ script:
- bash integration/etcd/test.sh
- bash integration/redis/test.sh
- bash integration/rancher/test.sh
- bash integration/vault/test.sh
23 changes: 23 additions & 0 deletions integration/vault/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
set -e
ROOT_TOKEN=$(vault read -field id auth/token/lookup-self)

vault mount -path database generic
vault mount -path key generic
vault mount -path upstream generic

vault write key value=foobar
vault write database/host value=127.0.0.1
vault write database/port value=3306
vault write database/username value=confd
vault write database/password value=p@sSw0rd
vault write upstream app1=10.0.1.10:8080 app2=10.0.1.11:8080


# Run confd
confd --onetime --log-level debug \
--confdir ./integration/confdir \
--backend vault \
--auth-type token \
--auth-token $ROOT_TOKEN \
--node http://127.0.0.1:8200
13 changes: 13 additions & 0 deletions src/github.com/kelseyhightower/confd/backends/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/kelseyhightower/confd/backends/rancher"
"github.com/kelseyhightower/confd/backends/redis"
"github.com/kelseyhightower/confd/backends/stackengine"
"github.com/kelseyhightower/confd/backends/vault"
"github.com/kelseyhightower/confd/backends/zookeeper"
"github.com/kelseyhightower/confd/log"
)
Expand Down Expand Up @@ -46,6 +47,18 @@ func New(config Config) (StoreClient, error) {
return redis.NewRedisClient(backendNodes)
case "env":
return env.NewEnvClient()
case "vault":
vaultConfig := map[string]string{
"app-id": config.AppID,
"user-id": config.UserID,
"username": config.Username,
"password": config.Password,
"token": config.AuthToken,
"cert": config.ClientCert,
"key": config.ClientKey,
"caCert": config.ClientCaKeys,
}
return vault.New(backendNodes[0], config.AuthType, vaultConfig)
case "dynamodb":
table := config.Table
log.Info("DynamoDB table set to " + table)
Expand Down
3 changes: 3 additions & 0 deletions src/github.com/kelseyhightower/confd/backends/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backends

type Config struct {
AuthToken string
AuthType string
Backend string
BasicAuth bool
ClientCaKeys string
Expand All @@ -12,4 +13,6 @@ type Config struct {
Scheme string
Table string
Username string
AppID string
UserID string
}
209 changes: 209 additions & 0 deletions src/github.com/kelseyhightower/confd/backends/vault/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package vault

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"path"

vaultapi "github.com/hashicorp/vault/api"
"github.com/kelseyhightower/confd/log"
)

// Client is a wrapper around the vault client
type Client struct {
client *vaultapi.Client
}

// get a
func getParameter(key string, parameters map[string]string) string {
value := parameters[key]
if value == "" {
// panic if a configuration is missing
panic(fmt.Sprintf("%s is missing from configuration", key))
}
return value
}

// panicToError converts a panic to an error
func panicToError(err *error) {
if r := recover(); r != nil {
switch t := r.(type) {
case string:
*err = errors.New(t)
case error:
*err = t
default: // panic again if we don't know how to handle
panic(r)
}
}
}

// authenticate with the remote client
func authenticate(c *vaultapi.Client, authType string, params map[string]string) (err error) {
var secret *vaultapi.Secret

// handle panics gracefully by creating an error
// this would happen when we get a parameter that is missing
defer panicToError(&err)

switch authType {
case "app-id":
secret, err = c.Logical().Write("/auth/app-id/login", map[string]interface{}{
"app_id": getParameter("app-id", params),
"user_id": getParameter("user-id", params),
})
case "github":
secret, err = c.Logical().Write("/auth/github/login", map[string]interface{}{
"token": getParameter("token", params),
})
case "token":
c.SetToken(getParameter("token", params))
secret, err = c.Logical().Read("/auth/token/lookup-self")
case "userpass":
username, password := getParameter("username", params), getParameter("password", params)
secret, err = c.Logical().Write(fmt.Sprintf("/auth/userpass/login/%s", username), map[string]interface{}{
"password": password,
})
}

if err != nil {
return err
}

// if the token has already been set
if c.Token() != "" {
return nil
}

log.Debug("client authenticated with auth backend: %s", authType)
// the default place for a token is in the auth section
// otherwise, the backend will set the token itself
c.SetToken(secret.Auth.ClientToken)
return nil
}

func getConfig(address, cert, key, caCert string) (*vaultapi.Config, error) {
conf := vaultapi.DefaultConfig()
conf.Address = address

tlsConfig := &tls.Config{}
if cert != "" && key != "" {
clientCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{clientCert}
tlsConfig.BuildNameToCertificate()
}

if caCert != "" {
ca, err := ioutil.ReadFile(caCert)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(ca)
tlsConfig.RootCAs = caCertPool
}

conf.HttpClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}

return conf, nil
}

// New returns an *vault.Client with a connection to named machines.
// It returns an error if a connection to the cluster cannot be made.
func New(address, authType string, params map[string]string) (*Client, error) {
if authType == "" {
return nil, errors.New("you have to set the auth type when using the vault backend")
}
log.Info("Vault authentication backend set to %s", authType)
conf, err := getConfig(address, params["cert"], params["key"], params["caCert"])

if err != nil {
return nil, err
}

c, err := vaultapi.NewClient(conf)
if err != nil {
return nil, err
}

if err := authenticate(c, authType, params); err != nil {
return nil, err
}
return &Client{c}, nil
}

// GetValues queries etcd for keys prefixed by prefix.
func (c *Client) GetValues(keys []string) (map[string]string, error) {
vars := make(map[string]string)
for _, key := range keys {
log.Debug("getting %s from vault", key)
resp, err := c.client.Logical().Read(key)

if err != nil {
log.Debug("there was an error extracting %s", key)
return nil, err
}
if resp == nil || resp.Data == nil {
continue
}

// if the key has only one string value
// treat it as a string and not a map of values
if val, ok := isKV(resp.Data); ok {
vars[key] = val
} else {
// save the json encoded response
// and flatten it to allow usage of gets & getvs
js, _ := json.Marshal(resp.Data)
vars[key] = string(js)
flatten(key, resp.Data, vars)
}
}
return vars, nil
}

// isKV checks if a given map has only one key of type string
// if so, returns the value of that key
func isKV(data map[string]interface{}) (string, bool) {
if len(data) == 1 {
if value, ok := data["value"]; ok {
if text, ok := value.(string); ok {
return text, true
}
}
}
return "", false
}

// recursively walks on all the values of a specific key and set them in the variables map
func flatten(key string, value interface{}, vars map[string]string) {
switch value.(type) {
case string:
log.Debug("setting key %s to: %s", key, value)
vars[key] = value.(string)
case map[string]interface{}:
inner := value.(map[string]interface{})
for innerKey, innerValue := range inner {
innerKey = path.Join(key, "/", innerKey)
flatten(innerKey, innerValue, vars)
}
default: // we don't know how to handle non string or maps of strings
log.Warning("type of '%s' is not supported (%T)", key, value)
}
}

// WatchPrefix - not implemented at the moment
func (c *Client) WatchPrefix(prefix string, waitIndex uint64, stopChan chan bool) (uint64, error) {
<-stopChan
return 0, nil
}
22 changes: 20 additions & 2 deletions src/github.com/kelseyhightower/confd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
configFile = ""
defaultConfigFile = "/etc/confd/confd.toml"
authToken string
authType string
backend string
basicAuth bool
clientCaKeys string
Expand All @@ -45,11 +46,14 @@ var (
username string
password string
watch bool
appID string
userID string
)

// A Config structure is used to configure confd.
type Config struct {
AuthToken string `toml:"auth_token"`
AuthType string `toml:"auth_type"`
Backend string `toml:"backend"`
BasicAuth bool `toml:"basic_auth"`
BackendNodes []string `toml:"nodes"`
Expand All @@ -68,6 +72,8 @@ type Config struct {
Username string `toml:"username"`
LogLevel string `toml:"log-level"`
Watch bool `toml:"watch"`
AppID string `toml:"app_id"`
UserID string `toml:"user_id"`
}

func init() {
Expand All @@ -90,9 +96,12 @@ func init() {
flag.StringVar(&scheme, "scheme", "http", "the backend URI scheme (http or https)")
flag.StringVar(&srvDomain, "srv-domain", "", "the name of the resource record")
flag.BoolVar(&syncOnly, "sync-only", false, "sync without check_cmd and reload_cmd")
flag.StringVar(&authType, "auth-type", "", "Vault auth backend type to use (only used with -backend=vault)")
flag.StringVar(&appID, "app-id", "", "Vault app-id to use with the app-id backend (only used with -backend=vault and auth-type=app-id)")
flag.StringVar(&userID, "user-id", "", "Vault user-id to use with the app-id backend (only used with -backend=value and auth-type=app-id)")
flag.StringVar(&table, "table", "", "the name of the DynamoDB table (only used with -backend=dynamodb)")
flag.StringVar(&username, "username", "", "the username to authenticate as (only used with -backend=etcd)")
flag.StringVar(&password, "password", "", "the password to authenticate with (only used with -backend=etcd)")
flag.StringVar(&username, "username", "", "the username to authenticate as (only used with vault and etcd backends)")
flag.StringVar(&password, "password", "", "the password to authenticate with (only used with vault and etcd backends)")
flag.BoolVar(&watch, "watch", false, "enable watch support")
}

Expand Down Expand Up @@ -189,6 +198,7 @@ func initConfig() error {

backendsConfig = backends.Config{
AuthToken: config.AuthToken,
AuthType: config.AuthType,
Backend: config.Backend,
BasicAuth: config.BasicAuth,
ClientCaKeys: config.ClientCaKeys,
Expand All @@ -199,6 +209,8 @@ func initConfig() error {
Scheme: config.Scheme,
Table: config.Table,
Username: config.Username,
AppID: config.AppID,
UserID: config.UserID,
}
// Template configuration.
templateConfig = template.Config{
Expand Down Expand Up @@ -255,6 +267,8 @@ func setConfigFromFlag(f *flag.Flag) {
switch f.Name {
case "auth-token":
config.AuthToken = authToken
case "auth-type":
config.AuthType = authType
case "backend":
config.Backend = backend
case "basic-auth":
Expand Down Expand Up @@ -291,5 +305,9 @@ func setConfigFromFlag(f *flag.Flag) {
config.LogLevel = logLevel
case "watch":
config.Watch = watch
case "app-id":
config.AppID = appID
case "user-id":
config.UserID = userID
}
}
Loading

0 comments on commit d133c25

Please sign in to comment.