Add x509 Client Auth to MongoDB Database Plugin (#8329)

* Mark deprecated plugins as deprecated

* Add redaction capability to database plugins

* Add x509 client auth

* Update vendored files

* Add integration test for x509 client auth

* Remove redaction logic pending further discussion

* Update vendored files

* Minor updates from code review

* Updated docs with x509 client auth

* Roles are required

* Disable x509 test because it doesn't work in CircleCI

* Add timeouts for container lifetime
This commit is contained in:
Michael Golowka
2020-02-13 15:54:00 -07:00
committed by GitHub
parent 33a7011e99
commit f96f4eebfc
10 changed files with 809 additions and 63 deletions

View File

@@ -112,18 +112,18 @@ func newRegistry() *registry {
"alicloud": logicalAlicloud.Factory, "alicloud": logicalAlicloud.Factory,
"aws": logicalAws.Factory, "aws": logicalAws.Factory,
"azure": logicalAzure.Factory, "azure": logicalAzure.Factory,
"cassandra": logicalCass.Factory, "cassandra": logicalCass.Factory, // Deprecated
"consul": logicalConsul.Factory, "consul": logicalConsul.Factory,
"gcp": logicalGcp.Factory, "gcp": logicalGcp.Factory,
"gcpkms": logicalGcpKms.Factory, "gcpkms": logicalGcpKms.Factory,
"kv": logicalKv.Factory, "kv": logicalKv.Factory,
"mongodb": logicalMongo.Factory, "mongodb": logicalMongo.Factory, // Deprecated
"mongodbatlas": logicalMongoAtlas.Factory, "mongodbatlas": logicalMongoAtlas.Factory,
"mssql": logicalMssql.Factory, "mssql": logicalMssql.Factory, // Deprecated
"mysql": logicalMysql.Factory, "mysql": logicalMysql.Factory, // Deprecated
"nomad": logicalNomad.Factory, "nomad": logicalNomad.Factory,
"pki": logicalPki.Factory, "pki": logicalPki.Factory,
"postgresql": logicalPostgres.Factory, "postgresql": logicalPostgres.Factory, // Deprecated
"rabbitmq": logicalRabbit.Factory, "rabbitmq": logicalRabbit.Factory,
"ssh": logicalSsh.Factory, "ssh": logicalSsh.Factory,
"totp": logicalTotp.Factory, "totp": logicalTotp.Factory,

View File

@@ -54,7 +54,7 @@ func PrepareTestContainerWithDatabase(t *testing.T, version, dbName string) (cle
// exponential backoff-retry // exponential backoff-retry
if err = pool.Retry(func() error { if err = pool.Retry(func() error {
var err error var err error
dialInfo, err := parseMongoURL(retURL) dialInfo, err := ParseMongoURL(retURL)
if err != nil { if err != nil {
return err return err
} }
@@ -75,8 +75,8 @@ func PrepareTestContainerWithDatabase(t *testing.T, version, dbName string) (cle
return return
} }
// parseMongoURL will parse a connection string and return a configured dialer // ParseMongoURL will parse a connection string and return a configured dialer
func parseMongoURL(rawURL string) (*mgo.DialInfo, error) { func ParseMongoURL(rawURL string) (*mgo.DialInfo, error) {
url, err := url.Parse(rawURL) url, err := url.Parse(rawURL)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,12 @@
# MongoDB Tests
The test `TestInit_clientTLS` cannot be run within CircleCI in its current form. This is because [it's not
possible to use volume mounting with the docker executor](https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers-).
Because of this, the test is skipped. Running this locally shouldn't present any issues as long as you have
docker set up to allow volume mounting from this directory:
```sh
go test -v -run Init_clientTLS
```
This may be able to be fixed if we mess with the entrypoint or the command arguments.

View File

@@ -0,0 +1,242 @@
package mongodb
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io/ioutil"
"math/big"
"os"
"strings"
"testing"
"time"
)
type certBuilder struct {
tmpl *x509.Certificate
parentTmpl *x509.Certificate
selfSign bool
parentKey *rsa.PrivateKey
isCA bool
}
type certOpt func(*certBuilder) error
func commonName(cn string) certOpt {
return func(builder *certBuilder) error {
builder.tmpl.Subject.CommonName = cn
return nil
}
}
func parent(parent certificate) certOpt {
return func(builder *certBuilder) error {
builder.parentKey = parent.privKey.privKey
builder.parentTmpl = parent.template
return nil
}
}
func isCA(isCA bool) certOpt {
return func(builder *certBuilder) error {
builder.isCA = isCA
return nil
}
}
func selfSign() certOpt {
return func(builder *certBuilder) error {
builder.selfSign = true
return nil
}
}
func dns(dns ...string) certOpt {
return func(builder *certBuilder) error {
builder.tmpl.DNSNames = dns
return nil
}
}
func newCert(t *testing.T, opts ...certOpt) (cert certificate) {
t.Helper()
builder := certBuilder{
tmpl: &x509.Certificate{
SerialNumber: makeSerial(t),
Subject: pkix.Name{
CommonName: makeCommonName(),
},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(1 * time.Hour),
IsCA: false,
KeyUsage: x509.KeyUsageDigitalSignature |
x509.KeyUsageKeyEncipherment |
x509.KeyUsageKeyAgreement,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
},
}
for _, opt := range opts {
err := opt(&builder)
if err != nil {
t.Fatalf("Failed to set up certificate builder: %s", err)
}
}
key := newPrivateKey(t)
builder.tmpl.SubjectKeyId = getSubjKeyID(t, key.privKey)
tmpl := builder.tmpl
parent := builder.parentTmpl
publicKey := key.privKey.Public()
signingKey := builder.parentKey
if builder.selfSign {
parent = tmpl
signingKey = key.privKey
}
if builder.isCA {
tmpl.IsCA = true
tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign
tmpl.ExtKeyUsage = nil
} else {
tmpl.KeyUsage = x509.KeyUsageDigitalSignature |
x509.KeyUsageKeyEncipherment |
x509.KeyUsageKeyAgreement
tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, parent, publicKey, signingKey)
if err != nil {
t.Fatalf("Unable to generate certificate: %s", err)
}
certPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
tlsCert, err := tls.X509KeyPair(certPem, key.pem)
if err != nil {
t.Fatalf("Unable to parse X509 key pair: %s", err)
}
return certificate{
template: tmpl,
privKey: key,
tlsCert: tlsCert,
rawCert: certBytes,
pem: certPem,
isCA: builder.isCA,
}
}
// ////////////////////////////////////////////////////////////////////////////
// Private Key
// ////////////////////////////////////////////////////////////////////////////
type keyWrapper struct {
privKey *rsa.PrivateKey
pem []byte
}
func newPrivateKey(t *testing.T) (key keyWrapper) {
t.Helper()
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Unable to generate key for cert: %s", err)
}
privKeyPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
},
)
key = keyWrapper{
privKey: privKey,
pem: privKeyPem,
}
return key
}
// ////////////////////////////////////////////////////////////////////////////
// Certificate
// ////////////////////////////////////////////////////////////////////////////
type certificate struct {
privKey keyWrapper
template *x509.Certificate
tlsCert tls.Certificate
rawCert []byte
pem []byte
isCA bool
}
func (cert certificate) CombinedPEM() []byte {
if cert.isCA {
return cert.pem
}
return bytes.Join([][]byte{cert.privKey.pem, cert.pem}, []byte{'\n'})
}
// ////////////////////////////////////////////////////////////////////////////
// Writing to file
// ////////////////////////////////////////////////////////////////////////////
func writeFile(t *testing.T, filename string, data []byte, perms os.FileMode) {
t.Helper()
err := ioutil.WriteFile(filename, data, perms)
if err != nil {
t.Fatalf("Unable to write to file [%s]: %s", filename, err)
}
}
// ////////////////////////////////////////////////////////////////////////////
// Helpers
// ////////////////////////////////////////////////////////////////////////////
func makeSerial(t *testing.T) *big.Int {
t.Helper()
v := &big.Int{}
serialNumberLimit := v.Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
t.Fatalf("Unable to generate serial number: %s", err)
}
return serialNumber
}
// Pulled from sdk/helper/certutil & slightly modified for test usage
func getSubjKeyID(t *testing.T, privateKey crypto.Signer) []byte {
t.Helper()
if privateKey == nil {
t.Fatalf("passed-in private key is nil")
}
marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
t.Fatalf("error marshalling public key: %s", err)
}
subjKeyID := sha1.Sum(marshaledKey)
return subjKeyID[:]
}
func makeCommonName() (cn string) {
return strings.ReplaceAll(time.Now().Format("20060102T150405.000"), ".", "")
}

View File

@@ -2,6 +2,8 @@ package mongodb
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -23,8 +25,12 @@ import (
type mongoDBConnectionProducer struct { type mongoDBConnectionProducer struct {
ConnectionURL string `json:"connection_url" structs:"connection_url" mapstructure:"connection_url"` ConnectionURL string `json:"connection_url" structs:"connection_url" mapstructure:"connection_url"`
WriteConcern string `json:"write_concern" structs:"write_concern" mapstructure:"write_concern"` WriteConcern string `json:"write_concern" structs:"write_concern" mapstructure:"write_concern"`
Username string `json:"username" structs:"username" mapstructure:"username"`
Password string `json:"password" structs:"password" mapstructure:"password"` Username string `json:"username" structs:"username" mapstructure:"username"`
Password string `json:"password" structs:"password" mapstructure:"password"`
TLSCertificateKeyData []byte `json:"tls_certificate_key" structs:"-" mapstructure:"tls_certificate_key"`
TLSCAData []byte `json:"tls_ca" structs:"-" mapstructure:"tls_ca"`
Initialized bool Initialized bool
RawConfig map[string]interface{} RawConfig map[string]interface{}
@@ -64,58 +70,19 @@ func (c *mongoDBConnectionProducer) Init(ctx context.Context, conf map[string]in
return nil, fmt.Errorf("connection_url cannot be empty") return nil, fmt.Errorf("connection_url cannot be empty")
} }
c.ConnectionURL = dbutil.QueryHelper(c.ConnectionURL, map[string]string{ writeOpts, err := c.getWriteConcern()
"username": c.Username, if err != nil {
"password": c.Password, return nil, err
})
if c.WriteConcern != "" {
input := c.WriteConcern
// Try to base64 decode the input. If successful, consider the decoded
// value as input.
inputBytes, err := base64.StdEncoding.DecodeString(input)
if err == nil {
input = string(inputBytes)
}
concern := &writeConcern{}
err = json.Unmarshal([]byte(input), concern)
if err != nil {
return nil, errwrap.Wrapf("error unmarshalling write_concern: {{err}}", err)
}
// Translate write concern to mongo options
var w writeconcern.Option
switch {
case concern.W != 0:
w = writeconcern.W(concern.W)
case concern.WMode != "":
w = writeconcern.WTagSet(concern.WMode)
default:
w = writeconcern.WMajority()
}
var j writeconcern.Option
switch {
case concern.FSync:
j = writeconcern.J(concern.FSync)
case concern.J:
j = writeconcern.J(concern.J)
default:
j = writeconcern.J(false)
}
writeConcern := writeconcern.New(
w,
j,
writeconcern.WTimeout(time.Duration(concern.WTimeout)*time.Millisecond))
c.clientOptions = &options.ClientOptions{
WriteConcern: writeConcern,
}
} }
authOpts, err := c.getTLSAuth()
if err != nil {
return nil, err
}
c.ConnectionURL = c.getConnectionURL()
c.clientOptions = options.MergeClientOptions(writeOpts, authOpts)
// Set initialized to true at this point since all fields are set, // Set initialized to true at this point since all fields are set,
// and the connection can be established at a later time. // and the connection can be established at a later time.
c.Initialized = true c.Initialized = true
@@ -157,7 +124,8 @@ func (c *mongoDBConnectionProducer) Connection(ctx context.Context) (interface{}
c.clientOptions.SetConnectTimeout(1 * time.Minute) c.clientOptions.SetConnectTimeout(1 * time.Minute)
var err error var err error
c.client, err = mongo.Connect(ctx, c.clientOptions.ApplyURI(c.ConnectionURL)) opts := c.clientOptions.ApplyURI(c.ConnectionURL)
c.client, err = mongo.Connect(ctx, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -187,3 +155,98 @@ func (c *mongoDBConnectionProducer) secretValues() map[string]interface{} {
c.Password: "[password]", c.Password: "[password]",
} }
} }
func (c *mongoDBConnectionProducer) getConnectionURL() (connURL string) {
connURL = dbutil.QueryHelper(c.ConnectionURL, map[string]string{
"username": c.Username,
"password": c.Password,
})
return connURL
}
func (c *mongoDBConnectionProducer) getWriteConcern() (opts *options.ClientOptions, err error) {
if c.WriteConcern == "" {
return nil, nil
}
input := c.WriteConcern
// Try to base64 decode the input. If successful, consider the decoded
// value as input.
inputBytes, err := base64.StdEncoding.DecodeString(input)
if err == nil {
input = string(inputBytes)
}
concern := &writeConcern{}
err = json.Unmarshal([]byte(input), concern)
if err != nil {
return nil, errwrap.Wrapf("error unmarshalling write_concern: {{err}}", err)
}
// Translate write concern to mongo options
var w writeconcern.Option
switch {
case concern.W != 0:
w = writeconcern.W(concern.W)
case concern.WMode != "":
w = writeconcern.WTagSet(concern.WMode)
default:
w = writeconcern.WMajority()
}
var j writeconcern.Option
switch {
case concern.FSync:
j = writeconcern.J(concern.FSync)
case concern.J:
j = writeconcern.J(concern.J)
default:
j = writeconcern.J(false)
}
writeConcern := writeconcern.New(
w,
j,
writeconcern.WTimeout(time.Duration(concern.WTimeout)*time.Millisecond))
opts = options.Client()
opts.SetWriteConcern(writeConcern)
return opts, nil
}
func (c *mongoDBConnectionProducer) getTLSAuth() (opts *options.ClientOptions, err error) {
if len(c.TLSCAData) == 0 && len(c.TLSCertificateKeyData) == 0 {
return nil, nil
}
opts = options.Client()
tlsConfig := &tls.Config{}
if len(c.TLSCAData) > 0 {
tlsConfig.RootCAs = x509.NewCertPool()
ok := tlsConfig.RootCAs.AppendCertsFromPEM(c.TLSCAData)
if !ok {
return nil, fmt.Errorf("failed to append CA to client options")
}
}
if len(c.TLSCertificateKeyData) > 0 {
certificate, err := tls.X509KeyPair(c.TLSCertificateKeyData, c.TLSCertificateKeyData)
if err != nil {
return nil, fmt.Errorf("unable to load tls_certificate_key_data: %w", err)
}
opts.SetAuth(options.Credential{
AuthMechanism: "MONGODB-X509",
Username: c.Username,
})
tlsConfig.Certificates = append(tlsConfig.Certificates, certificate)
}
opts.SetTLSConfig(tlsConfig)
return opts, nil
}

View File

@@ -0,0 +1,303 @@
package mongodb
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
paths "path"
"path/filepath"
"reflect"
"sort"
"testing"
"time"
"github.com/hashicorp/vault/helper/testhelpers/mongodb"
"github.com/ory/dockertest"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"gopkg.in/mgo.v2"
)
func TestInit_clientTLS(t *testing.T) {
t.Skip("Skipping this test because CircleCI can't mount the files we need without further investigation: " +
"https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers-")
// Set up temp directory so we can mount it to the docker container
confDir := makeTempDir(t)
defer os.RemoveAll(confDir)
// Create certificates for Mongo authentication
caCert := newCert(t,
commonName("test certificate authority"),
isCA(true),
selfSign(),
)
serverCert := newCert(t,
commonName("server"),
dns("localhost"),
parent(caCert),
)
clientCert := newCert(t,
commonName("client"),
dns("client"),
parent(caCert),
)
writeFile(t, paths.Join(confDir, "ca.pem"), caCert.CombinedPEM(), 0644)
writeFile(t, paths.Join(confDir, "server.pem"), serverCert.CombinedPEM(), 0644)
writeFile(t, paths.Join(confDir, "client.pem"), clientCert.CombinedPEM(), 0644)
// //////////////////////////////////////////////////////
// Set up Mongo config file
rawConf := `
net:
tls:
mode: preferTLS
certificateKeyFile: /etc/mongo/server.pem
CAFile: /etc/mongo/ca.pem
allowInvalidHostnames: true`
writeFile(t, paths.Join(confDir, "mongod.conf"), []byte(rawConf), 0644)
// //////////////////////////////////////////////////////
// Start Mongo container
retURL, cleanup := startMongoWithTLS(t, "latest", confDir)
defer cleanup()
// //////////////////////////////////////////////////////
// Set up x509 user
mClient := connect(t, retURL)
setUpX509User(t, mClient, clientCert)
// //////////////////////////////////////////////////////
// Test
mongo := new()
conf := map[string]interface{}{
"connection_url": retURL,
"allowed_roles": "*",
"tls_certificate_key": clientCert.CombinedPEM(),
"tls_ca": caCert.pem,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := mongo.Init(ctx, conf, true)
if err != nil {
t.Fatalf("Unable to initialize mongo engine: %s", err)
}
// Initialization complete. The connection was established, but we need to ensure
// that we're connected as the right user
whoamiCmd := map[string]interface{}{
"connectionStatus": 1,
}
client, err := mongo.getConnection(ctx)
if err != nil {
t.Fatalf("Unable to make connection to Mongo: %s", err)
}
result := client.Database("test").RunCommand(ctx, whoamiCmd)
if result.Err() != nil {
t.Fatalf("Unable to connect to Mongo: %s", err)
}
expected := connStatus{
AuthInfo: authInfo{
AuthenticatedUsers: []user{
{
User: fmt.Sprintf("CN=%s", clientCert.template.Subject.CommonName),
DB: "$external",
},
},
AuthenticatedUserRoles: []role{
{
Role: "readWrite",
DB: "test",
},
{
Role: "userAdminAnyDatabase",
DB: "admin",
},
},
},
Ok: 1,
}
// Sort the AuthenticatedUserRoles because Mongo doesn't return them in the same order every time
// Thanks Mongo! /tableflip
sort.Sort(expected.AuthInfo.AuthenticatedUserRoles)
actual := connStatus{}
err = result.Decode(&actual)
if err != nil {
t.Fatalf("Unable to decode connection status: %s", err)
}
sort.Sort(actual.AuthInfo.AuthenticatedUserRoles)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("Actual:%#v\nExpected:\n%#v", actual, expected)
}
}
func makeTempDir(t *testing.T) (confDir string) {
confDir, err := ioutil.TempDir(".", "mongodb-test-data")
if err != nil {
t.Fatalf("Unable to make temp directory: %s", err)
}
// Convert the directory to an absolute path because docker needs it when mounting
confDir, err = filepath.Abs(filepath.Clean(confDir))
if err != nil {
t.Fatalf("Unable to determine where temp directory is on absolute path: %s", err)
}
return confDir
}
func startMongoWithTLS(t *testing.T, version string, confDir string) (retURL string, cleanup func()) {
if os.Getenv("MONGODB_URL") != "" {
return os.Getenv("MONGODB_URL"), func() {}
}
pool, err := dockertest.NewPool("")
if err != nil {
t.Fatalf("Failed to connect to docker: %s", err)
}
pool.MaxWait = 30 * time.Second
containerName := "mongo-unit-test"
// Remove previously running container if it is still running because cleanup failed
err = pool.RemoveContainerByName(containerName)
if err != nil {
t.Fatalf("Unable to remove old running containers: %s", err)
}
runOpts := &dockertest.RunOptions{
Name: containerName,
Repository: "mongo",
Tag: version,
Cmd: []string{"mongod", "--config", "/etc/mongo/mongod.conf"},
// Mount the directory from local filesystem into the container
Mounts: []string{
fmt.Sprintf("%s:/etc/mongo", confDir),
},
}
resource, err := pool.RunWithOptions(runOpts)
if err != nil {
t.Fatalf("Could not start local mongo docker container: %s", err)
}
resource.Expire(30)
cleanup = func() {
err := pool.Purge(resource)
if err != nil {
t.Fatalf("Failed to cleanup local container: %s", err)
}
}
uri := url.URL{
Scheme: "mongodb",
Host: fmt.Sprintf("localhost:%s", resource.GetPort("27017/tcp")),
}
retURL = uri.String()
// exponential backoff-retry
err = pool.Retry(func() error {
var err error
dialInfo, err := mongodb.ParseMongoURL(retURL)
if err != nil {
return err
}
session, err := mgo.DialWithInfo(dialInfo)
if err != nil {
return err
}
defer session.Close()
session.SetSyncTimeout(1 * time.Minute)
session.SetSocketTimeout(1 * time.Minute)
return session.Ping()
})
if err != nil {
cleanup()
t.Fatalf("Could not connect to mongo docker container: %s", err)
}
return retURL, cleanup
}
func connect(t *testing.T, uri string) (client *mongo.Client) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
if err != nil {
t.Fatalf("Unable to make connection to Mongo: %s", err)
}
err = client.Ping(ctx, readpref.Primary())
if err != nil {
t.Fatalf("Failed to ping Mongo server: %s", err)
}
return client
}
func setUpX509User(t *testing.T, client *mongo.Client, cert certificate) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
username := fmt.Sprintf("CN=%s", cert.template.Subject.CommonName)
cmd := &createUserCommand{
Username: username,
Roles: []interface{}{
mongodbRole{
Role: "readWrite",
DB: "test",
},
mongodbRole{
Role: "userAdminAnyDatabase",
DB: "admin",
},
},
}
result := client.Database("$external").RunCommand(ctx, cmd)
err := result.Err()
if err != nil {
t.Fatalf("Failed to create x509 user in database: %s", err)
}
}
type connStatus struct {
AuthInfo authInfo `bson:"authInfo"`
Ok int `bson:"ok"`
}
type authInfo struct {
AuthenticatedUsers []user `bson:"authenticatedUsers"`
AuthenticatedUserRoles roles `bson:"authenticatedUserRoles"`
}
type user struct {
User string `bson:"user"`
DB string `bson:"db"`
}
type role struct {
Role string `bson:"role"`
DB string `bson:"db"`
}
type roles []role
func (r roles) Len() int { return len(r) }
func (r roles) Less(i, j int) bool { return r[i].Role < r[j].Role }
func (r roles) Swap(i, j int) { r[i], r[j] = r[j], r[i] }

View File

@@ -2,7 +2,10 @@ package mongodb
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -233,3 +236,100 @@ func testCreateDBUser(t testing.TB, connURL, db, username, password string) {
t.Fatal(result.Err()) t.Fatal(result.Err())
} }
} }
func TestGetTLSAuth(t *testing.T) {
ca := newCert(t,
commonName("certificate authority"),
isCA(true),
selfSign(),
)
cert := newCert(t,
commonName("test cert"),
parent(ca),
)
type testCase struct {
username string
tlsCAData []byte
tlsKeyData []byte
expectOpts *options.ClientOptions
expectErr bool
}
tests := map[string]testCase{
"no TLS data set": {
expectOpts: nil,
expectErr: false,
},
"bad CA": {
tlsCAData: []byte("foobar"),
expectOpts: nil,
expectErr: true,
},
"bad key": {
tlsKeyData: []byte("foobar"),
expectOpts: nil,
expectErr: true,
},
"good ca": {
tlsCAData: cert.pem,
expectOpts: options.Client().
SetTLSConfig(
&tls.Config{
RootCAs: appendToCertPool(t, x509.NewCertPool(), cert.pem),
},
),
expectErr: false,
},
"good key": {
username: "unittest",
tlsKeyData: cert.CombinedPEM(),
expectOpts: options.Client().
SetTLSConfig(
&tls.Config{
Certificates: []tls.Certificate{cert.tlsCert},
},
).
SetAuth(options.Credential{
AuthMechanism: "MONGODB-X509",
Username: "unittest",
}),
expectErr: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
c := new()
c.Username = test.username
c.TLSCAData = test.tlsCAData
c.TLSCertificateKeyData = test.tlsKeyData
actual, err := c.getTLSAuth()
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
if !reflect.DeepEqual(actual, test.expectOpts) {
t.Fatalf("Actual:\n%#v\nExpected:\n%#v", actual, test.expectOpts)
}
})
}
}
func appendToCertPool(t *testing.T, pool *x509.CertPool, caPem []byte) *x509.CertPool {
t.Helper()
ok := pool.AppendCertsFromPEM(caPem)
if !ok {
t.Fatalf("Unable to append cert to cert pool")
}
return pool
}

View File

@@ -4,7 +4,7 @@ import "go.mongodb.org/mongo-driver/mongo/writeconcern"
type createUserCommand struct { type createUserCommand struct {
Username string `bson:"createUser"` Username string `bson:"createUser"`
Password string `bson:"pwd"` Password string `bson:"pwd,omitempty"`
Roles []interface{} `bson:"roles"` Roles []interface{} `bson:"roles"`
} }

View File

@@ -36,6 +36,11 @@ has a number of parameters to further configure a connection.
map to the values in the [Safe][mgo-safe] struct from the mgo driver. map to the values in the [Safe][mgo-safe] struct from the mgo driver.
- `username` `(string: "")` - The root credential username used in the connection URL. - `username` `(string: "")` - The root credential username used in the connection URL.
- `password` `(string: "")` - The root credential password used in the connection URL. - `password` `(string: "")` - The root credential password used in the connection URL.
- `tls_certificate_key` `(string: "")` - x509 certificate for connecting to the database.
This must be a PEM encoded version of the private key and the certificate combined.
- `tls_ca` `(string: "")` - x509 CA file for validating the certificate presented by the
MongoDB server. Must be PEM encoded.
### Sample Payload ### Sample Payload

View File

@@ -35,7 +35,7 @@ more information about setting up the database secrets engine.
$ vault write database/config/my-mongodb-database \ $ vault write database/config/my-mongodb-database \
plugin_name=mongodb-database-plugin \ plugin_name=mongodb-database-plugin \
allowed_roles="my-role" \ allowed_roles="my-role" \
connection_url="mongodb://{{username}}:{{password}}@mongodb.acme.com:27017/admin?ssl=true" \ connection_url="mongodb://{{username}}:{{password}}@mongodb.acme.com:27017/admin?tls=true" \
username="admin" \ username="admin" \
password="Password!" password="Password!"
``` ```
@@ -71,6 +71,27 @@ the proper permission, it can generate credentials.
username v-root-e2978cd0- username v-root-e2978cd0-
``` ```
## Client x509 Certificate Authentication
This plugin supports using MongoDB's [x509 Client-side Certificate Authentication](https://docs.mongodb.com/manual/core/security-x.509/)
To use this authentication mechanism, configure the plugin:
```text
$ vault write database/config/my-mongodb-database \
plugin_name=mongodb-database-plugin \
allowed_roles="my-role" \
connection_url="mongodb://@mongodb.acme.com:27017/admin" \
tls_certificate_key=@/path/to/client.pem \
tls_ca=@/path/to/client.ca
```
Note: `tls_certificate_key` and `tls_ca` map to [`tlsCertificateKeyFile`](https://docs.mongodb.com/manual/reference/program/mongo/#cmdoption-mongo-tlscertificatekeyfile)
and [`tlsCAFile`](https://docs.mongodb.com/manual/reference/program/mongo/#cmdoption-mongo-tlscafile) configuration options
from MongoDB with the exception that the Vault parameters are the contents of those files, not filenames. As such,
the two options are independent of each other. See the [MongoDB Configuration Options](https://docs.mongodb.com/manual/reference/program/mongo/)
for more information.
## API ## API
The full list of configurable options can be seen in the [MongoDB database The full list of configurable options can be seen in the [MongoDB database