Skip to content

Commit 04bfc90

Browse files
authored
Add the ability to rotate the configured API key (#19)
Add a path, config/rotate-root, to allow the rotation the admin API key used by the secret engine. Closes: #12
1 parent 95f6de1 commit 04bfc90

File tree

9 files changed

+528
-22
lines changed

9 files changed

+528
-22
lines changed

README.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,30 @@ the plugin executable from source.
4545

4646
To configure a role that uses an existing service ID:
4747
```shell script
48-
$ vault write ibmcloud/roles/myRole service_id=ServiceId-123456dbd-de02-4435-86ce-123456789abc
49-
Success! Data written to: ibmcloud/roles/myRole
48+
$ vault write ibmcloud/roles/myRole service_id=ServiceId-123456dbd-de02-4435-86ce-123456789abc
49+
Success! Data written to: ibmcloud/roles/myRole
5050
```
5151

5252
To configure a role that uses existing access groups:
5353
```shell script
54-
$ vault write ibmcloud/roles/myRole access_group_ids=AccessGroupId-43f12338-fc2c-41cd-b4f9-14eff0cbeb47,AccessGroupId-43f12111-fc2c-41cd-b4f9-14eff0cbeb21
55-
Success! Data written to: ibmcloud/roles/myRole
54+
$ vault write ibmcloud/roles/myRole access_group_ids=AccessGroupId-43f12338-fc2c-41cd-b4f9-14eff0cbeb47,AccessGroupId-43f12111-fc2c-41cd-b4f9-14eff0cbeb21
55+
Success! Data written to: ibmcloud/roles/myRole
5656
```
5757
**There is a limit of 10 access groups per role.**
5858

59+
5. (Optional) Rotate the configured API key
60+
61+
The API key provided in the initial configuration can be rotated. This creates a new API key in IBM Cloud, updates the secret engine configuration,
62+
and deletes the currently configured API key from IBM Cloud.
63+
64+
```shell script
65+
$ vault write -f ibmcloud/config/rotate-root
66+
67+
Key Value
68+
--- -----
69+
apikey_id ApiKey-2a3984a6-f855-4c69-893d-491d32228c17
70+
```
71+
5972
## Usage
6073
After the secrets engine is configured and a user/machine has a Vault token with the proper permission,
6174
it can generate credentials.
@@ -164,6 +177,38 @@ $ curl \
164177
}
165178
```
166179

180+
## Rotate Root Credentials
181+
182+
Rotates the IBM Cloud API key used by Vault for this mount. A new key will be generated
183+
for same user or service ID and account as the existing API key. The configuration is updated
184+
and then the old API key is deleted.
185+
186+
The ID of the new API key is returned in the response.
187+
188+
189+
| Method | Path |
190+
|----------|-------------------------------------------------|
191+
| `POST` | `/ibmcloud/config/rotate-root` |
192+
193+
194+
### Sample Request
195+
```shell script
196+
$ curl \
197+
--header "X-Vault-Token: ..." \
198+
--request POST \
199+
https://127.0.0.1:8200/v1/ibmcloud/config/rotate-root
200+
```
201+
202+
### Sample Response
203+
```json
204+
{
205+
"data": {
206+
"api_key_id": "ApiKey-0abbbbbb-21cc-4dcc-a9cc-b59bc15c7aa1"
207+
},
208+
"...": "..."
209+
}
210+
```
211+
167212
## Delete Config
168213
Deletes the previously configured configuration and clears the configured credentials in the plugin.
169214

backend.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ibmcloudsecrets
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"strings"
78
"sync"
89
"time"
@@ -43,6 +44,7 @@ func backend(c *logical.BackendConfig) *ibmCloudSecretBackend {
4344
Paths: framework.PathAppend(
4445
[]*framework.Path{
4546
pathConfig(b),
47+
pathConfigRotateRoot(b),
4648
pathSecretServiceIDKey(b),
4749
},
4850
pathsRoles(b),
@@ -128,6 +130,20 @@ func (b *ibmCloudSecretBackend) getAdminToken(ctx context.Context, s logical.Sto
128130
if resp != nil {
129131
return "", resp.Error()
130132
}
133+
134+
// Verify the configured admin API key is for the same account that is configured for the engine
135+
apiKeyDetails, err := iam.GetAPIKeyDetails(token, config.APIKey)
136+
if err != nil {
137+
b.Logger().Error("error obtaining details about the configured admin API key", "error", err)
138+
return "", err
139+
}
140+
141+
if apiKeyDetails.AccountID != config.Account {
142+
err = fmt.Errorf("error: the account of the configured API key, %s, does not match the account in the configuration: %s", apiKeyDetails.AccountID, config.Account)
143+
b.Logger().Error("error: configuration account mismatch", "error", err)
144+
return "", err
145+
}
146+
131147
b.adminToken = token
132148
b.adminTokenExpiry = adminTokenInfo.Expiry
133149
return b.adminToken, nil

iam_helper.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131
getAccessGroup = "/v2/groups/%s"
3232
v1APIKeys = "/v1/apikeys"
3333
v1APIKeysID = v1APIKeys + "/%s"
34+
v1APIKeyDetails = "/v1/apikeys/details"
3435
identity = "/identity"
3536
identityToken = "/identity/token"
3637
)
@@ -63,6 +64,12 @@ type APIKeyV1Response struct {
6364
ID string `json:"id"`
6465
}
6566

67+
type APIKeyDetailsResponse struct {
68+
ID string `json:"id"`
69+
IAMID string `json:"iam_id"`
70+
AccountID string `json:"account_id"`
71+
}
72+
6673
type iamHelper interface {
6774
ObtainToken(apiKey string) (string, error)
6875
VerifyToken(ctx context.Context, token string) (*tokenInfo, *logical.Response)
@@ -71,8 +78,9 @@ type iamHelper interface {
7178
CreateServiceID(iamToken, accountID, roleName string) (iamID, identifier string, err error)
7279
DeleteServiceID(iamToken, identifier string) error
7380
AddServiceIDToAccessGroup(iamToken, iamID, group string) error
74-
CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error)
81+
CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error)
7582
DeleteAPIKey(iamToken, apiKeyID string) error
83+
GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error)
7684
Init(iamEndpoint string)
7785
Cleanup()
7886
}
@@ -340,12 +348,12 @@ func (h *ibmCloudHelper) AddServiceIDToAccessGroup(iamToken string, iamID string
340348
return nil
341349
}
342350

343-
func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error) {
351+
func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error) {
344352
requestBody, err := json.Marshal(map[string]interface{}{
345-
"name": fmt.Sprintf("vault-generated-%s", roleName),
353+
"name": name,
346354
"iam_id": IAMid,
347355
"account_id": accountID,
348-
"description": fmt.Sprintf("Generated by Vault's secret engine for IBM Cloud credentials using Vault role %s.", roleName),
356+
"description": description,
349357
"store_value": false,
350358
})
351359
if err != nil {
@@ -403,6 +411,33 @@ func (h *ibmCloudHelper) DeleteAPIKey(iamToken, apiKeyID string) error {
403411
return nil
404412
}
405413

414+
func (h *ibmCloudHelper) GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error) {
415+
r, err := http.NewRequest(http.MethodGet, h.getURL(v1APIKeyDetails), nil)
416+
if err != nil {
417+
return nil, errwrap.Wrapf("failed creating http request: {{err}}", err)
418+
}
419+
420+
r.Header.Set("Authorization", "Bearer "+iamToken)
421+
r.Header.Set("IAM-Apikey", apiKeyValue)
422+
r.Header.Set("Content-Type", "application/json")
423+
r.Header.Set("Accept", "application/json")
424+
body, httpStatus, err := httpRequest(h.httpClient, r)
425+
if err != nil {
426+
return nil, err
427+
}
428+
429+
keyDetails := new(APIKeyDetailsResponse)
430+
431+
if err := json.Unmarshal(body, &keyDetails); err != nil {
432+
return nil, err
433+
}
434+
435+
if httpStatus != 200 {
436+
return nil, fmt.Errorf("unexpected http status code: %v with response %v", httpStatus, string(body))
437+
}
438+
return keyDetails, nil
439+
}
440+
406441
func (h *ibmCloudHelper) DeleteServiceID(iamToken, identifier string) error {
407442
r, err := http.NewRequest(http.MethodDelete, h.getURL(serviceIDDetails, identifier), nil)
408443
if err != nil {

mocks_test.go

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

path_config_rotate_root.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package ibmcloudsecrets
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/hashicorp/vault/sdk/framework"
8+
"github.com/hashicorp/vault/sdk/logical"
9+
)
10+
11+
func pathConfigRotateRoot(b *ibmCloudSecretBackend) *framework.Path {
12+
return &framework.Path{
13+
Pattern: "config/rotate-root",
14+
15+
Operations: map[logical.Operation]framework.OperationHandler{
16+
logical.UpdateOperation: &framework.PathOperation{
17+
Callback: b.pathConfigRotateRootWrite,
18+
},
19+
},
20+
21+
HelpSynopsis: pathConfigRotateRootHelpSyn,
22+
HelpDescription: pathConfigRotateRootHelpDesc,
23+
}
24+
}
25+
26+
func (b *ibmCloudSecretBackend) pathConfigRotateRootWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
27+
28+
config, errResp := b.getConfig(ctx, req.Storage)
29+
if errResp != nil {
30+
return errResp, nil
31+
}
32+
33+
if config == nil || config.APIKey == "" {
34+
return nil, errors.New("no API key was set in the configuration")
35+
}
36+
37+
iam, resp := b.getIAMHelper(ctx, req.Storage)
38+
if resp != nil {
39+
b.Logger().Error("failed to retrieve an IAM helper", "error", resp.Error())
40+
return resp, nil
41+
}
42+
43+
adminToken, err := b.getAdminToken(ctx, req.Storage)
44+
if err != nil {
45+
b.Logger().Error("error obtaining the token for the configured API key", "error", err)
46+
return nil, err
47+
}
48+
49+
oldKeyDetails, err := iam.GetAPIKeyDetails(adminToken, config.APIKey)
50+
if err != nil {
51+
b.Logger().Error("error obtaining details about the current API key", "error", err)
52+
return nil, err
53+
}
54+
55+
// with old key, verify account == acount from the config
56+
keyName := "vault-generated-root-credential"
57+
keyDescription := "Generated by Vault's secret engine for IBM Cloud credentials during root key rotation."
58+
59+
// Generate a new service account key
60+
newAPIKey, err := iam.CreateAPIKey(adminToken, oldKeyDetails.IAMID, oldKeyDetails.AccountID, keyName, keyDescription)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
// Update the configuration with the new key
66+
config.APIKey = newAPIKey.APIKey
67+
entry, err := logical.StorageEntryJSON("config", config)
68+
if err != nil {
69+
return nil, err
70+
}
71+
if err := req.Storage.Put(ctx, entry); err != nil {
72+
return nil, err
73+
}
74+
75+
// Reset the backend to pick up the new key
76+
b.reset()
77+
78+
// Delete the old API key
79+
err = iam.DeleteAPIKey(adminToken, oldKeyDetails.ID)
80+
if err != nil {
81+
errResponse := logical.ErrorResponse("error deleting API key %s after successfully rotating the API key to key %s: %s", oldKeyDetails.ID, newAPIKey.ID, err)
82+
return errResponse, err
83+
}
84+
85+
return &logical.Response{
86+
Data: map[string]interface{}{
87+
apiKeyID: newAPIKey.ID,
88+
},
89+
}, nil
90+
}
91+
92+
const pathConfigRotateRootHelpSyn = `
93+
Request to rotate the IBM Cloud credentials used by Vault
94+
`
95+
96+
const pathConfigRotateRootHelpDesc = `
97+
This path attempts to rotate the IBM Cloud API key used by Vault
98+
for this mount. It does this by generating a new key for the user or service ID,
99+
replacing the internal value, and then deleting the old API key.
100+
Note that it does not create a new service ID or user account, only a new
101+
API key on the same IAM ID as the existing key.
102+
`

0 commit comments

Comments
 (0)