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:
Vault Automation 2025-11-20 13:45:14 -05:00 committed by GitHub
parent a88dedf9ad
commit cb88546ad2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 159 additions and 50 deletions

4
changelog/_9395.txt Normal file
View 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.
```

View file

@ -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
}

View file

@ -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) {

View file

@ -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))

View file

@ -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,
},
},
}},
},

View file

@ -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,
}),
}
}