mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
Backport Allow entropy source to be configured for password generation, including seal ent augmentation into ce/main (#9613)
* Allow entropy source to be configured for password generation, including seal ent augmentation (#9395) * Allow entropy source to be configured for password generation, including seal ent augmentation * Allow entropy source to be configured for password generation, including seal ent augmentation * omit seal on CE * Update vault/logical_system.go Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * changelog * error formatting * add the param to the fielddata * Fix unit tests/failures * fix invalid source test --------- Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Fix tests that require ENT * Sync with ENT * Resync with ENT version --------- Co-authored-by: Scott Miller <smiller@hashicorp.com> Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
This commit is contained in:
parent
a88dedf9ad
commit
cb88546ad2
6 changed files with 159 additions and 50 deletions
4
changelog/_9395.txt
Normal file
4
changelog/_9395.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
```release-note:improvement
|
||||
core (enterprise): Allow setting of an entropy source on password generation
|
||||
policies, and with it the selection of "seal" to use entropy augmentation.
|
||||
```
|
||||
|
|
@ -8,6 +8,7 @@ package vault
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/helper/activationflags"
|
||||
|
|
@ -228,3 +229,16 @@ func (c *Core) ReloadRequestLimiter() {}
|
|||
|
||||
// createSnapshotManager is a no-op on CE.
|
||||
func (c *Core) createSnapshotManager() {}
|
||||
|
||||
func (c *Core) GetConfigurableRNG(source string, defaultSource io.Reader) (io.Reader, error) {
|
||||
var rng io.Reader
|
||||
switch source {
|
||||
case "platform":
|
||||
rng = defaultSource
|
||||
case "":
|
||||
rng = defaultSource
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entropy source: %s", source)
|
||||
}
|
||||
return rng, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package vault
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -321,7 +322,11 @@ func (d dynamicSystemView) GeneratePasswordFromPolicy(ctx context.Context, polic
|
|||
return "", fmt.Errorf("stored password policy is invalid: %w", err)
|
||||
}
|
||||
|
||||
return passPolicy.Generate(ctx, nil)
|
||||
rng, err := d.core.GetConfigurableRNG(policyCfg.EntropySource, rand.Reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stored password policy is invalid: %w", err)
|
||||
}
|
||||
return passPolicy.Generate(ctx, rng)
|
||||
}
|
||||
|
||||
func (d dynamicSystemView) ClusterID(ctx context.Context) (string, error) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package vault
|
|||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
|
|
@ -3651,7 +3652,8 @@ func (b *SystemBackend) handlePoliciesDelete(policyType PolicyType) framework.Op
|
|||
}
|
||||
|
||||
type passwordPolicyConfig struct {
|
||||
HCLPolicy string `json:"policy"`
|
||||
HCLPolicy string `json:"policy"`
|
||||
EntropySource string `json:"entropy_source,omitempty"`
|
||||
}
|
||||
|
||||
func getPasswordPolicyKey(policyName string) string {
|
||||
|
|
@ -3704,6 +3706,15 @@ func (*SystemBackend) handlePoliciesPasswordSet(ctx context.Context, req *logica
|
|||
fmt.Sprintf("passwords must be between %d and %d characters", minPasswordLength, maxPasswordLength))
|
||||
}
|
||||
|
||||
entropySource := data.Get("entropy_source").(string)
|
||||
switch entropySource {
|
||||
case "":
|
||||
case "seal":
|
||||
case "platform":
|
||||
default:
|
||||
return nil, logical.CodedError(http.StatusBadRequest, fmt.Sprintf("unsupported entropy source %s", entropySource))
|
||||
}
|
||||
|
||||
// Attempt to construct a test password from the rules to ensure that the policy isn't impossible
|
||||
var testPassword []rune
|
||||
|
||||
|
|
@ -3745,7 +3756,8 @@ func (*SystemBackend) handlePoliciesPasswordSet(ctx context.Context, req *logica
|
|||
}
|
||||
|
||||
cfg := passwordPolicyConfig{
|
||||
HCLPolicy: rawPolicy,
|
||||
HCLPolicy: rawPolicy,
|
||||
EntropySource: entropySource,
|
||||
}
|
||||
entry, err := logical.StorageEntryJSON(getPasswordPolicyKey(policyName), cfg)
|
||||
if err != nil {
|
||||
|
|
@ -3782,6 +3794,10 @@ func (*SystemBackend) handlePoliciesPasswordGet(ctx context.Context, req *logica
|
|||
},
|
||||
}
|
||||
|
||||
if cfg.EntropySource != "" {
|
||||
resp.Data["entropy_source"] = cfg.EntropySource
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
|
@ -3821,7 +3837,7 @@ func (*SystemBackend) handlePoliciesPasswordDelete(ctx context.Context, req *log
|
|||
}
|
||||
|
||||
// handlePoliciesPasswordGenerate generates a password from the specified password policy
|
||||
func (*SystemBackend) handlePoliciesPasswordGenerate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
func (b *SystemBackend) handlePoliciesPasswordGenerate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
policyName := data.Get("name").(string)
|
||||
if policyName == "" {
|
||||
return nil, logical.CodedError(http.StatusBadRequest, "missing policy name")
|
||||
|
|
@ -3841,7 +3857,11 @@ func (*SystemBackend) handlePoliciesPasswordGenerate(ctx context.Context, req *l
|
|||
"stored password policy configuration failed to parse")
|
||||
}
|
||||
|
||||
password, err := policy.Generate(ctx, nil)
|
||||
rng, err := b.Core.GetConfigurableRNG(cfg.EntropySource, crand.Reader)
|
||||
if err != nil {
|
||||
return nil, logical.CodedError(http.StatusBadRequest, fmt.Sprintf("failed to retrieve rng: %v", err))
|
||||
}
|
||||
password, err := policy.Generate(ctx, rng)
|
||||
if err != nil {
|
||||
return nil, logical.CodedError(http.StatusInternalServerError,
|
||||
fmt.Sprintf("failed to generate password from policy: %s", err))
|
||||
|
|
|
|||
|
|
@ -4112,17 +4112,7 @@ func (b *SystemBackend) policyPaths() []*framework.Path {
|
|||
OperationSuffix: "password-policy",
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The name of the password policy.",
|
||||
},
|
||||
"policy": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The password policy",
|
||||
},
|
||||
},
|
||||
|
||||
Fields: passwordPolicySchema,
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.handlePoliciesPasswordSet,
|
||||
|
|
@ -4144,6 +4134,10 @@ func (b *SystemBackend) policyPaths() []*framework.Path {
|
|||
Type: framework.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
"entropy_source": {
|
||||
Type: framework.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package vault
|
|||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
|
@ -30,6 +31,7 @@ import (
|
|||
"github.com/hashicorp/vault/audit"
|
||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||
"github.com/hashicorp/vault/helper/constants"
|
||||
"github.com/hashicorp/vault/helper/experiments"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
|
|
@ -5144,7 +5146,7 @@ func TestHandlePoliciesPasswordSet(t *testing.T) {
|
|||
expectedStore: makeStorageMap(storageEntry(t, "testpolicy", "length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
},
|
||||
"base64 encoded": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
|
|
@ -5169,7 +5171,47 @@ func TestHandlePoliciesPasswordSet(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
},
|
||||
"invalid entropy source": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testpolicy",
|
||||
"policy": base64Encode(
|
||||
"length = 20\n" +
|
||||
"rule \"charset\" {\n" +
|
||||
" charset=\"abcdefghij\"\n" +
|
||||
"}"),
|
||||
"entropy_source": "bad",
|
||||
}),
|
||||
|
||||
storage: new(logical.InmemStorage),
|
||||
expectErr: true,
|
||||
expectedStore: map[string]*logical.StorageEntry{},
|
||||
},
|
||||
"seal source": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testpolicy",
|
||||
"policy": base64Encode(
|
||||
"length = 20\n" +
|
||||
"rule \"charset\" {\n" +
|
||||
" charset=\"abcdefghij\"\n" +
|
||||
"}"),
|
||||
"entropy_source": "seal",
|
||||
}),
|
||||
|
||||
storage: new(logical.InmemStorage),
|
||||
|
||||
expectedResp: &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPStatusCode: http.StatusNoContent,
|
||||
},
|
||||
},
|
||||
expectedStore: makeStorageMap(storageEntry(t, "testpolicy",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}", "seal")),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -5255,7 +5297,7 @@ func TestHandlePoliciesPasswordGet(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
|
||||
expectedResp: &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
|
|
@ -5270,7 +5312,34 @@ func TestHandlePoliciesPasswordGet(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
},
|
||||
"good value, seal source": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testpolicy",
|
||||
}),
|
||||
|
||||
storage: makeStorage(t, storageEntry(t, "testpolicy",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}", "seal")),
|
||||
|
||||
expectedResp: &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"policy": "length = 20\n" +
|
||||
"rule \"charset\" {\n" +
|
||||
" charset=\"abcdefghij\"\n" +
|
||||
"}",
|
||||
"entropy_source": "seal",
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
expectedStore: makeStorageMap(storageEntry(t, "testpolicy",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}", "seal")),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -5370,7 +5439,7 @@ func TestHandlePoliciesPasswordDelete(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -5578,6 +5647,20 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
}
|
||||
|
||||
tests := map[string]testCase{
|
||||
"success via seal": {
|
||||
expectErr: !constants.IsEnterprise, // Only works on ENT, CE seal is an unknown source
|
||||
timeout: 1 * time.Second, // Timeout immediately
|
||||
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testpolicy",
|
||||
}),
|
||||
|
||||
storage: makeStorage(t, storageEntry(t, "testpolicy",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}", "seal")),
|
||||
},
|
||||
"missing policy name": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{}),
|
||||
|
||||
|
|
@ -5593,8 +5676,7 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
|
||||
storage: new(logical.InmemStorage).FailGet(true),
|
||||
|
||||
expectedResp: nil,
|
||||
expectErr: true,
|
||||
expectErr: true,
|
||||
},
|
||||
"policy does not exist": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
|
|
@ -5603,18 +5685,16 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
|
||||
storage: new(logical.InmemStorage),
|
||||
|
||||
expectedResp: nil,
|
||||
expectErr: true,
|
||||
expectErr: true,
|
||||
},
|
||||
"policy improperly saved": {
|
||||
inputData: passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testpolicy",
|
||||
}),
|
||||
|
||||
storage: makeStorage(t, storageEntry(t, "testpolicy", "badpolicy")),
|
||||
storage: makeStorage(t, storageEntry(t, "testpolicy", "badpolicy", "")),
|
||||
|
||||
expectedResp: nil,
|
||||
expectErr: true,
|
||||
expectErr: true,
|
||||
},
|
||||
"failed to generate": {
|
||||
timeout: 0 * time.Second, // Timeout immediately
|
||||
|
|
@ -5626,10 +5706,9 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")),
|
||||
"}", "")),
|
||||
|
||||
expectedResp: nil,
|
||||
expectErr: true,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -5642,18 +5721,19 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
Storage: test.storage,
|
||||
}
|
||||
|
||||
b := &SystemBackend{}
|
||||
b := &SystemBackend{
|
||||
Core: &Core{
|
||||
secureRandomReader: crand.Reader,
|
||||
},
|
||||
}
|
||||
|
||||
actualResp, err := b.handlePoliciesPasswordGenerate(ctx, req, test.inputData)
|
||||
_, err := b.handlePoliciesPasswordGenerate(ctx, req, test.inputData)
|
||||
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(actualResp, test.expectedResp) {
|
||||
t.Fatalf("Actual response: %#v\nExpected response: %#v", actualResp, test.expectedResp)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -5666,7 +5746,7 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
" charset=\"abcdefghij\"\n"+
|
||||
"}")
|
||||
"}", "")
|
||||
storage := makeStorage(t, policyEntry)
|
||||
|
||||
inputData := passwordPoliciesFieldData(map[string]interface{}{
|
||||
|
|
@ -5751,17 +5831,8 @@ func assertIsString(t *testing.T, val interface{}, f string, vals ...interface{}
|
|||
|
||||
func passwordPoliciesFieldData(raw map[string]interface{}) *framework.FieldData {
|
||||
return &framework.FieldData{
|
||||
Raw: raw,
|
||||
Schema: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The name of the password policy.",
|
||||
},
|
||||
"policy": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The password policy",
|
||||
},
|
||||
},
|
||||
Raw: raw,
|
||||
Schema: passwordPolicySchema,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5779,11 +5850,12 @@ func toJson(t *testing.T, val interface{}) []byte {
|
|||
return b
|
||||
}
|
||||
|
||||
func storageEntry(t *testing.T, key string, policy string) *logical.StorageEntry {
|
||||
func storageEntry(t *testing.T, key string, policy string, entropySource string) *logical.StorageEntry {
|
||||
return &logical.StorageEntry{
|
||||
Key: getPasswordPolicyKey(key),
|
||||
Value: toJson(t, passwordPolicyConfig{
|
||||
HCLPolicy: policy,
|
||||
HCLPolicy: policy,
|
||||
EntropySource: entropySource,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue