Skip to content

Commit

Permalink
Add Vault backend
Browse files Browse the repository at this point in the history
Fixes #725
  • Loading branch information
dominikschulz committed Mar 26, 2018
1 parent 2260495 commit 2fe0273
Show file tree
Hide file tree
Showing 13 changed files with 692 additions and 31 deletions.
36 changes: 36 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,39 @@ different from what GPG is using.

Please see the backend [Readme](https://github.com/justwatchcom/gopass/blob/master/pkg/backend/crypto/xc/README.md) for more details. Proper documentation for this
backend still needs to written and will be added at a later point.

### Vault (vault)

This is an experimental crypto and storage backend currently available as a
preview. This backend is special in that it's not implemented as a traditional
backend but instead as an alternative `sub store` implementation. That was
necessary as Vault already works with complex Secrets by itself and it didn't
seem wise to force the internal gopass architecture onto this sophisticated
storage scheme. That would have worked well for gopass, but would have stopped
interoperability with other Vault users.

**Note**: This backend fully relies on Vault for encryption and access
management. It mostly exists as an easy access path to sync static secrets
between a password store and Vault.

To use the Vault backend manually create a mount in the config like in the
following example:

```
cat <<EOF >> $HOME/.config/gopass/config.yml
mounts:
vault:
path: vault+https://vault:8200/secret?token=some-token
EOF
```

All `TLSConfig` options for Vault are supported as query parameters.

| **Query Parameter** | **TLSConfig Attribute** | Description |
| ------------------- | ----------------------- | ----------- |
| tls-cacert | CACert | the Path to a PEM-encoded CA cert file |
| tls-capath | CAPath | the Path to a directory of PEM-encoded CA cert files |
| tls-clientcert | ClientCert | the Path to the certificate for Vault communication |
| tls-clientkey | ClientKey | the path to the private key for Vault communication |
| tls-servername | TLSServerName | set the SNI host when connecting |
| tls-insecure | Disables SSL verification |
2 changes: 2 additions & 0 deletions pkg/backend/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
XC
// OpenPGP is a GPG1.x compatible pure-Go crypto backend
OpenPGP
// Vault is Hashicorp Vault backend
Vault
)

func (c CryptoBackend) String() string {
Expand Down
1 change: 1 addition & 0 deletions pkg/backend/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var (
"gpgcli": GPGCLI,
"xc": XC,
"openpgp": OpenPGP,
"vault": Vault,
}
cryptoBackendToNameMap = map[CryptoBackend]string{}
rcsNameToBackendMap = map[string]RCSBackend{
Expand Down
5 changes: 4 additions & 1 deletion pkg/backend/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,11 @@ func splitBackends(in string) (string, string, string, string, error) {
backends := p[0]
scheme := p[1]
p = strings.Split(backends, "-")
if len(p) < 3 {
if len(p) < 1 {
return "", "", "", "", fmt.Errorf("invalid")
}
if len(p) < 3 {
return p[0], "", "", scheme, nil
}
return p[0], p[1], p[2], scheme, nil
}
13 changes: 11 additions & 2 deletions pkg/store/root/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ func (r *Store) Initialized(ctx context.Context) bool {
// Init tries to initialize a new password store location matching the object
func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) error {
out.Debug(ctx, "Instantiating new sub store %s at %s for %+v", alias, path, ids)
sub, err := sub.New(ctx, alias, path, r.cfg.Directory(), r.agent)
// parse backend URL
pathURL, err := backend.ParseURL(path)
if err != nil {
return errors.Wrapf(err, "failed to parse backend URL '%s': %s", path, err)
}
sub, err := sub.New(ctx, alias, pathURL, r.cfg.Directory(), r.agent)
if err != nil {
return err
}
Expand Down Expand Up @@ -94,7 +99,11 @@ func (r *Store) initialize(ctx context.Context) error {
if !backend.HasStorageBackend(ctx) {
ctx = backend.WithStorageBackend(ctx, r.cfg.Root.Path.Storage)
}
s, err := sub.New(ctx, "", r.url.String(), r.cfg.Directory(), r.agent)
bu, err := backend.ParseURL(r.url.String())
if err != nil {
return errors.Wrapf(err, "failed to parse backend URL '%s': %s", r.url.String(), err)
}
s, err := sub.New(ctx, "", bu, r.cfg.Directory(), r.agent)
if err != nil {
return errors.Wrapf(err, "failed to initialize the root store at '%s': %s", r.url.String(), err)
}
Expand Down
65 changes: 47 additions & 18 deletions pkg/store/root/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/justwatchcom/gopass/pkg/out"
"github.com/justwatchcom/gopass/pkg/store"
"github.com/justwatchcom/gopass/pkg/store/sub"
"github.com/justwatchcom/gopass/pkg/store/vault"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -47,33 +48,23 @@ func (r *Store) addMount(ctx context.Context, alias, path string, sc *config.Sto
out.Debug(ctx, "addMount - Using RCS backend %s", backend.RCSBackendName(sc.Path.RCS))
}
}
s, err := sub.New(ctx, alias, path, r.cfg.Directory(), r.agent)

// parse backend URL
pathURL, err := backend.ParseURL(path)
if err != nil {
return errors.Wrapf(err, "failed to initialize store '%s' at '%s': %s", alias, path, err)
return errors.Wrapf(err, "failed to parse backend URL '%s': %s", path, err)
}

if !s.Initialized(ctx) {
out.Debug(ctx, "[%s] Mount %s is not initialized", alias, path)
if len(keys) < 1 {
return errors.Errorf("password store %s is not initialized. Try gopass init --store %s --path %s", alias, alias, path)
}
if err := s.Init(ctx, path, keys...); err != nil {
return errors.Wrapf(err, "failed to initialize store '%s' at '%s'", alias, path)
}
out.Green(ctx, "Password store %s initialized for:", path)
for _, r := range s.Recipients(ctx) {
color.Yellow(r)
}
// initialize sub store
s, err := r.initSub(ctx, alias, pathURL, keys)
if err != nil {
return errors.Wrapf(err, "failed to init sub store")
}

r.mounts[alias] = s
if r.cfg.Mounts == nil {
r.cfg.Mounts = make(map[string]*config.StoreConfig, 1)
}
pathURL, err := backend.ParseURL(path)
if err != nil {
return errors.Wrapf(err, "failed to parse backend URL '%s': %s", path, err)
}
if sc == nil {
// imporant: copy root config to avoid overwriting it with sub store
// values
Expand All @@ -91,9 +82,47 @@ func (r *Store) addMount(ctx context.Context, alias, path string, sc *config.Sto
sc.Path.Storage = backend.GetStorageBackend(ctx)
}
r.cfg.Mounts[alias] = sc

out.Debug(ctx, "Added mount %s -> %s", alias, sc.Path.String())
return nil
}

func (r *Store) initSubVault(ctx context.Context, alias string, path *backend.URL) (store.Store, error) {
return vault.New(alias, path)
}

func (r *Store) initSub(ctx context.Context, alias string, path *backend.URL, keys []string) (store.Store, error) {
// init vault sub store
if backend.GetCryptoBackend(ctx) == backend.Vault || path.Crypto == backend.Vault {
out.Debug(ctx, "Initializing Vault Store at %s -> %s", alias, path.String())
return r.initSubVault(ctx, alias, path)
}

// init regular sub store
s, err := sub.New(ctx, alias, path, r.cfg.Directory(), r.agent)
if err != nil {
return nil, errors.Wrapf(err, "failed to initialize store '%s' at '%s': %s", alias, path, err)
}

if s.Initialized(ctx) {
return s, nil
}

out.Debug(ctx, "[%s] Mount %s is not initialized", alias, path)
if len(keys) < 1 {
return s, errors.Errorf("password store %s is not initialized. Try gopass init --store %s --path %s", alias, alias, path)
}
if err := s.Init(ctx, path.String(), keys...); err != nil {
return s, errors.Wrapf(err, "failed to initialize store '%s' at '%s'", alias, path)
}
out.Green(ctx, "Password store %s initialized for:", path)
for _, r := range s.Recipients(ctx) {
color.Yellow(r)
}

return s, nil
}

// RemoveMount removes and existing mount
func (r *Store) RemoveMount(ctx context.Context, alias string) error {
if _, found := r.mounts[alias]; !found {
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/sub/recipients_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func TestListRecipients(t *testing.T) {
s, err := New(
ctx,
"",
tempdir,
backend.FromPath(tempdir),
tempdir,
nil,
)
Expand Down
7 changes: 1 addition & 6 deletions pkg/store/sub/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,7 @@ type Store struct {
}

// New creates a new store, copying settings from the given root store
func New(ctx context.Context, alias, path, cfgdir string, agent *client.Client) (*Store, error) {
out.Debug(ctx, "sub.New - Path: %s", path)
u, err := backend.ParseURL(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse path URL '%s': %s", path, err)
}
func New(ctx context.Context, alias string, u *backend.URL, cfgdir string, agent *client.Client) (*Store, error) {
out.Debug(ctx, "sub.New - URL: %s", u.String())

s := &Store{
Expand Down
4 changes: 2 additions & 2 deletions pkg/store/sub/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func createSubStore(dir string) (*Store, error) {
return New(
ctx,
"",
sd,
backend.FromPath(sd),
sd,
nil,
)
Expand Down Expand Up @@ -185,7 +185,7 @@ func TestNew(t *testing.T) {
ok: false,
},
} {
s, err := New(tc.ctx, "", tempdir, tempdir, nil)
s, err := New(tc.ctx, "", backend.FromPath(tempdir), tempdir, nil)
if tc.ok {
assert.NoError(t, err)
assert.NotNil(t, s)
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/sub/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestTemplates(t *testing.T) {
s, err := New(
ctx,
"",
tempdir,
backend.FromPath(tempdir),
tempdir,
nil,
)
Expand Down
143 changes: 143 additions & 0 deletions pkg/store/vault/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package vault

import (
"bytes"
"errors"
"sort"
"strings"

"github.com/justwatchcom/gopass/pkg/store"
)

// Secret is a vault secret
type Secret struct {
d map[string]interface{}
}

// Body always returns the empty string
func (s *Secret) Body() string {
return ""
}

// Bytes returns a list serialized copy of this secret
func (s *Secret) Bytes() ([]byte, error) {
if s.d == nil {
return []byte{}, nil
}

buf := &bytes.Buffer{}
if pw, found := s.d[passwordKey]; found {
if sv, ok := pw.(string); ok {
_, _ = buf.WriteString(sv)
}
}
_, _ = buf.WriteString("\n")
keys := make([]string, 0, len(s.d))
for k := range s.d {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := s.d[k]
if k == passwordKey {
continue
}
_, _ = buf.WriteString(k)
_, _ = buf.WriteString(": ")
if sv, ok := v.(string); ok {
_, _ = buf.WriteString(sv)
}
_, _ = buf.WriteString("\n")
}
return buf.Bytes(), nil
}

// Data returns the data map. Will never ne nil
func (s *Secret) Data() map[string]interface{} {
if s.d == nil {
s.d = make(map[string]interface{})
}
return s.d
}

// DeleteKey removes a single key
func (s *Secret) DeleteKey(key string) error {
if s.d == nil {
return nil
}
delete(s.d, key)
return nil
}

// Equal returns true if two secrets match
func (s *Secret) Equal(other store.Secret) bool {
b1, err := s.Bytes()
if err != nil {
return false
}
b2, err := other.Bytes()
if err != nil {
return false
}
return string(b1) == string(b2)
}

// Password returns the password
func (s *Secret) Password() string {
v := s.d[passwordKey]
if sv, ok := v.(string); ok {
return sv
}
return ""
}

// SetBody is not supported
func (s *Secret) SetBody(string) error {
return errors.New("not supported")
}

// SetPassword sets the password
func (s *Secret) SetPassword(pw string) {
s.d[passwordKey] = pw
}

// SetValue sets a single key
func (s *Secret) SetValue(key string, value string) error {
s.d[key] = value
return nil
}

// String implement fmt.Stringer
func (s *Secret) String() string {
var buf strings.Builder
if sv, ok := s.d[passwordKey].(string); ok {
_, _ = buf.WriteString(sv)
}
keys := make([]string, 0, len(s.d))
for k := range s.d {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
value := s.d[key]
if key == passwordKey {
continue
}
_, _ = buf.WriteString(key)
_, _ = buf.WriteString(": ")
if sv, ok := value.(string); ok {
_, _ = buf.WriteString(sv)
}
_, _ = buf.WriteString("\n")
}
return buf.String()
}

// Value returns a single value
func (s *Secret) Value(key string) (string, error) {
v := s.d[key]
if sv, ok := v.(string); ok {
return sv, nil
}
return "", nil
}
Loading

0 comments on commit 2fe0273

Please sign in to comment.