Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
machine: true
#### TEMPLATE_NOTE: go expects specific checkout path representing url
#### expecting it in the form of
#### /go/src/github.com/circleci/go-tool
#### /go/src/bitbucket.org/circleci/go-tool
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
steps:
- run:
command: |
[ -n "$GO_VERSION" ] || { echo "You must set GO_VERSION"; exit 1; }
# Install Go
curl -sSLO "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
rm -f "go${GO_VERSION}.linux-amd64.tar.gz"
GOPATH="/go"
mkdir $GOPATH 2>/dev/null || { sudo mkdir $GOPATH && sudo chmod 777 $GOPATH; }
echo "export GOPATH='$GOPATH'" >> "$BASH_ENV"
echo "export PATH='$PATH:$GOPATH/bin:/usr/local/go/bin'" >> "$BASH_ENV"

echo "$ go version"
go version
name: Setup Go
working_directory: ~/

- checkout
# specify any bash command here prefixed with `run: `
- run: go get -v -t -d ./...
- run: go test -v ./...
131 changes: 130 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,130 @@
# vault-plugin-database-couchbase
# vault-plugin-database-couchbase

A [Vault](https://www.vaultproject.io) plugin for Couchbase

This project uses the database plugin interface introduced in Vault version 0.7.1.

The plugin supports the generation of static and dynamic user roles and root credential rotation.

## Build

For Linux/AMD64, pre-built binaries can be found at [the releases page](https://releases.hashicorp.com/vault-plugin-database-couchbase/) (built with the Couchbase Go SDK version 2.1.1)

For other platforms, there are not currently pre-built binaries available.

To build this package for any platform you will need to clone this repository and cd into the repo directory and `go build -o couchbase-database-plugin ./couchbase-database-plugin/`. To test `go test` will execute a set of basic tests against against a custom Docker version of Couchbase (fhitchen/vault-couchbase this will be replaced with an customized latest version of Couchbase when the database customization can be directly done from the test suite). If you want to run the tests against an already running couchbase instance, set the environment variable COUCHBASE_HOST before executing. Set VAULT_ACC to execute all of the tests.

## Installation

The Vault plugin system is documented on the [Vault documentation site](https://www.vaultproject.io/docs/internals/plugins.html).

You will need to define a plugin directory using the `plugin_directory` configuration directive, then place the
`vault-plugin-database-couchbase` executable generated above, into the directory.

Sample commands for registering and starting to use the plugin:

```
$ SHA256=$(shasum -a 256 plugins/couchbase-database-plugin | cut -d' ' -f1)

$ vault secrets enable database

$ vault write sys/plugins/catalog/database/couchbase-database-plugin sha256=$SHA256 \
command=couchbase-database-plugin
```
At this stage you are now ready to initialize the plugin to connect to couchbase cluster using unencrypted or encrypted communications.

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.

### Unencrypted plugin initialization
```
$ vault write database/config/insecure-couchbase plugin_name="couchbase-database-plugin" \
hosts="localhost" username="Administrator" password="password" \
bucket_name="default" \ # only needed for pre-6.5.0 clusters
allowed_roles="insecure-couchbase-admin-role,insecure-couchbase-*-bucket-role"

# You should consider rotating the admin password. Note that if you do, the new password will never be made available
# through Vault, so you should create a vault-specific database admin user for this.
$ vault write -force database/rotate-root/insecure-couchbase

```
**Note: If you want to connect the plugin to a couchbase cluster prior to version 6.5.0 you will also have to supply an existing bucket (bucket_name="default") or the command will fail with the error message [TBD]**

### Encrypted plugin initialization

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.
```
$ BASE64PEM=$(curl -X GET http://Administrator:Admin123@127.0.0.1:8091/pools/default/certificate|base64 -w0)

$ vault write database/config/secure-couchbase plugin_name="couchbase-database-plugin" \
hosts="couchbases://localhost" username="Administrator" password="password" \
tls=true base64pem=${BASE64PEM} \
bucket_name="default" \ # only needed for pre-6.5.0 clusters
allowed_roles="secure-couchbase-admin-role,secure-couchbase-*-bucket-role"

# You should consider rotating the admin password. Note that if you do, the new password will never be made available
# through Vault, so you should create a vault-specific database admin user for this.
$ vault write -force database/rotate-root/secure-couchbase
```
### Dynamic Role Creation
When you create roles, you need to provide a JSON string containing the Couchbase RBAC roles which are documented https://docs.couchbase.com/server/6.5/learn/security/roles.html.
```
# if a creation_statement is not provided the user account will default to read only admin, '[{"name":"ro_admin"}]'

$ vault write database/roles/insecure-couchbase-admin-role db_name=insecure-couchbase \
default_ttl="5m" max_ttl="1h" creation_statements='[{"name":"admin"}]'

$ vault write database/roles/insecure-couchbase-default-bucket-role db_name=insecure-couchbase \
default_ttl="5m" max_ttl="1h" creation_statements='[{"name":"bucket_full_access","bucket":"default"}]'
Success! Data written to: database/roles/insecure-couchbase-default-bucket-role
```
To retrieve the credentials for the dynamic accounts
```

$ vault read database/creds/insecure-couchbase-admin-role
Key Value
--- -----
lease_id database/creds/insecure-couchbase-admin-role/KJ7CTmpFni6U6BCDJ14HcmDm
lease_duration 5m
lease_renewable true
password A1a-yCSH5rAh8QAkCzwu
username v-token-insecure-couchbase-admin-role-yA2hgb0tfewf

$ vault read database/creds/insecure-couchbase-default-bucket-role
Key Value
--- -----
lease_id database/creds/insecure-couchbase-default-bucket-role/OzHdfkIZdeY9p8kjdWur512j
lease_duration 5m
lease_renewable true
password A1a-0yTIuO4q0dCvphz1
username v-token-insecure-couchbase-default-bucket-role-iN5

```
### Static Role Creation

In order to use static roles, the user must already exist in the Couchbase 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.
```
* 1 error occurred:
* error setting credentials: rpc error: code = Unknown desc = user not found | {"unique_id":"74f229fd-b3b3-4036-9673-312adae094bb","endpoint":"http://localhost:8091"}
```

```
$ vault write database/static-roles/static-account db_name=insecure-couchbase \
username="vault-edu" rotation_period="5m"
Success! Data written to: database/static-roles/static-account
````
To retrieve the credentials for the vault-edu user
```
$ vault read database/static-creds/static-account
Key Value
--- -----
last_vault_rotation 2020-06-15T14:32:16.682130141-05:00
password A1a-09ApRvglZY1Usdjp
rotation_period 5m
ttl 30s
username vault-edu
```





175 changes: 175 additions & 0 deletions connection_producer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package couchbase

import (
"context"
"crypto/x509"
"encoding/base64"
"fmt"
"strings"
"sync"
"time"

"github.com/couchbase/gocb/v2"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/mitchellh/mapstructure"
)

type couchbaseDBConnectionProducer struct {
PublicKey string `json:"public_key" structs:"public_key" mapstructure:"public_key"`
PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
ProjectID string `json:"project_id" structs:"project_id" mapstructure:"project_id"`
Hosts string `json:"hosts" structs:"hosts" mapstructure:"hosts"`
Port int `json:"port" structs:"port" mapstructure:"port"`
Username string `json:"username" structs:"username" mapstructure:"username"`
Password string `json:"password" structs:"password" mapstructure:"password"`
TLS bool `json:"tls" structs:"tls" mapstructure:"tls"`
Insecure_TLS bool `json:"insecure_tls" structs:"insecure_tls" mapstructure:"insecure_tls"`
Base64Pem string `json:"base64pem" structs:"base64pem" mapstructure:"base64pem"`
Bucket_name string `json:"bucket_name" structs:bucket_name" mapstructure:bucket_name"`

Initialized bool
rawConfig map[string]interface{}
Type string
cluster *gocb.Cluster
sync.Mutex
}

func (c *couchbaseDBConnectionProducer) secretValues() map[string]interface{} {
return map[string]interface{}{
c.Password: "[password]",
c.Username: "[username]",
}
}

func (c *couchbaseDBConnectionProducer) Init(ctx context.Context, config map[string]interface{}, verifyConnection bool) (saveConfig map[string]interface{}, err error) {

c.Lock()
defer c.Unlock()

c.rawConfig = config

err = mapstructure.WeakDecode(config, c)
if err != nil {
return nil, err
}

switch {
case len(c.Hosts) == 0:
return nil, fmt.Errorf("hosts cannot be empty")
case len(c.Username) == 0:
return nil, fmt.Errorf("username cannot be empty")
case len(c.Password) == 0:
return nil, fmt.Errorf("password cannot be empty")
}

if c.TLS {
if len(c.Base64Pem) == 0 {
return nil, fmt.Errorf("base64pem cannot be empty")
}

if !strings.HasPrefix(c.Hosts, "couchbases://") {
return nil, fmt.Errorf("hosts list must start with couchbases:// for TLS connection")
}
}

c.Initialized = true

if verifyConnection {
if _, err := c.Connection(ctx); err != nil {
return nil, errwrap.Wrapf("error verifying connection: {{err}}", err)
}
}

return config, nil
}

func (c *couchbaseDBConnectionProducer) Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) error {
_, err := c.Init(ctx, config, verifyConnection)
return err
}
func (c *couchbaseDBConnectionProducer) Connection(_ context.Context) (interface{}, error) {
// This is intentionally not grabbing the lock since the calling functions (e.g. CreateUser)
// are claiming it. (The locking patterns could be refactored to be more consistent/clear.)

if !c.Initialized {
return nil, connutil.ErrNotInitialized
}

if c.cluster != nil {
return c.cluster, nil
}
var err error
var sec gocb.SecurityConfig
var PEM []byte

if c.TLS {
PEM, err = base64.StdEncoding.DecodeString(c.Base64Pem)
if err != nil {
return nil, errwrap.Wrapf("error decoding Base64Pem: {{err}}", err)
}
rootCAs := x509.NewCertPool()
ok := rootCAs.AppendCertsFromPEM([]byte(PEM))
if !ok {
return nil, fmt.Errorf("Failed to parse root certificate")
}
sec = gocb.SecurityConfig{
TLSRootCAs: rootCAs,
TLSSkipVerify: c.Insecure_TLS,
}
}

c.cluster, err = gocb.Connect(
c.Hosts,
gocb.ClusterOptions{
Username: c.Username,
Password: c.Password,
SecurityConfig: sec,
})
if err != nil {
return nil, errwrap.Wrapf("error in Connection: {{err}}", err)
}

// For databases 6.0 and earlier, we will need to open a `Bucket instance before connecting to any other
// HTTP services such as UserManager.

if c.Bucket_name != "" {
bucket := c.cluster.Bucket(c.Bucket_name)
// We wait until the bucket is definitely connected and setup.
err = bucket.WaitUntilReady(5*time.Second, nil)
if err != nil {
return nil, errwrap.Wrapf("error in Connection waiting for bucket: {{err}}", err)
}
} else {
err = c.cluster.WaitUntilReady(5*time.Second, nil)

if err != nil {
//s := fmt.Sprintf("Error, user %#v, error {{err}}", c)
//return nil, errwrap.Wrapf(s, err)
return nil, errwrap.Wrapf("error in Connection waiting for cluster: {{err}}", err)
}
}

return c.cluster, nil
}

// close terminates the database connection without locking
func (c *couchbaseDBConnectionProducer) close() error {

if c.cluster != nil {
if err := c.cluster.Close(&gocb.ClusterCloseOptions{}); err != nil {
return err
}
}

c.cluster = nil
return nil
}

// Close terminates the database connection with locking
func (c *couchbaseDBConnectionProducer) Close() error {
c.Lock()
defer c.Unlock()

return c.close()
}
21 changes: 21 additions & 0 deletions couchbase-database-plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"log"
"os"

couchbase "github.com/fhitchen/vault-plugin-database-couchbase"
"github.com/hashicorp/vault/api"
)

func main() {
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args[1:])

err := couchbase.Run(apiClientMeta.GetTLSConfig())
if err != nil {
log.Println(err)
os.Exit(1)
}
}
Loading