Skip to content

Commit e4fc7f1

Browse files
Clouds: autorotation implementation via periodicfunc (#128)
Clouds: autorotation implementation via periodicfunc PR implements autorotation for clouds root passwords via periodicfunc which happens every 1h. All root passwords for all clouds will be rotated, if root_password_ttl was not set on creation - a default 60 days rotation duration will be used. Acceptance tests vault-plugin-secrets-openstack % make functional Running acceptance tests... === RUN TestPlugin === RUN TestPlugin/TestCloudLifecycle === RUN TestPlugin/TestCloudLifecycle/WriteCloud === RUN TestPlugin/TestCloudLifecycle/ReadCloud === RUN TestPlugin/TestCloudLifecycle/ListClouds === RUN TestPlugin/TestCloudLifecycle/ListClouds/method-LIST === PAUSE TestPlugin/TestCloudLifecycle/ListClouds/method-LIST === RUN TestPlugin/TestCloudLifecycle/ListClouds/method-GET === PAUSE TestPlugin/TestCloudLifecycle/ListClouds/method-GET === CONT TestPlugin/TestCloudLifecycle/ListClouds/method-LIST === CONT TestPlugin/TestCloudLifecycle/ListClouds/method-GET === RUN TestPlugin/TestCloudLifecycle/DeleteCloud === RUN TestPlugin/TestCredsLifecycle === RUN TestPlugin/TestCredsLifecycle/user_password === RUN TestPlugin/TestCredsLifecycle/user_domain_id_token === RUN TestPlugin/TestCredsLifecycle/root_token === RUN TestPlugin/TestCredsLifecycle/user_token === RUN TestPlugin/TestInfo === RUN TestPlugin/TestRoleLifecycle === RUN TestPlugin/TestRoleLifecycle/WriteRole === RUN TestPlugin/TestRoleLifecycle/ReadRole === RUN TestPlugin/TestRoleLifecycle/ListRoles === RUN TestPlugin/TestRoleLifecycle/ListRoles/method-LIST === PAUSE TestPlugin/TestRoleLifecycle/ListRoles/method-LIST === RUN TestPlugin/TestRoleLifecycle/ListRoles/method-GET === PAUSE TestPlugin/TestRoleLifecycle/ListRoles/method-GET === CONT TestPlugin/TestRoleLifecycle/ListRoles/method-LIST === CONT TestPlugin/TestRoleLifecycle/ListRoles/method-GET === RUN TestPlugin/TestRoleLifecycle/DeleteRole === RUN TestPlugin/TestRootRotate rotate_test.go:65: Cloud with name default1 was created rotate_test.go:68: Cloud with name xe9o was created plugin_test.go:337: Cloud with name xe9o has been removed plugin_test.go:337: Cloud with name default1 has been removed === RUN TestPlugin/TestStaticCredsLifecycle === RUN TestPlugin/TestStaticCredsLifecycle/user_password === RUN TestPlugin/TestStaticCredsLifecycle/user_token_project_id === RUN TestPlugin/TestStaticCredsLifecycle/user_token_project_name === RUN TestPlugin/TestStaticCredsLifecycle/user_domain_id_token === RUN TestPlugin/TestStaticRoleLifecycle === RUN TestPlugin/TestStaticRoleLifecycle/WriteRole === RUN TestPlugin/TestStaticRoleLifecycle/ReadRole === RUN TestPlugin/TestStaticRoleLifecycle/ListRoles === RUN TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST === PAUSE TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST === RUN TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET === PAUSE TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET === CONT TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST === CONT TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET === RUN TestPlugin/TestStaticRoleLifecycle/DeleteRole --- PASS: TestPlugin (31.87s) --- PASS: TestPlugin/TestCloudLifecycle (0.05s) --- PASS: TestPlugin/TestCloudLifecycle/WriteCloud (0.04s) --- PASS: TestPlugin/TestCloudLifecycle/ReadCloud (0.00s) --- PASS: TestPlugin/TestCloudLifecycle/ListClouds (0.00s) --- PASS: TestPlugin/TestCloudLifecycle/ListClouds/method-LIST (0.00s) --- PASS: TestPlugin/TestCloudLifecycle/ListClouds/method-GET (0.00s) --- PASS: TestPlugin/TestCloudLifecycle/DeleteCloud (0.00s) --- PASS: TestPlugin/TestCredsLifecycle (7.94s) --- PASS: TestPlugin/TestCredsLifecycle/user_password (1.89s) --- PASS: TestPlugin/TestCredsLifecycle/user_domain_id_token (1.98s) --- PASS: TestPlugin/TestCredsLifecycle/root_token (0.82s) --- PASS: TestPlugin/TestCredsLifecycle/user_token (2.35s) --- PASS: TestPlugin/TestInfo (0.00s) --- PASS: TestPlugin/TestRoleLifecycle (0.59s) --- PASS: TestPlugin/TestRoleLifecycle/WriteRole (0.58s) --- PASS: TestPlugin/TestRoleLifecycle/ReadRole (0.00s) --- PASS: TestPlugin/TestRoleLifecycle/ListRoles (0.00s) --- PASS: TestPlugin/TestRoleLifecycle/ListRoles/method-GET (0.00s) --- PASS: TestPlugin/TestRoleLifecycle/ListRoles/method-LIST (0.00s) --- PASS: TestPlugin/TestRoleLifecycle/DeleteRole (0.00s) --- PASS: TestPlugin/TestRootRotate (4.56s) --- PASS: TestPlugin/TestStaticCredsLifecycle (15.54s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_password (3.26s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_token_project_id (3.70s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_token_project_name (3.76s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_domain_id_token (3.77s) --- PASS: TestPlugin/TestStaticRoleLifecycle (2.72s) --- PASS: TestPlugin/TestStaticRoleLifecycle/WriteRole (1.01s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ReadRole (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/DeleteRole (0.00s) PASS ok github.com/opentelekomcloud/vault-plugin-secrets-openstack/acceptance 32.365s Reviewed-by: Anton Sidelnikov Reviewed-by: Aloento
1 parent c8ee9df commit e4fc7f1

File tree

5 files changed

+121
-6
lines changed

5 files changed

+121
-6
lines changed

doc/source/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ will overwrite them.
2222

2323
* `password` `(string: <required>)` - OpenStack password of the root user.
2424

25-
* `root_password_ttl` `(string: <optional>)` - Password rotation period. Default period is six month.
25+
* `root_password_ttl` `(string: <optional>)` - Password rotation period. Default period is 2 month.
2626

2727
* `username_template` `(string: "vault{{random 8 | lowercase}}")` - Template used for usernames
2828
of temporary users. For details on templating syntax please refer to

openstack/backend.go

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package openstack
33
import (
44
"context"
55
"fmt"
6+
"github.com/gophercloud/gophercloud/openstack/identity/v3/users"
7+
"github.com/hashicorp/go-multierror"
68
"github.com/opentelekomcloud/vault-plugin-secrets-openstack/openstack/common"
9+
"net/http"
710
"sync"
811
"time"
912

@@ -32,8 +35,8 @@ type sharedCloud struct {
3235

3336
type backend struct {
3437
*framework.Backend
35-
36-
clouds map[string]*sharedCloud
38+
clouds map[string]*sharedCloud
39+
checkAutoRotateAfter time.Time
3740
}
3841

3942
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
@@ -62,7 +65,8 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
6265
secretToken(b),
6366
secretUser(b),
6467
},
65-
BackendType: logical.TypeLogical,
68+
BackendType: logical.TypeLogical,
69+
PeriodicFunc: b.periodicFunc,
6670
}
6771

6872
if err := b.Setup(ctx, conf); err != nil {
@@ -147,3 +151,82 @@ func (c *sharedCloud) initClient(ctx context.Context, s logical.Storage) error {
147151

148152
return nil
149153
}
154+
155+
func (b *backend) periodicFunc(ctx context.Context, req *logical.Request) error {
156+
// Check for autorotation once an hour to avoid unnecessarily iterating
157+
// over all keys too frequently.
158+
if time.Now().Before(b.checkAutoRotateAfter) {
159+
return nil
160+
}
161+
b.Logger().Debug("periodic func", "rotate-root", "rotation cycle in progress")
162+
b.checkAutoRotateAfter = time.Now().Add(1 * time.Hour)
163+
164+
return b.autoRotateKeys(ctx, req)
165+
}
166+
167+
func (b *backend) autoRotateKeys(ctx context.Context, req *logical.Request) error {
168+
keys, err := req.Storage.List(ctx, "clouds/")
169+
if err != nil {
170+
return err
171+
}
172+
173+
// Collect errors in a multierror to ensure a single failure doesn't prevent
174+
// all keys from being rotated.
175+
var errs *multierror.Error
176+
177+
for _, key := range keys {
178+
cloudEntry := b.getSharedCloud(key)
179+
if cloudEntry == nil {
180+
continue
181+
}
182+
183+
err = b.rotateIfRequired(ctx, req, cloudEntry)
184+
if err != nil {
185+
errs = multierror.Append(errs, err)
186+
}
187+
}
188+
b.Logger().Debug("periodic func", "rotate-root", "rotation cycle complete")
189+
return errs.ErrorOrNil()
190+
}
191+
192+
func (b *backend) rotateIfRequired(ctx context.Context, req *logical.Request, sCloud *sharedCloud) error {
193+
cloudConfig, err := sCloud.getCloudConfig(ctx, req.Storage)
194+
if err != nil {
195+
return err
196+
}
197+
if time.Now().After(cloudConfig.RootPasswordExpirationDate) {
198+
client, err := sCloud.getClient(ctx, req.Storage)
199+
if err != nil {
200+
return logical.CodedError(http.StatusConflict, common.LogHttpError(err).Error())
201+
}
202+
newPassword, err := sCloud.passwords.Generate(ctx)
203+
if err != nil {
204+
return err
205+
}
206+
207+
// make sure we don't use this cloud until the password is changed
208+
sCloud.lock.Lock()
209+
defer sCloud.lock.Unlock()
210+
211+
user, err := tokens.Get(client, client.Token()).ExtractUser()
212+
if err != nil {
213+
return logical.CodedError(http.StatusConflict, common.LogHttpError(err).Error())
214+
}
215+
err = users.ChangePassword(client, user.ID, users.ChangePasswordOpts{
216+
Password: newPassword,
217+
OriginalPassword: cloudConfig.Password,
218+
}).ExtractErr()
219+
if err != nil {
220+
errorMessage := fmt.Sprintf("error changing root password: %s", common.LogHttpError(err).Error())
221+
return logical.CodedError(http.StatusConflict, errorMessage)
222+
}
223+
cloudConfig.Password = newPassword
224+
cloudConfig.RootPasswordExpirationDate = time.Now().Add(cloudConfig.RootPasswordTTL)
225+
226+
if err := cloudConfig.save(ctx, req.Storage); err != nil {
227+
return err
228+
}
229+
b.Logger().Debug("password rotated", "cloud", cloudConfig.Name)
230+
}
231+
return nil
232+
}

openstack/backend_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openstack
33
import (
44
"context"
55
"fmt"
6+
"github.com/hashicorp/vault/sdk/helper/logging"
67
"net/http"
78
"sync"
89
"testing"
@@ -13,6 +14,7 @@ import (
1314
th "github.com/gophercloud/gophercloud/testhelper"
1415
thClient "github.com/gophercloud/gophercloud/testhelper/client"
1516
"github.com/hashicorp/go-hclog"
17+
log "github.com/hashicorp/go-hclog"
1618
"github.com/hashicorp/vault/sdk/framework"
1719
"github.com/hashicorp/vault/sdk/logical"
1820
"github.com/stretchr/testify/assert"
@@ -25,6 +27,8 @@ const (
2527
failVerbPut
2628
failVerbList
2729
failVerbDelete
30+
defaultLeaseTTLHr = 1 * time.Hour
31+
maxLeaseTTLHr = 12 * time.Hour
2832
)
2933

3034
func testBackend(t *testing.T, fvs ...failVerb) (*backend, logical.Storage) {
@@ -165,3 +169,31 @@ func TestSharedCloud_client(t *testing.T) {
165169
assert.NoError(t, err)
166170
})
167171
}
172+
173+
func TestPeriodicFuncNilConfig(t *testing.T) {
174+
th.SetupHTTP()
175+
defer th.TeardownHTTP()
176+
177+
b, _ := testBackend(t)
178+
179+
config := &logical.BackendConfig{
180+
Logger: logging.NewVaultLogger(log.Trace),
181+
System: &logical.StaticSystemView{
182+
DefaultLeaseTTLVal: defaultLeaseTTLHr,
183+
MaxLeaseTTLVal: maxLeaseTTLHr,
184+
},
185+
StorageView: &logical.InmemStorage{},
186+
}
187+
err := b.Setup(context.Background(), config)
188+
if err != nil {
189+
t.Fatalf("unable to create backend: %v", err)
190+
}
191+
192+
err = b.periodicFunc(context.Background(), &logical.Request{
193+
Storage: config.StorageView,
194+
})
195+
196+
if err != nil {
197+
t.Fatalf("periodicFunc error not nil: %v", err)
198+
}
199+
}

openstack/path_cloud.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Configure the root credentials for an OpenStack cloud using the above parameters
2121
pathCloudListHelpDesc = `List existing OpenStack clouds by name.`
2222

2323
DefaultUsernameTemplate = "vault{{random 8 | lowercase}}"
24-
defaultRootPasswordTTL = 4380 * time.Hour
24+
defaultRootPasswordTTL = 1440 * time.Hour
2525
)
2626

2727
func storageCloudKey(name string) string {

openstack/path_cloud_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func TestConfig(t *testing.T) {
221221
"username": "test-username-1",
222222
"user_domain_name": "testUserDomainName",
223223
"username_template": "user-{{ .RoleName }}-{{ random 4 }}",
224-
"root_password_ttl": 15768000,
224+
"root_password_ttl": 5184000,
225225
"password_policy": "",
226226
},
227227
},

0 commit comments

Comments
 (0)