aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorNiall Sheridan <nsheridan@gmail.com>2016-10-04 14:37:01 -0700
committerNiall Sheridan <nsheridan@gmail.com>2016-10-06 22:02:39 -0500
commit17cd70cea546e287713a3d4c086528a85abefa2e (patch)
treef52ffa10f2065c47445bd6c37f07a57f68074100 /server
parent294020406c257ad4eb1867a1e7fb8b694aefddd2 (diff)
Add support for Hashicorp Vault
Vault is supported for the following: As a well-known filesystem for TLS cert, TLS key and SSH signing key. For configuration secrets for cookie_secret, csrf_secret, oauth_client_id and oauth_client_secret options.
Diffstat (limited to 'server')
-rw-r--r--server/config/config.go70
-rw-r--r--server/helpers/vault/vault.go55
-rw-r--r--server/store/store.go4
-rw-r--r--server/util/util.go (renamed from server/certutil/util.go)2
-rw-r--r--server/util/util_test.go (renamed from server/certutil/util_test.go)2
-rw-r--r--server/wkfs/s3fs/s3.go (renamed from server/fs/s3.go)2
-rw-r--r--server/wkfs/vaultfs/vault.go91
7 files changed, 218 insertions, 8 deletions
diff --git a/server/config/config.go b/server/config/config.go
index 3587e9f..9678f6d 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -7,6 +7,7 @@ import (
"strconv"
"github.com/hashicorp/go-multierror"
+ "github.com/nsheridan/cashier/server/helpers/vault"
"github.com/spf13/viper"
)
@@ -16,6 +17,7 @@ type Config struct {
Auth *Auth `mapstructure:"auth"`
SSH *SSH `mapstructure:"ssh"`
AWS *AWS `mapstructure:"aws"`
+ Vault *Vault `mapstructure:"vault"`
}
// unmarshalled holds the raw config.
@@ -24,6 +26,7 @@ type unmarshalled struct {
Auth []Auth `mapstructure:"auth"`
SSH []SSH `mapstructure:"ssh"`
AWS []AWS `mapstructure:"aws"`
+ Vault []Vault `mapstructure:"vault"`
}
// Server holds the configuration specific to the web server and sessions.
@@ -66,21 +69,31 @@ type AWS struct {
SecretKey string `mapstructure:"secret_key"`
}
+// Vault holds Hashicorp Vault configuration.
+type Vault struct {
+ Address string `mapstructure:"address"`
+ Token string `mapstructure:"token"`
+}
+
func verifyConfig(u *unmarshalled) error {
var err error
if len(u.SSH) == 0 {
- err = multierror.Append(errors.New("missing ssh config block"))
+ err = multierror.Append(errors.New("missing ssh config section"))
}
if len(u.Auth) == 0 {
- err = multierror.Append(errors.New("missing auth config block"))
+ err = multierror.Append(errors.New("missing auth config section"))
}
if len(u.Server) == 0 {
- err = multierror.Append(errors.New("missing server config block"))
+ err = multierror.Append(errors.New("missing server config section"))
}
if len(u.AWS) == 0 {
// AWS config is optional
u.AWS = append(u.AWS, AWS{})
}
+ if len(u.Vault) == 0 {
+ // Vault config is optional
+ u.Vault = append(u.Vault, Vault{})
+ }
return err
}
@@ -106,6 +119,53 @@ func setFromEnv(u *unmarshalled) {
}
}
+func setFromVault(u *unmarshalled) error {
+ if len(u.Vault) == 0 || u.Vault[0].Token == "" || u.Vault[0].Address == "" {
+ return nil
+ }
+ v, err := vault.NewClient(u.Vault[0].Address, u.Vault[0].Token)
+ if err != nil {
+ return err
+ }
+ get := func(value string) (string, error) {
+ if value[:7] == "/vault/" {
+ return v.Read(value)
+ }
+ return value, nil
+ }
+ if len(u.Auth) > 0 {
+ u.Auth[0].OauthClientID, err = get(u.Auth[0].OauthClientID)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ u.Auth[0].OauthClientSecret, err = get(u.Auth[0].OauthClientSecret)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ }
+ if len(u.Server) > 0 {
+ u.Server[0].CSRFSecret, err = get(u.Server[0].CSRFSecret)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ u.Server[0].CookieSecret, err = get(u.Server[0].CookieSecret)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ }
+ if len(u.AWS) > 0 {
+ u.AWS[0].AccessKey, err = get(u.AWS[0].AccessKey)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ u.AWS[0].SecretKey, err = get(u.AWS[0].SecretKey)
+ if err != nil {
+ err = multierror.Append(err)
+ }
+ }
+ return err
+}
+
// ReadConfig parses a JSON configuration file into a Config struct.
func ReadConfig(r io.Reader) (*Config, error) {
u := &unmarshalled{}
@@ -118,6 +178,9 @@ func ReadConfig(r io.Reader) (*Config, error) {
return nil, err
}
setFromEnv(u)
+ if err := setFromVault(u); err != nil {
+ return nil, err
+ }
if err := verifyConfig(u); err != nil {
return nil, err
}
@@ -126,5 +189,6 @@ func ReadConfig(r io.Reader) (*Config, error) {
Auth: &u.Auth[0],
SSH: &u.SSH[0],
AWS: &u.AWS[0],
+ Vault: &u.Vault[0],
}, nil
}
diff --git a/server/helpers/vault/vault.go b/server/helpers/vault/vault.go
new file mode 100644
index 0000000..bec18b9
--- /dev/null
+++ b/server/helpers/vault/vault.go
@@ -0,0 +1,55 @@
+package vault
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/hashicorp/vault/api"
+)
+
+// NewClient returns a new vault client.
+func NewClient(address, token string) (*Client, error) {
+ config := &api.Config{
+ Address: address,
+ }
+ client, err := api.NewClient(config)
+ if err != nil {
+ return nil, err
+ }
+ client.SetToken(token)
+ return &Client{
+ vault: client,
+ }, nil
+}
+
+func parseName(name string) (path, key string) {
+ name = strings.TrimPrefix(name, "/vault/")
+ i := strings.LastIndex(name, "/")
+ if i < 0 {
+ return name, ""
+ }
+ return name[:i], name[i+1:]
+}
+
+// Client is a simple client for vault.
+type Client struct {
+ vault *api.Client
+}
+
+// Read returns a secret for a given path and key of the form `/vault/secret/path/key`.
+// If the requested key cannot be read the original string is returned along with an error.
+func (c *Client) Read(value string) (string, error) {
+ p, k := parseName(value)
+ data, err := c.vault.Logical().Read(p)
+ if err != nil {
+ return value, err
+ }
+ if data == nil {
+ return value, fmt.Errorf("no such key %s", k)
+ }
+ secret, ok := data.Data[k]
+ if !ok {
+ return value, fmt.Errorf("no such key %s", k)
+ }
+ return secret.(string), nil
+}
diff --git a/server/store/store.go b/server/store/store.go
index a846bda..c039d3c 100644
--- a/server/store/store.go
+++ b/server/store/store.go
@@ -5,7 +5,7 @@ import (
"golang.org/x/crypto/ssh"
- "github.com/nsheridan/cashier/server/certutil"
+ "github.com/nsheridan/cashier/server/util"
)
// CertStorer records issued certs in a persistent store for audit and
@@ -40,6 +40,6 @@ func parseCertificate(cert *ssh.Certificate) *CertRecord {
Principals: cert.ValidPrincipals,
CreatedAt: parseTime(cert.ValidAfter),
Expires: parseTime(cert.ValidBefore),
- Raw: certutil.GetPublicKey(cert),
+ Raw: util.GetPublicKey(cert),
}
}
diff --git a/server/certutil/util.go b/server/util/util.go
index eb1900b..10f5eca 100644
--- a/server/certutil/util.go
+++ b/server/util/util.go
@@ -1,4 +1,4 @@
-package certutil
+package util
import "golang.org/x/crypto/ssh"
diff --git a/server/certutil/util_test.go b/server/util/util_test.go
index df42b90..d294d86 100644
--- a/server/certutil/util_test.go
+++ b/server/util/util_test.go
@@ -1,4 +1,4 @@
-package certutil
+package util
import (
"testing"
diff --git a/server/fs/s3.go b/server/wkfs/s3fs/s3.go
index e16e7d6..a71d874 100644
--- a/server/fs/s3.go
+++ b/server/wkfs/s3fs/s3.go
@@ -1,4 +1,4 @@
-package fs
+package s3fs
import (
"bytes"
diff --git a/server/wkfs/vaultfs/vault.go b/server/wkfs/vaultfs/vault.go
new file mode 100644
index 0000000..6f11057
--- /dev/null
+++ b/server/wkfs/vaultfs/vault.go
@@ -0,0 +1,91 @@
+package vaultfs
+
+import (
+ "bytes"
+ "errors"
+ "os"
+ "path"
+ "time"
+
+ "github.com/nsheridan/cashier/server/config"
+ "github.com/nsheridan/cashier/server/helpers/vault"
+ "go4.org/wkfs"
+)
+
+// Register the /vault/ filesystem as a well-known filesystem.
+func Register(vc *config.Vault) {
+ client, err := vault.NewClient(vc.Address, vc.Token)
+ if err != nil {
+ registerBrokenFS(err)
+ return
+ }
+ wkfs.RegisterFS("/vault/", &vaultFS{
+ client: client,
+ })
+}
+
+func registerBrokenFS(err error) {
+ wkfs.RegisterFS("/vault/", &vaultFS{
+ err: err,
+ })
+}
+
+type vaultFS struct {
+ err error
+ client *vault.Client
+}
+
+// Open opens the named file for reading.
+func (fs *vaultFS) Open(name string) (wkfs.File, error) {
+ secret, err := fs.client.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ return &file{
+ name: name,
+ Reader: bytes.NewReader([]byte(secret)),
+ }, nil
+}
+
+func (fs *vaultFS) Stat(name string) (os.FileInfo, error) { return fs.Lstat(name) }
+func (fs *vaultFS) Lstat(name string) (os.FileInfo, error) {
+ secret, err := fs.client.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ return &statInfo{
+ name: path.Base(name),
+ size: int64(len(secret)),
+ }, nil
+}
+
+func (fs *vaultFS) MkdirAll(path string, perm os.FileMode) error { return nil }
+
+func (fs *vaultFS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileWriter, error) {
+ return nil, errors.New("not implemented")
+}
+
+type statInfo struct {
+ name string
+ size int64
+ isDir bool
+ modtime time.Time
+}
+
+func (si *statInfo) IsDir() bool { return si.isDir }
+func (si *statInfo) ModTime() time.Time { return si.modtime }
+func (si *statInfo) Mode() os.FileMode { return 0644 }
+func (si *statInfo) Name() string { return path.Base(si.name) }
+func (si *statInfo) Size() int64 { return si.size }
+func (si *statInfo) Sys() interface{} { return nil }
+
+type file struct {
+ name string
+ *bytes.Reader
+}
+
+func (*file) Close() error { return nil }
+func (f *file) Name() string { return path.Base(f.name) }
+func (f *file) Stat() (os.FileInfo, error) {
+ return nil, errors.New("Stat not implemented on /vault/ files")
+}