mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-11 09:51:16 -04:00
Add prefix support to password policies
This adds a 'prefix' field to password policy configuration, allowing generated passwords to include a configurable prefix. This enables secret scanning tools to detect Vault-generated passwords. Fixes #31889
This commit is contained in:
parent
4d1cc6e442
commit
f9fadfc3fa
4 changed files with 123 additions and 1 deletions
|
|
@ -326,7 +326,14 @@ func (d dynamicSystemView) GeneratePasswordFromPolicy(ctx context.Context, polic
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("stored password policy is invalid: %w", err)
|
||||
}
|
||||
return passPolicy.Generate(ctx, rng)
|
||||
password, err := passPolicy.Generate(ctx, rng)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate password: %w", err)
|
||||
}
|
||||
if policyCfg.Prefix != "" {
|
||||
password = policyCfg.Prefix + password
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (d dynamicSystemView) ClusterID(ctx context.Context) (string, error) {
|
||||
|
|
|
|||
|
|
@ -4200,6 +4200,7 @@ func (b *SystemBackend) handlePoliciesDelete(policyType PolicyType) framework.Op
|
|||
type passwordPolicyConfig struct {
|
||||
HCLPolicy string `json:"policy"`
|
||||
EntropySource string `json:"entropy_source,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
}
|
||||
|
||||
func getPasswordPolicyKey(policyName string) string {
|
||||
|
|
@ -4304,6 +4305,7 @@ func (*SystemBackend) handlePoliciesPasswordSet(ctx context.Context, req *logica
|
|||
cfg := passwordPolicyConfig{
|
||||
HCLPolicy: rawPolicy,
|
||||
EntropySource: entropySource,
|
||||
Prefix: data.Get("prefix").(string),
|
||||
}
|
||||
entry, err := logical.StorageEntryJSON(getPasswordPolicyKey(policyName), cfg)
|
||||
if err != nil {
|
||||
|
|
@ -4343,6 +4345,9 @@ func (*SystemBackend) handlePoliciesPasswordGet(ctx context.Context, req *logica
|
|||
if cfg.EntropySource != "" {
|
||||
resp.Data["entropy_source"] = cfg.EntropySource
|
||||
}
|
||||
if cfg.Prefix != "" {
|
||||
resp.Data["prefix"] = cfg.Prefix
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
|
@ -4413,6 +4418,10 @@ func (b *SystemBackend) handlePoliciesPasswordGenerate(ctx context.Context, req
|
|||
fmt.Sprintf("failed to generate password from policy: %s", err))
|
||||
}
|
||||
|
||||
if cfg.Prefix != "" {
|
||||
password = cfg.Prefix + password
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"password": password,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ var passwordPolicySchema = map[string]*framework.FieldSchema{
|
|||
Type: framework.TypeString,
|
||||
Description: "The entropy source for generation",
|
||||
},
|
||||
"prefix": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The prefix to prepend to generated passwords",
|
||||
},
|
||||
}
|
||||
|
||||
func (b *SystemBackend) configPaths() []*framework.Path {
|
||||
|
|
|
|||
|
|
@ -5877,6 +5877,97 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success with prefix", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
policyEntry := storageEntryWithPrefix(t, "testprefix",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
"\tcharset=\"abcdefghij\"\n"+
|
||||
"}", "", "vault.")
|
||||
storage := makeStorage(t, policyEntry)
|
||||
|
||||
inputData := passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testprefix",
|
||||
})
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
req := &logical.Request{
|
||||
Storage: storage,
|
||||
}
|
||||
|
||||
b := &SystemBackend{}
|
||||
|
||||
actualResp, err := b.handlePoliciesPasswordGenerate(ctx, req, inputData)
|
||||
if err != nil {
|
||||
t.Fatalf("no error expected, got: %s", err)
|
||||
}
|
||||
|
||||
assertTrue(t, actualResp != nil, "response is nil")
|
||||
assertTrue(t, actualResp.Data != nil, "expected data, got nil")
|
||||
assertHasKey(t, actualResp.Data, "password", "password key not found in data")
|
||||
password := actualResp.Data["password"].(string)
|
||||
|
||||
if len(password) < 6 || password[:6] != "vault." {
|
||||
t.Fatalf("password %s does not start with prefix 'vault.'", password)
|
||||
}
|
||||
|
||||
passwordWithoutPrefix := password[6:]
|
||||
passwordLength := len([]rune(passwordWithoutPrefix))
|
||||
if passwordLength != 20 {
|
||||
t.Fatalf("password without prefix is %d characters but should be 20", passwordLength)
|
||||
}
|
||||
|
||||
rules := []random.Rule{
|
||||
random.CharsetRule{
|
||||
Charset: []rune("abcdefghij"),
|
||||
MinChars: 20,
|
||||
},
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if !rule.Pass([]rune(passwordWithoutPrefix)) {
|
||||
t.Fatalf("password %s does not have the correct characters after prefix", password)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success empty prefix", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
policyEntry := storageEntryWithPrefix(t, "testemptyprefix",
|
||||
"length = 20\n"+
|
||||
"rule \"charset\" {\n"+
|
||||
"\tcharset=\"abcdefghij\"\n"+
|
||||
"}", "", "")
|
||||
storage := makeStorage(t, policyEntry)
|
||||
|
||||
inputData := passwordPoliciesFieldData(map[string]interface{}{
|
||||
"name": "testemptyprefix",
|
||||
})
|
||||
|
||||
req := &logical.Request{
|
||||
Storage: storage,
|
||||
}
|
||||
|
||||
b := &SystemBackend{}
|
||||
|
||||
actualResp, err := b.handlePoliciesPasswordGenerate(ctx, req, inputData)
|
||||
if err != nil {
|
||||
t.Fatalf("no error expected, got: %s", err)
|
||||
}
|
||||
|
||||
assertTrue(t, actualResp != nil, "response is nil")
|
||||
password := actualResp.Data["password"].(string)
|
||||
|
||||
passwordLength := len([]rune(password))
|
||||
if passwordLength != 20 {
|
||||
t.Fatalf("password is %d characters but should be 20", passwordLength)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertTrue(t *testing.T, pass bool, f string, vals ...interface{}) {
|
||||
|
|
@ -5933,6 +6024,17 @@ func storageEntry(t *testing.T, key string, policy string, entropySource string)
|
|||
}
|
||||
}
|
||||
|
||||
func storageEntryWithPrefix(t *testing.T, key string, policy string, entropySource string, prefix string) *logical.StorageEntry {
|
||||
return &logical.StorageEntry{
|
||||
Key: getPasswordPolicyKey(key),
|
||||
Value: toJson(t, passwordPolicyConfig{
|
||||
HCLPolicy: policy,
|
||||
EntropySource: entropySource,
|
||||
Prefix: prefix,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func makeStorageMap(entries ...*logical.StorageEntry) map[string]*logical.StorageEntry {
|
||||
m := map[string]*logical.StorageEntry{}
|
||||
for _, entry := range entries {
|
||||
|
|
|
|||
Loading…
Reference in a new issue