Enable root user credential rotation in MongoDB (#8540)

* Enable root user credential rotation in MongoDB

This takes its logic from the SetCredentials function with some changes
(ex: it's generating a password rather than taking one as a parameter).

This will error if the username isn't specified in the config. Since
Mongo defaults to unauthorized, this seemed like an easy check to make
to prevent strange behaviors when it tries to rotate the "" user.
This commit is contained in:
Michael Golowka
2020-05-15 11:24:10 -06:00
committed by GitHub
parent 1a1d3a1b9e
commit 2190cccfa3
3 changed files with 174 additions and 14 deletions

View File

@@ -80,7 +80,6 @@ func (c *mongoDBConnectionProducer) Init(ctx context.Context, conf map[string]in
return nil, err return nil, err
} }
c.ConnectionURL = c.getConnectionURL()
c.clientOptions = options.MergeClientOptions(writeOpts, authOpts) 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,
@@ -117,21 +116,30 @@ func (c *mongoDBConnectionProducer) Connection(ctx context.Context) (interface{}
_ = c.client.Disconnect(ctx) _ = c.client.Disconnect(ctx)
} }
if c.clientOptions == nil { connURL := c.getConnectionURL()
c.clientOptions = options.Client() client, err := createClient(ctx, connURL, c.clientOptions)
}
c.clientOptions.SetSocketTimeout(1 * time.Minute)
c.clientOptions.SetConnectTimeout(1 * time.Minute)
var err error
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
} }
c.client = client
return c.client, nil return c.client, nil
} }
func createClient(ctx context.Context, connURL string, clientOptions *options.ClientOptions) (client *mongo.Client, err error) {
if clientOptions == nil {
clientOptions = options.Client()
}
clientOptions.SetSocketTimeout(1 * time.Minute)
clientOptions.SetConnectTimeout(1 * time.Minute)
opts := clientOptions.ApplyURI(connURL)
client, err = mongo.Connect(ctx, opts)
if err != nil {
return nil, err
}
return client, nil
}
// Close terminates the database connection. // Close terminates the database connection.
func (c *mongoDBConnectionProducer) Close() error { func (c *mongoDBConnectionProducer) Close() error {
c.Lock() c.Lock()

View File

@@ -3,7 +3,6 @@ package mongodb
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"strings" "strings"
@@ -155,7 +154,8 @@ func (m *MongoDB) SetCredentials(ctx context.Context, statements dbplugin.Statem
Password: password, Password: password,
} }
cs, err := connstring.Parse(m.ConnectionURL) connURL := m.getConnectionURL()
cs, err := connstring.Parse(connURL)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -212,9 +212,33 @@ func (m *MongoDB) RevokeUser(ctx context.Context, statements dbplugin.Statements
return m.runCommandWithRetry(ctx, db, dropUserCmd) return m.runCommandWithRetry(ctx, db, dropUserCmd)
} }
// RotateRootCredentials is not currently supported on MongoDB // RotateRootCredentials in MongoDB
func (m *MongoDB) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) { func (m *MongoDB) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) {
return nil, errors.New("root credential rotation is not currently implemented in this database secrets engine") // Grab the lock
m.Lock()
defer m.Unlock()
if m.Username == "" {
return m.RawConfig, fmt.Errorf("username not specified for root credentials")
}
password, err := m.GeneratePassword()
if err != nil {
return nil, err
}
changeUserCmd := &updateUserCommand{
Username: m.Username,
Password: password,
}
if err := m.runCommandWithRetry(ctx, "admin", changeUserCmd); err != nil {
return nil, err
}
m.RawConfig["password"] = password
m.Password = password
return m.RawConfig, nil
} }
// runCommandWithRetry runs a command and retries once more if there's a failure // runCommandWithRetry runs a command and retries once more if there's a failure

View File

@@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net/url"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -333,3 +334,130 @@ func appendToCertPool(t *testing.T, pool *x509.CertPool, caPem []byte) *x509.Cer
} }
return pool return pool
} }
func TestMongoDB_RotateRootCredentials(t *testing.T) {
cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
defer cleanup()
// Test to ensure that we can't rotate the root creds if no username has been specified
testCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
db := new()
connDetailsWithoutUsername := map[string]interface{}{
"connection_url": connURL,
}
_, err := db.Init(testCtx, connDetailsWithoutUsername, true)
if err != nil {
t.Fatalf("err: %s", err)
}
// Rotate credentials should fail because no username is specified
cfg, err := db.RotateRootCredentials(testCtx, nil)
if err == nil {
t.Fatalf("successfully rotated root credentials when no username was present")
}
if !reflect.DeepEqual(cfg, connDetailsWithoutUsername) {
t.Fatalf("expected connection details: %#v but were %#v", connDetailsWithoutUsername, cfg)
}
db.Close()
// Reset the database object with new connection details
username := "vault-test-admin"
initialPassword := "myreallysecurepassword"
db = new()
connDetailsWithUsername := map[string]interface{}{
"connection_url": connURL,
"username": username,
"password": initialPassword,
}
_, err = db.Init(testCtx, connDetailsWithUsername, true)
if err != nil {
t.Fatalf("err: %s", err)
}
// Create root user
createUser(t, connURL, username, initialPassword)
initialURL := setUserPassOnURL(t, connURL, username, initialPassword)
// Ensure the initial root user can connect
err = assertConnection(testCtx, initialURL)
if err != nil {
t.Fatalf("%s", err)
}
// Rotate credentials
newCfg, err := db.RotateRootCredentials(testCtx, nil)
if err != nil {
t.Fatalf("unexpected err rotating root credentials: %s", err)
}
// Ensure the initial root user can no longer connect
err = assertConnection(testCtx, initialURL)
if err == nil {
t.Fatalf("connection with initial credentials succeeded when it shouldn't have")
}
// Ensure the new password can connect
newURL := setUserPassOnURL(t, connURL, username, newCfg["password"].(string))
err = assertConnection(testCtx, newURL)
if err != nil {
t.Fatalf("unexpected error pinging client with new credentials: %s", err)
}
}
func createUser(t *testing.T, connURL, username, password string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := createClient(ctx, connURL, nil)
if err != nil {
t.Fatalf("Unable to make initial connection: %s", err)
}
createUserCmd := createUserCommand{
Username: username,
Password: password,
Roles: []interface{}{
"userAdminAnyDatabase",
"dbAdminAnyDatabase",
"readWriteAnyDatabase",
},
}
result := client.Database("admin").RunCommand(ctx, createUserCmd, nil)
err = result.Err()
if err != nil {
t.Fatalf("Unable to create admin user: %s", err)
}
}
func assertConnection(testCtx context.Context, connURL string) error {
// Connect as initial root user and ensure the connection is successful
client, err := createClient(testCtx, connURL, nil)
if err != nil {
return fmt.Errorf("unable to create client connection with initial root user: %w", err)
}
err = client.Ping(testCtx, nil)
if err != nil {
return fmt.Errorf("failed to ping server with initial root user: %w", err)
}
client.Disconnect(testCtx)
return nil
}
func setUserPassOnURL(t *testing.T, connURL, username, password string) string {
t.Helper()
uri, err := url.Parse(connURL)
if err != nil {
t.Fatalf("unable to parse connection URL: %s", err)
}
uri.User = url.UserPassword(username, password)
return uri.String()
}