Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalidates WAL entry for static role if password policy has changed #56

Merged
merged 2 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import (
)

var (
defaultLeaseTTLVal = time.Hour * 12
maxLeaseTTLVal = time.Hour * 24
defaultLeaseTTLVal = time.Hour * 12
maxLeaseTTLVal = time.Hour * 24
testPasswordPolicy1 = "test_policy_1"
testPasswordPolicy2 = "test_policy_2"
testPasswordFromPolicy1 = "TestPolicy1Password"
testPasswordFromPolicy2 = "TestPolicy2Password"
)

func getBackend(throwsErr bool) (*backend, logical.Storage) {
Expand All @@ -29,6 +33,14 @@ func getBackend(throwsErr bool) (*backend, logical.Storage) {
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
PasswordPolicies: map[string]logical.PasswordGenerator{
testPasswordPolicy1: func() (string, error) {
return testPasswordFromPolicy1, nil
},
testPasswordPolicy2: func() (string, error) {
return testPasswordFromPolicy2, nil
},
},
},
StorageView: &logical.InmemStorage{},
}
Expand Down
25 changes: 19 additions & 6 deletions path_static_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,16 +741,29 @@ func TestWALsDeletedOnRoleDeletion(t *testing.T) {

func configureOpenLDAPMount(t *testing.T, b *backend, storage logical.Storage) {
t.Helper()

configureOpenLDAPMountWithPasswordPolicy(t, b, storage, "")
}

func configureOpenLDAPMountWithPasswordPolicy(t *testing.T, b *backend, storage logical.Storage, policy string) {
t.Helper()

data := map[string]interface{}{
"binddn": "tester",
"bindpass": "pa$$w0rd",
"url": "ldap://138.91.247.105",
"certificate": validCertificate,
}

if policy != "" {
data["password_policy"] = policy
}

resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: configPath,
Storage: storage,
Data: map[string]interface{}{
"binddn": "tester",
"bindpass": "pa$$w0rd",
"url": "ldap://138.91.247.105",
"certificate": validCertificate,
},
Data: data,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
Expand Down
51 changes: 27 additions & 24 deletions rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ func (b *backend) runTicker(ctx context.Context, s logical.Storage) {
// setCredentialsWAL is used to store information in a WAL that can retry a
// credential setting or rotation in the event of partial failure.
type setCredentialsWAL struct {
NewPassword string `json:"new_password"`
RoleName string `json:"role_name"`
Username string `json:"username"`
DN string `json:"dn"`

NewPassword string `json:"new_password"`
RoleName string `json:"role_name"`
Username string `json:"username"`
DN string `json:"dn"`
PasswordPolicy string `json:"password_policy"`
LastVaultRotation time.Time `json:"last_vault_rotation"`

// Private fields which will not be included in json.Marshal/Unmarshal.
Expand Down Expand Up @@ -258,12 +258,13 @@ func (b *backend) findStaticWAL(ctx context.Context, s logical.Storage, id strin

data := wal.Data.(map[string]interface{})
walEntry := setCredentialsWAL{
walID: id,
walCreatedAt: wal.CreatedAt,
NewPassword: data["new_password"].(string),
RoleName: data["role_name"].(string),
Username: data["username"].(string),
DN: data["dn"].(string),
walID: id,
walCreatedAt: wal.CreatedAt,
NewPassword: data["new_password"].(string),
RoleName: data["role_name"].(string),
Username: data["username"].(string),
DN: data["dn"].(string),
PasswordPolicy: data["password_policy"].(string),
}
lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string))
if err != nil {
Expand Down Expand Up @@ -333,21 +334,22 @@ func (b *backend) setStaticAccountPassword(ctx context.Context, s logical.Storag
}

switch {
case wal != nil && wal.NewPassword != "":
newPassword = wal.NewPassword
default:
if wal == nil {
b.Logger().Error("expected role to have WAL, but WAL not found in storage", "role", input.RoleName, "WAL ID", output.WALID)
} else {
b.Logger().Error("expected WAL to have a new password set, but empty", "role", input.RoleName, "WAL ID", output.WALID)
err = framework.DeleteWAL(ctx, s, output.WALID)
if err != nil {
b.Logger().Warn("failed to delete WAL with no new password", "error", err, "WAL ID", output.WALID)
}
case wal == nil:
b.Logger().Error("expected role to have WAL, but WAL not found in storage", "role", input.RoleName, "WAL ID", output.WALID)

// Generate a new WAL entry and credential
output.WALID = ""
case wal.NewPassword != "" && wal.PasswordPolicy != config.PasswordPolicy:
b.Logger().Debug("password policy changed, generating new password", "role", input.RoleName, "WAL ID", output.WALID)
if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil {
b.Logger().Warn("failed to delete WAL", "error", err, "WAL ID", output.WALID)
}
// If there's anything wrong with the WAL in storage, we'll need
// to generate a fresh WAL and password

// Generate a new WAL entry and credential
output.WALID = ""
default:
// Reuse the password from the existing WAL entry
newPassword = wal.NewPassword
}
}

Expand All @@ -362,6 +364,7 @@ func (b *backend) setStaticAccountPassword(ctx context.Context, s logical.Storag
DN: input.Role.StaticAccount.DN,
NewPassword: newPassword,
LastVaultRotation: input.Role.StaticAccount.LastVaultRotation,
PasswordPolicy: config.PasswordPolicy,
})
b.Logger().Debug("wrote WAL", "role", input.RoleName, "WAL ID", output.WALID)
if err != nil {
Expand Down
57 changes: 57 additions & 0 deletions rotation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,63 @@ func TestAutoRotate(t *testing.T) {
})
}

// TestPasswordPolicyModificationInvalidatesWAL tests that modification of the
// password policy set on the config invalidates pre-generated passwords in WAL
// entries. WAL entries are used to roll forward during partial failure, but
// a password policy change should cause the WAL to be discarded and a new
// password to be generated using the updated policy.
func TestPasswordPolicyModificationInvalidatesWAL(t *testing.T) {
ctx := context.Background()
b, storage := getBackend(false)
defer b.Cleanup(ctx)

configureOpenLDAPMountWithPasswordPolicy(t, b, storage, testPasswordPolicy1)
createRole(t, b, storage, "hashicorp")

// Create a WAL entry from a partial failure to rotate
generateWALFromFailedRotation(t, b, storage, "hashicorp")
requireWALs(t, storage, 1)

// The role password should still be the password generated from policy 1
role, err := b.staticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
if role.StaticAccount.Password != testPasswordFromPolicy1 {
t.Fatalf("expected %v, got %v", testPasswordFromPolicy1, role.StaticAccount.Password)
}

// Update the password policy on the configuration
configureOpenLDAPMountWithPasswordPolicy(t, b, storage, testPasswordPolicy2)

// Manually rotate the role. It should not use the password from the WAL entry
// created earlier. Instead, it should result in generation of a new password
// using the updated policy 2.
_, err = b.HandleRequest(ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/hashicorp",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}

// The role password should be the password generated from policy 2
role, err = b.staticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
if role.StaticAccount.Password != testPasswordFromPolicy2 {
t.Fatalf("expected %v, got %v", testPasswordFromPolicy2, role.StaticAccount.Password)
}
if role.StaticAccount.LastPassword != testPasswordFromPolicy1 {
t.Fatalf("expected %v, got %v", testPasswordFromPolicy1, role.StaticAccount.LastPassword)
}

// The WAL entry should be deleted after the successful rotation
requireWALs(t, storage, 0)
}

func TestRollsPasswordForwardsUsingWAL(t *testing.T) {
ctx := context.Background()
b, storage := getBackend(false)
Expand Down