Skip to content

Commit 8678980

Browse files
committed
Initial commit
0 parents  commit 8678980

12 files changed

Lines changed: 2835 additions & 0 deletions

File tree

LICENSE

Lines changed: 373 additions & 0 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
TOOL?=vault-plugin-database-couchbasecapella
2+
TEST?=$$(go list ./... | grep -v /vendor/ | grep -v teamcity)
3+
VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr
4+
BUILD_TAGS?=${TOOL}
5+
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
6+
GO_TEST_CMD?=go test -v
7+
8+
# bin generates the releaseable binaries for this plugin
9+
bin: fmtcheck
10+
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'"
11+
12+
default: dev
13+
14+
# dev starts up `vault` from your $PATH, then builds the couchbasecapella
15+
# plugin, registers it with vault and enables it.
16+
# A ./tmp dir is created for configs and binaries, and cleaned up on exit.
17+
dev: fmtcheck
18+
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' VAULT_DEV_BUILD=1 sh -c "'$(CURDIR)/scripts/build.sh'"
19+
20+
# test runs the unit tests and vets the code
21+
test: fmtcheck
22+
CGO_ENABLED=0 VAULT_TOKEN= ${GO_TEST_CMD} -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=5m -parallel=4
23+
24+
testacc: fmtcheck
25+
CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC=1 ${GO_TEST_CMD} -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m
26+
27+
testcompile: fmtcheck
28+
@for pkg in $(TEST) ; do \
29+
go test -v -c -tags='$(BUILD_TAGS)' $$pkg ; \
30+
done
31+
32+
fmtcheck:
33+
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
34+
35+
fmt:
36+
gofmt -w $(GOFMT_FILES)
37+
38+
.PHONY: bin default dev test testcompile fmtcheck fmt

README.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# vault-plugin-database-couchbasecapella
2+
3+
[![CircleCI](https://circleci.com/gh/hashicorp/vault-plugin-database-couchbase.svg?style=svg)](https://circleci.com/gh/hashicorp/vault-plugin-database-couchbasecapella)
4+
5+
A [Vault](https://www.vaultproject.io) plugin for Couchbase Capella
6+
7+
This project uses the database plugin interface introduced in Vault version 0.7.1.
8+
9+
The plugin supports the generation of static and dynamic user roles and root credential rotation.
10+
11+
## Build
12+
13+
To build this package for any platform you will need to clone this repository and cd into the repo directory and `go build -o couchbasecapella-database-plugin ./cmd/couchbasecapella-database-plugin/`. To test `go test` will execute a set of basic tests against against the docker.io/couchbase/server-sandbox:6.5.0 couchbase database image. To test against different sandbox images, for example 5.5.1, set the `COUCHBASE_VERSION=5.5.1` environment variable. If you want to run the tests against a local couchbase installation or an already running couchbase container, set the environment variable `COUCHBASE_HOST` before executing. **Note** you will need to align the Administrator username, password and bucket_name with the pre-set values in the `couchbasecapella_test.go` file. Set VAULT_ACC to execute all of the tests. A subset of tests can be run using the command `go test -run TestDriver/Init` for example.
14+
15+
## Installation
16+
17+
The Vault plugin system is documented on the [Vault documentation site](https://www.vaultproject.io/docs/internals/plugins.html).
18+
19+
You will need to define a plugin directory using the `plugin_directory` configuration directive, then place the
20+
`vault-plugin-database-couchbasecapella` executable generated above, into the directory.
21+
22+
**Please note:** Versions v0.2.0 onwards of this plugin are incompatible with Vault versions before 1.6.0 due to an update of the database plugin interface.
23+
24+
Sample commands for registering and starting to use the plugin:
25+
26+
```bash
27+
$ SHA256=$(shasum -a 256 plugins/couchbasecapella-database-plugin | cut -d' ' -f1)
28+
29+
$ vault secrets enable database
30+
31+
$ vault write sys/plugins/catalog/database/couchbasecapella-database-plugin sha256=$SHA256 \
32+
command=couchbasecapella-database-plugin
33+
```
34+
35+
At this stage you are now ready to initialize the plugin to connect to couchbase capella cluster using unencrypted or encrypted communications.
36+
37+
Prior to initializing the plugin, ensure that you have created an administration account. Vault will use the user specified here to create/update/revoke database credentials. That user must have the appropriate permissions to perform actions upon other database users.
38+
39+
### Unencrypted plugin initialization
40+
41+
```bash
42+
$ vault write database/config/insecure-couchbasecapella plugin_name="couchbasecapella-database-plugin" \
43+
hosts="localhost" username="Administrator" password="password" \
44+
bucket_name="travel-sample" \ # only needed for pre-6.5.0 clusters
45+
allowed_roles="insecure-couchbasecapella-admin-role,insecure-couchbasecapella-*-bucket-role,static-account"
46+
47+
# You should consider rotating the admin password. Note that if you do, the new password will never be made available
48+
# through Vault, so you should create a vault-specific database admin user for this.
49+
$ vault write -force database/rotate-root/insecure-couchbasecapella
50+
51+
```
52+
53+
Note: If you want to connect the plugin to a couchbase capella cluster prior to version 6.5.0 you will also have to supply an existing bucket (bucket_name="travel-sample") or the command will fail with the error message **"error verifying connection: error in Connection waiting for cluster: unambiguous timeout"**.
54+
55+
### Encrypted plugin initialization
56+
57+
The example here uses the self signed CA certificate that comes with the out of the box couchbase cluster installation and is not suitable for real production use where commercial grade certificates should be obtained.
58+
59+
```bash
60+
$ BASE64PEM=$(curl -X GET http://Administrator:Admin123@127.0.0.1:8091/pools/default/certificate|base64 -w0)
61+
62+
$ vault write database/config/secure-couchbasecapella plugin_name="couchbasecapella-database-plugin" \
63+
hosts="couchbases://localhost" username="Administrator" password="password" \
64+
tls=true base64pem=${BASE64PEM} \
65+
bucket_name="travel-sample" \ # only needed for pre-6.5.0 clusters
66+
allowed_roles="secure-couchbasecapella-admin-role,secure-couchbasecapella-*-bucket-role,static-account"
67+
68+
# You should consider rotating the admin password. Note that if you do, the new password will never be made available
69+
# through Vault, so you should create a vault-specific database admin user for this.
70+
$ vault write -force database/rotate-root/secure-couchbasecapella
71+
```
72+
73+
### Dynamic Role Creation
74+
75+
When you create roles, you need to provide a JSON string containing the Couchbase RBAC roles which are documented [here](https://docs.couchbase.com/server/6.5/learn/security/roles.html). From Couchbase 6.5 groups are supported and the creation statement can contain just roles or just groups or a mixture of the two. **Note** to use a group, it must have been created in the database previously.
76+
77+
```bash
78+
# if a creation_statement is not provided the user account will default to read only admin, '{"roles":[{"role":"ro_admin"}]}'
79+
$ vault write database/roles/insecure-couchbasecapella-admin-role db_name=insecure-couchbasecapella \
80+
default_ttl="5m" max_ttl="1h" creation_statements='{"roles":[{"role":"admin"}],"groups":["Supervisor"]}'
81+
82+
$ vault write database/roles/insecure-couchbasecapella-travel-sample-bucket-role db_name=insecure-couchbasecapella \
83+
default_ttl="5m" max_ttl="1h" creation_statements='{"roles":[{"role":"bucket_full_access","bucket_name":"travel-sample"}]}'
84+
Success! Data written to: database/roles/insecure-couchbasecapella-travel-sample-bucket-role
85+
```
86+
87+
If you create a role that uses groups on a pre 6.5 couchbase server it will be successful, but when you try to generate credentials
88+
you will receive the error **rpc error: code = Unknown desc = {"errors":{"groups":"Unsupported key"}} ...**
89+
90+
To retrieve the credentials for the dynamic accounts
91+
92+
```bash
93+
94+
$ vault read database/creds/insecure-couchbasecapella-admin-role
95+
Key Value
96+
--- -----
97+
lease_id database/creds/insecure-couchbasecapella-admin-role/KJ7CTmpFni6U6BCDJ14HcmDm
98+
lease_duration 5m
99+
lease_renewable true
100+
password A1a-yCSH5rAh8QAkCzwu
101+
username v-token-insecure-couchbasecapella-admin-role-yA2hgb0tfewf
102+
103+
$ vault read database/creds/insecure-couchbasecapella-travel-sample-bucket-role
104+
Key Value
105+
--- -----
106+
lease_id database/creds/insecure-couchbasecapella-travel-sample-bucket-role/OzHdfkIZdeY9p8kjdWur512j
107+
lease_duration 5m
108+
lease_renewable true
109+
password A1a-0yTIuO4q0dCvphz1
110+
username v-token-insecure-couchbasecapella-travel-sample-bucket-role-iN5
111+
112+
```
113+
114+
### Static Role Creation
115+
116+
In order to use static roles, the user must already exist in the Couchbase Capella security settings. The example below assumes that there is an existing user with the name "vault-edu". If the user does not exist you will receive the following error.
117+
118+
```bash
119+
* 1 error occurred:
120+
* error setting credentials: rpc error: code = Unknown desc = user not found | {"unique_id":"74f229fd-b3b3-4036-9673-312adae094bb","endpoint":"http://localhost:8091"}
121+
```
122+
123+
```bash
124+
$ vault write database/static-roles/static-account db_name=insecure-couchbasecapella \
125+
username="vault-edu" rotation_period="5m"
126+
Success! Data written to: database/static-roles/static-account
127+
````
128+
129+
To retrieve the credentials for the vault-edu user
130+
131+
```bash
132+
$ vault read database/static-creds/static-account
133+
Key Value
134+
--- -----
135+
last_vault_rotation 2020-06-15T14:32:16.682130141-05:00
136+
password A1a-09ApRvglZY1Usdjp
137+
rotation_period 5m
138+
ttl 30s
139+
username vault-edu
140+
```
141+
142+
## Developing
143+
144+
You can run `make dev` in the root of the repo to start up a development vault server and automatically register a local build of the plugin. You will need to have a built `vault` binary available in your `$PATH` to do so.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
hclog "github.com/hashicorp/go-hclog"
7+
couchbase "github.com/hashicorp/vault-plugin-database-couchbase"
8+
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
9+
)
10+
11+
func main() {
12+
err := Run()
13+
if err != nil {
14+
logger := hclog.New(&hclog.LoggerOptions{})
15+
16+
logger.Error("plugin shutting down", "error", err)
17+
os.Exit(1)
18+
}
19+
}
20+
21+
// Run instantiates a CouchbaseDB object, and runs the RPC server for the plugin
22+
func Run() error {
23+
dbplugin.ServeMultiplex(couchbase.New)
24+
25+
return nil
26+
}

connection_producer.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package couchbasecapella
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
"encoding/base64"
7+
"fmt"
8+
"strings"
9+
"sync"
10+
11+
"github.com/couchbase/gocb/v2"
12+
"github.com/hashicorp/errwrap"
13+
"github.com/hashicorp/vault/sdk/database/helper/connutil"
14+
"github.com/mitchellh/mapstructure"
15+
)
16+
17+
type couchbaseCapellaDBConnectionProducer struct {
18+
AccessKey string `json:"access_key"`
19+
SecretKey string `json:"secret_key"`
20+
ClusterID string `json:"cluster_id"`
21+
Hosts string `json:"hosts"`
22+
Username string `json:"username"`
23+
Password string `json:"password"`
24+
TLS bool `json:"tls"`
25+
InsecureTLS bool `json:"insecure_tls"`
26+
Base64Pem string `json:"base64pem"`
27+
BucketName string `json:"bucket_name"`
28+
29+
Initialized bool
30+
rawConfig map[string]interface{}
31+
Type string
32+
cluster *gocb.Cluster
33+
sync.RWMutex
34+
}
35+
36+
func (c *couchbaseCapellaDBConnectionProducer) secretValues() map[string]string {
37+
return map[string]string{
38+
c.Password: "[password]",
39+
c.Username: "[username]",
40+
}
41+
}
42+
43+
func (c *couchbaseCapellaDBConnectionProducer) Init(ctx context.Context, initConfig map[string]interface{}, verifyConnection bool) (saveConfig map[string]interface{}, err error) {
44+
// Don't let anyone read or write the config while we're using it
45+
c.Lock()
46+
defer c.Unlock()
47+
48+
c.rawConfig = initConfig
49+
50+
decoderConfig := &mapstructure.DecoderConfig{
51+
Result: c,
52+
WeaklyTypedInput: true,
53+
TagName: "json",
54+
}
55+
56+
decoder, err := mapstructure.NewDecoder(decoderConfig)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
err = decoder.Decode(initConfig)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
switch {
67+
case len(c.Hosts) == 0:
68+
return nil, fmt.Errorf("hosts cannot be empty")
69+
case len(c.Username) == 0:
70+
return nil, fmt.Errorf("username cannot be empty")
71+
case len(c.Password) == 0:
72+
return nil, fmt.Errorf("password cannot be empty")
73+
}
74+
75+
if c.TLS {
76+
if len(c.Base64Pem) == 0 {
77+
return nil, fmt.Errorf("base64pem cannot be empty")
78+
}
79+
80+
if !strings.HasPrefix(c.Hosts, "couchbases://") {
81+
return nil, fmt.Errorf("hosts list must start with couchbases:// for TLS connection")
82+
}
83+
}
84+
85+
c.Initialized = true
86+
87+
if verifyConnection {
88+
if _, err := c.Connection(ctx); err != nil {
89+
c.close()
90+
return nil, errwrap.Wrapf("error verifying connection: {{err}}", err)
91+
}
92+
}
93+
94+
return initConfig, nil
95+
}
96+
97+
func (c *couchbaseCapellaDBConnectionProducer) Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) error {
98+
_, err := c.Init(ctx, config, verifyConnection)
99+
return err
100+
}
101+
102+
func (c *couchbaseCapellaDBConnectionProducer) Connection(ctx context.Context) (interface{}, error) {
103+
// This is intentionally not grabbing the lock since the calling functions
104+
// (e.g. CreateUser) are claiming it.
105+
106+
if !c.Initialized {
107+
return nil, connutil.ErrNotInitialized
108+
}
109+
110+
if c.cluster != nil {
111+
return c.cluster, nil
112+
}
113+
var err error
114+
var sec gocb.SecurityConfig
115+
var pem []byte
116+
117+
if c.TLS {
118+
pem, err = base64.StdEncoding.DecodeString(c.Base64Pem)
119+
if err != nil {
120+
return nil, errwrap.Wrapf("error decoding Base64Pem: {{err}}", err)
121+
}
122+
rootCAs := x509.NewCertPool()
123+
ok := rootCAs.AppendCertsFromPEM([]byte(pem))
124+
if !ok {
125+
return nil, fmt.Errorf("failed to parse root certificate")
126+
}
127+
sec = gocb.SecurityConfig{
128+
TLSRootCAs: rootCAs,
129+
TLSSkipVerify: c.InsecureTLS,
130+
}
131+
}
132+
133+
c.cluster, err = gocb.Connect(
134+
c.Hosts,
135+
gocb.ClusterOptions{
136+
Username: c.Username,
137+
Password: c.Password,
138+
SecurityConfig: sec,
139+
})
140+
if err != nil {
141+
return nil, errwrap.Wrapf("error in Connection: {{err}}", err)
142+
}
143+
144+
// For databases 6.0 and earlier, we will need to open a `Bucket instance before connecting to any other
145+
// HTTP services such as UserManager.
146+
147+
if c.BucketName != "" {
148+
bucket := c.cluster.Bucket(c.BucketName)
149+
// We wait until the bucket is definitely connected and setup.
150+
err = bucket.WaitUntilReady(computeTimeout(ctx), nil)
151+
if err != nil {
152+
return nil, errwrap.Wrapf("error in Connection waiting for bucket: {{err}}", err)
153+
}
154+
} else {
155+
err = c.cluster.WaitUntilReady(computeTimeout(ctx), nil)
156+
157+
if err != nil {
158+
return nil, errwrap.Wrapf("error in Connection waiting for cluster: {{err}}", err)
159+
}
160+
}
161+
162+
return c.cluster, nil
163+
}
164+
165+
// close terminates the database connection without locking
166+
func (c *couchbaseCapellaDBConnectionProducer) close() error {
167+
if c.cluster != nil {
168+
if err := c.cluster.Close(&gocb.ClusterCloseOptions{}); err != nil {
169+
return err
170+
}
171+
}
172+
173+
c.cluster = nil
174+
return nil
175+
}
176+
177+
// Close terminates the database connection with locking
178+
func (c *couchbaseCapellaDBConnectionProducer) Close() error {
179+
// Don't let anyone read or write the config while we're using it
180+
c.Lock()
181+
defer c.Unlock()
182+
183+
return c.close()
184+
}

0 commit comments

Comments
 (0)