Integrate password policies into RabbitMQ secret engine (#9143)

* Add password policies to RabbitMQ & update docs
* Also updates some parts of the password policies to aid/fix testing
This commit is contained in:
Michael Golowka 2020-06-11 16:08:20 -06:00 committed by GitHub
parent 9cc77b94a8
commit a89f09802d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 262 additions and 79 deletions

View File

@ -2,7 +2,6 @@ package rabbitmq
import (
"context"
"fmt"
"strings"
"sync"
@ -73,18 +72,10 @@ func (b *backend) Client(ctx context.Context, s logical.Storage) (*rabbithole.Cl
b.lock.RUnlock()
// Otherwise, attempt to make connection
entry, err := s.Get(ctx, "config/connection")
connConfig, err := readConfig(ctx, s)
if err != nil {
return nil, err
}
if entry == nil {
return nil, fmt.Errorf("configure the client connection with config/connection first")
}
var connConfig connectionConfig
if err := entry.DecodeJSON(&connConfig); err != nil {
return nil, err
}
b.lock.Lock()
defer b.lock.Unlock()

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/vault/helper/testhelpers/docker"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole"
"github.com/mitchellh/mapstructure"
@ -27,6 +28,8 @@ const (
testTags = "administrator"
testVHosts = `{"/": {"configure": ".*", "write": ".*", "read": ".*"}}`
testVHostTopics = `{"/": {"amq.topic": {"write": ".*", "read": ".*"}}}`
roleName = "web"
)
func prepareRabbitMQTestContainer(t *testing.T) (func(), string, int) {
@ -89,9 +92,9 @@ func TestBackend_basic(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
testAccStepRole(t),
testAccStepReadCreds(t, b, uri, "web"),
testAccStepReadCreds(t, b, uri, roleName),
},
})
@ -111,10 +114,10 @@ func TestBackend_returnsErrs(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
{
Operation: logical.CreateOperation,
Path: "roles/web",
Path: fmt.Sprintf("roles/%s", roleName),
Data: map[string]interface{}{
"tags": testTags,
"vhosts": `{"invalid":{"write": ".*", "read": ".*"}}`,
@ -123,7 +126,7 @@ func TestBackend_returnsErrs(t *testing.T) {
},
{
Operation: logical.ReadOperation,
Path: "creds/web",
Path: fmt.Sprintf("creds/%s", roleName),
ErrorOk: true,
},
},
@ -144,11 +147,35 @@ func TestBackend_roleCrud(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
testAccStepRole(t),
testAccStepReadRole(t, "web", testTags, testVHosts, testVHostTopics),
testAccStepDeleteRole(t, "web"),
testAccStepReadRole(t, "web", "", "", ""),
testAccStepReadRole(t, roleName, testTags, testVHosts, testVHostTopics),
testAccStepDeleteRole(t, roleName),
testAccStepReadRole(t, roleName, "", "", ""),
},
})
}
func TestBackend_roleWithPasswordPolicy(t *testing.T) {
if os.Getenv(logicaltest.TestEnvVar) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar))
return
}
backendConfig := logical.TestBackendConfig()
backendConfig.System.(*logical.StaticSystemView).SetPasswordPolicy("testpolicy", random.DefaultStringGenerator)
b, _ := Factory(context.Background(), backendConfig)
cleanup, uri, _ := prepareRabbitMQTestContainer(t)
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri, "testpolicy"),
testAccStepRole(t),
testAccStepReadCreds(t, b, uri, roleName),
},
})
}
@ -161,7 +188,7 @@ func testAccPreCheckFunc(t *testing.T, uri string) func() {
}
}
func testAccStepConfig(t *testing.T, uri string) logicaltest.TestStep {
func testAccStepConfig(t *testing.T, uri string, passwordPolicy string) logicaltest.TestStep {
username := os.Getenv(envRabbitMQUsername)
if len(username) == 0 {
username = "guest"
@ -175,9 +202,10 @@ func testAccStepConfig(t *testing.T, uri string) logicaltest.TestStep {
Operation: logical.UpdateOperation,
Path: "config/connection",
Data: map[string]interface{}{
"connection_uri": uri,
"username": username,
"password": password,
"connection_uri": uri,
"username": username,
"password": password,
"password_policy": passwordPolicy,
},
}
}
@ -185,7 +213,7 @@ func testAccStepConfig(t *testing.T, uri string) logicaltest.TestStep {
func testAccStepRole(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "roles/web",
Path: fmt.Sprintf("roles/%s", roleName),
Data: map[string]interface{}{
"tags": testTags,
"vhosts": testVHosts,

View File

@ -0,0 +1,14 @@
package rabbitmq
import (
"context"
"github.com/hashicorp/vault/sdk/helper/base62"
)
func (b *backend) generatePassword(ctx context.Context, policyName string) (password string, err error) {
if policyName != "" {
return b.System().GeneratePasswordFromPolicy(ctx, policyName)
}
return base62.Random(36)
}

View File

@ -9,6 +9,10 @@ import (
rabbithole "github.com/michaelklishin/rabbit-hole"
)
const (
storageKey = "config/connection"
)
func pathConfigConnection(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/connection",
@ -30,6 +34,10 @@ func pathConfigConnection(b *backend) *framework.Path {
Default: true,
Description: `If set, connection_uri is verified by actually connecting to the RabbitMQ management API`,
},
"password_policy": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the password policy to use to generate passwords for dynamic credentials.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -57,6 +65,8 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
return logical.ErrorResponse("missing password"), nil
}
passwordPolicy := data.Get("password_policy").(string)
// Don't check the connection_url if verification is disabled
verifyConnection := data.Get("verify_connection").(bool)
if verifyConnection {
@ -73,15 +83,14 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
}
// Store it
entry, err := logical.StorageEntryJSON("config/connection", connectionConfig{
URI: uri,
Username: username,
Password: password,
})
if err != nil {
return nil, err
config := connectionConfig{
URI: uri,
Username: username,
Password: password,
PasswordPolicy: passwordPolicy,
}
if err := req.Storage.Put(ctx, entry); err != nil {
err := writeConfig(ctx, req.Storage, config)
if err != nil {
return nil, err
}
@ -91,6 +100,33 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
return nil, nil
}
func readConfig(ctx context.Context, storage logical.Storage) (connectionConfig, error) {
entry, err := storage.Get(ctx, storageKey)
if err != nil {
return connectionConfig{}, err
}
if entry == nil {
return connectionConfig{}, nil
}
var connConfig connectionConfig
if err := entry.DecodeJSON(&connConfig); err != nil {
return connectionConfig{}, err
}
return connConfig, nil
}
func writeConfig(ctx context.Context, storage logical.Storage, config connectionConfig) error {
entry, err := logical.StorageEntryJSON(storageKey, config)
if err != nil {
return err
}
if err := storage.Put(ctx, entry); err != nil {
return err
}
return nil
}
// connectionConfig contains the information required to make a connection to a RabbitMQ node
type connectionConfig struct {
// URI of the RabbitMQ server
@ -101,6 +137,9 @@ type connectionConfig struct {
// Password for the Username
Password string `json:"password"`
// PasswordPolicy for generating passwords for dynamic credentials
PasswordPolicy string `json:"password_policy"`
}
const pathConfigConnectionHelpSyn = `

View File

@ -53,7 +53,12 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
}
username := fmt.Sprintf("%s-%s", req.DisplayName, uuidVal)
password, err := uuid.GenerateUUID()
config, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, fmt.Errorf("unable to read configuration: %w", err)
}
password, err := b.generatePassword(ctx, config.PasswordPolicy)
if err != nil {
return nil, err
}

View File

@ -37,7 +37,7 @@ var (
AlphaNumericFullSymbolRuneset = []rune(AlphaNumericFullSymbolCharset)
// DefaultStringGenerator has reasonable default rules for generating strings
DefaultStringGenerator = StringGenerator{
DefaultStringGenerator = &StringGenerator{
Length: 20,
Rules: []Rule{
CharsetRule{

View File

@ -106,7 +106,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
type testCase struct {
timeout time.Duration
generator *StringGenerator
rng io.Reader
rng io.Reader
}
tests := map[string]testCase{
@ -121,7 +121,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
},
charset: AlphaNumericShortSymbolRuneset,
},
rng: rand.Reader,
rng: rand.Reader,
},
"impossible rules": {
timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long
@ -134,7 +134,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
},
charset: AlphaNumericShortSymbolRuneset,
},
rng: rand.Reader,
rng: rand.Reader,
},
"bad RNG reader": {
timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long
@ -143,7 +143,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
Rules: []Rule{},
charset: AlphaNumericShortSymbolRuneset,
},
rng: badReader{},
rng: badReader{},
},
"0 length": {
timeout: 10 * time.Millisecond,
@ -157,7 +157,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
},
charset: []rune("abcde"),
},
rng: rand.Reader,
rng: rand.Reader,
},
"-1 length": {
timeout: 10 * time.Millisecond,
@ -171,7 +171,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
},
charset: []rune("abcde"),
},
rng: rand.Reader,
rng: rand.Reader,
},
"no charset": {
timeout: 10 * time.Millisecond,
@ -179,7 +179,7 @@ func TestStringGenerator_Generate_errors(t *testing.T) {
Length: 20,
Rules: []Rule{},
},
rng: rand.Reader,
rng: rand.Reader,
},
}
@ -333,8 +333,8 @@ func TestRandomRunes_errors(t *testing.T) {
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠" +
"Σ",
),
length:20,
rng: rand.Reader,
length: 20,
rng: rand.Reader,
},
"length is zero": {
charset: []rune("abcde"),
@ -372,22 +372,24 @@ func BenchmarkStringGenerator_Generate(b *testing.B) {
}
type testCase struct {
generator StringGenerator
generator *StringGenerator
}
benches := map[string]testCase{
"no rules": {
generator: StringGenerator{
charset: AlphaNumericFullSymbolRuneset,
Rules: []Rule{},
"no restrictions": {
generator: &StringGenerator{
Rules: []Rule{
CharsetRule{
Charset: AlphaNumericFullSymbolRuneset,
},
},
},
},
"default generator": {
generator: DefaultStringGenerator,
},
"large symbol set": {
generator: StringGenerator{
charset: AlphaNumericFullSymbolRuneset,
generator: &StringGenerator{
Rules: []Rule{
CharsetRule{
Charset: LowercaseRuneset,
@ -409,13 +411,14 @@ func BenchmarkStringGenerator_Generate(b *testing.B) {
},
},
"max symbol set": {
generator: StringGenerator{
charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠",
),
generator: &StringGenerator{
Rules: []Rule{
CharsetRule{
Charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠"),
},
CharsetRule{
Charset: LowercaseRuneset,
MinChars: 1,
@ -432,9 +435,11 @@ func BenchmarkStringGenerator_Generate(b *testing.B) {
},
},
"restrictive charset rules": {
generator: StringGenerator{
charset: AlphaNumericShortSymbolRuneset,
generator: &StringGenerator{
Rules: []Rule{
CharsetRule{
Charset: AlphaNumericShortSymbolRuneset,
},
CharsetRule{
Charset: []rune("A"),
MinChars: 1,
@ -551,7 +556,7 @@ func (badReader) Read([]byte) (int, error) {
func TestValidate(t *testing.T) {
type testCase struct {
generator StringGenerator
generator *StringGenerator
expectErr bool
}
@ -561,33 +566,33 @@ func TestValidate(t *testing.T) {
expectErr: false,
},
"length is 0": {
generator: StringGenerator{
generator: &StringGenerator{
Length: 0,
},
expectErr: true,
},
"length is negative": {
generator: StringGenerator{
generator: &StringGenerator{
Length: -2,
},
expectErr: true,
},
"nil charset, no rules": {
generator: StringGenerator{
generator: &StringGenerator{
Length: 5,
charset: nil,
},
expectErr: true,
},
"zero length charset, no rules": {
generator: StringGenerator{
generator: &StringGenerator{
Length: 5,
charset: []rune{},
},
expectErr: true,
},
"rules require password longer than length": {
generator: StringGenerator{
generator: &StringGenerator{
Length: 5,
charset: []rune("abcde"),
Rules: []Rule{
@ -600,7 +605,7 @@ func TestValidate(t *testing.T) {
expectErr: true,
},
"charset has non-printable characters": {
generator: StringGenerator{
generator: &StringGenerator{
Length: 0,
charset: []rune{
'a',

View File

@ -194,3 +194,16 @@ func (d StaticSystemView) GeneratePasswordFromPolicy(ctx context.Context, policy
}
return policy.Generate(ctx, nil)
}
func (d *StaticSystemView) SetPasswordPolicy(name string, policy PasswordPolicy) {
if d.PasswordPolicies == nil {
d.PasswordPolicies = map[string]PasswordPolicy{}
}
d.PasswordPolicies[name] = policy
}
func (d *StaticSystemView) DeletePasswordPolicy(name string) (existed bool) {
_, existed = d.PasswordPolicies[name]
delete(d.PasswordPolicies, name)
return existed
}

View File

@ -3188,7 +3188,7 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
})
t.Run("success", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
policyEntry := storageEntry(t, "testpolicy",

View File

@ -37,7 +37,7 @@ var (
AlphaNumericFullSymbolRuneset = []rune(AlphaNumericFullSymbolCharset)
// DefaultStringGenerator has reasonable default rules for generating strings
DefaultStringGenerator = StringGenerator{
DefaultStringGenerator = &StringGenerator{
Length: 20,
Rules: []Rule{
CharsetRule{

View File

@ -194,3 +194,16 @@ func (d StaticSystemView) GeneratePasswordFromPolicy(ctx context.Context, policy
}
return policy.Generate(ctx, nil)
}
func (d *StaticSystemView) SetPasswordPolicy(name string, policy PasswordPolicy) {
if d.PasswordPolicies == nil {
d.PasswordPolicies = map[string]PasswordPolicy{}
}
d.PasswordPolicies[name] = policy
}
func (d *StaticSystemView) DeletePasswordPolicy(name string) (existed bool) {
_, existed = d.PasswordPolicies[name]
delete(d.PasswordPolicies, name)
return existed
}

View File

@ -26,17 +26,16 @@ RabbitMQ.
### Parameters
- `connection_uri` `(string: <required>)`  Specifies the RabbitMQ connection
URI.
- `connection_uri` `(string: <required>)`  Specifies the RabbitMQ connection URI.
- `username` `(string: <required>)` Specifies the RabbitMQ management
administrator username.
- `username` `(string: <required>)` Specifies the RabbitMQ management administrator username.
- `password` `(string: <required>)`  Specifies the RabbitMQ management
administrator password.
- `password` `(string: <required>)`  Specifies the RabbitMQ management administrator password.
- `verify_connection` `(bool: true)`  Specifies whether to verify connection
URI, username, and password.
- `verify_connection` `(bool: true)`  Specifies whether to verify connection URI, username, and password.
- `password_policy` `(string: "")` - Specifies a [password policy](/docs/concepts/password-policies) to
use when creating dynamic credentials. Defaults to generating an alphanumeric password if not set.
### Sample Payload
@ -44,12 +43,16 @@ RabbitMQ.
{
"connection_uri": "https://...",
"username": "user",
"password": "password"
"password": "password",
"password_policy": "rabbitmq_policy"
}
```
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
@ -57,6 +60,18 @@ $ curl \
--data @payload.json \
http://127.0.0.1:8200/v1/rabbitmq/config/connection
```
</Tab>
<Tab heading="CLI">
```shell-session
$ vault write rabbitmq/config/connection \
connection_uri="http://localhost:8080" \
username="user" \
password="password" \
password_policy="rabbitmq_policy"
```
</Tab>
</Tabs>
## Configure Lease
@ -83,6 +98,9 @@ This endpoint configures the lease settings for generated credentials.
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
@ -90,6 +108,16 @@ $ curl \
--data @payload.json \
http://127.0.0.1:8200/v1/rabbitmq/config/lease
```
</Tab>
<Tab heading="CLI">
```shell-session
$ vault write rabbitmq/config/lease \
ttl=1800 \
max_ttl=3600
```
</Tab>
</Tabs>
## Create Role
@ -124,6 +152,9 @@ This endpoint creates or updates the role definition.
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
@ -131,6 +162,17 @@ $ curl \
--data @payload.json \
http://127.0.0.1:8200/v1/rabbitmq/roles/my-role
```
</Tab>
<Tab heading="CLI">
```shell-session
$ vault write rabbitmq/roles/my-role \
tags="tag1,tag2" \
vhosts="..." \
vhost_topics="..."
```
</Tab>
</Tabs>
## Read Role
@ -147,11 +189,22 @@ This endpoint queries the role definition.
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
http://127.0.0.1:8200/v1/rabbitmq/roles/my-role
```
</Tab>
<Tab heading="CLI">
```shell-session
$ vault read rabbitmq/roles/my-role
```
</Tab>
</Tabs>
### Sample Response
@ -180,12 +233,23 @@ This endpoint deletes the role definition.
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
--request DELETE \
http://127.0.0.1:8200/v1/rabbitmq/roles/my-role
```
</Tab>
<Tab heading="CLI">
```shell-session
vault delete rabbitmq/roles/my-role
```
</Tab>
</Tabs>
## Generate Credentials
@ -203,11 +267,22 @@ role.
### Sample Request
<Tabs>
<Tab heading="cURL">
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
http://127.0.0.1:8200/v1/rabbitmq/creds/my-role
```
</Tab>
<Tab heading="CLI">
```shell-session
$ vault read rabbitmq/creds/my-role
```
</Tab>
</Tabs>
### Sample Response

View File

@ -81,11 +81,11 @@ the proper permission, it can generate credentials.
$ vault read rabbitmq/creds/my-role
Key Value
--- -----
lease_id rabbitmq/creds/my-role/37d70d04-f24d-760a-e06e-b9b21087f0f4
lease_id rabbitmq/creds/my-role/I39Hu8XXOombof4wiK5bKMn9
lease_duration 768h
lease_renewable true
password a98af72b-b6c9-b4b1-fe37-c73a572befed
username token-590f1fe2-1094-a4d6-01a7-9d4ff756a085
password 3yNDBikgQvrkx2VA2zhq5IdSM7IWk1RyMYJr
username root-39669250-3894-8032-c420-3d58483ebfc4
```
Using ACLs, it is possible to restrict using the rabbitmq secrets engine