From f9fadfc3fa9ec0b20d4114dac6b47d4e43a3eac6 Mon Sep 17 00:00:00 2001 From: Wahaj Ahmed Date: Wed, 13 May 2026 00:25:48 +0200 Subject: [PATCH] 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 --- vault/dynamic_system_view.go | 9 ++- vault/logical_system.go | 9 +++ vault/logical_system_paths.go | 4 ++ vault/logical_system_test.go | 102 ++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 157522cc7a..47dbe47ab9 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -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) { diff --git a/vault/logical_system.go b/vault/logical_system.go index 7e1c3a2bae..db3a04d157 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -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, diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index a71f56e997..d57bcbdadc 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -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 { diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index aabb5dc26d..95c763578b 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -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 {