Skip to content

Commit

Permalink
Add the ability to rotate the configured API key (#19)
Browse files Browse the repository at this point in the history
Add a path, config/rotate-root, to allow the rotation
the admin API key used by the secret engine.

Closes: #12
  • Loading branch information
smatzek authored Apr 27, 2022
1 parent 95f6de1 commit 04bfc90
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 22 deletions.
53 changes: 49 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,30 @@ the plugin executable from source.

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

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

5. (Optional) Rotate the configured API key

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,
and deletes the currently configured API key from IBM Cloud.

```shell script
$ vault write -f ibmcloud/config/rotate-root
Key Value
--- -----
apikey_id ApiKey-2a3984a6-f855-4c69-893d-491d32228c17
```

## Usage
After the secrets engine is configured and a user/machine has a Vault token with the proper permission,
it can generate credentials.
Expand Down Expand Up @@ -164,6 +177,38 @@ $ curl \
}
```

## Rotate Root Credentials

Rotates the IBM Cloud API key used by Vault for this mount. A new key will be generated
for same user or service ID and account as the existing API key. The configuration is updated
and then the old API key is deleted.

The ID of the new API key is returned in the response.


| Method | Path |
|----------|-------------------------------------------------|
| `POST` | `/ibmcloud/config/rotate-root` |


### Sample Request
```shell script
$ curl \
--header "X-Vault-Token: ..." \
--request POST \
https://127.0.0.1:8200/v1/ibmcloud/config/rotate-root
```

### Sample Response
```json
{
"data": {
"api_key_id": "ApiKey-0abbbbbb-21cc-4dcc-a9cc-b59bc15c7aa1"
},
"...": "..."
}
```

## Delete Config
Deletes the previously configured configuration and clears the configured credentials in the plugin.

Expand Down
16 changes: 16 additions & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ibmcloudsecrets
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -43,6 +44,7 @@ func backend(c *logical.BackendConfig) *ibmCloudSecretBackend {
Paths: framework.PathAppend(
[]*framework.Path{
pathConfig(b),
pathConfigRotateRoot(b),
pathSecretServiceIDKey(b),
},
pathsRoles(b),
Expand Down Expand Up @@ -128,6 +130,20 @@ func (b *ibmCloudSecretBackend) getAdminToken(ctx context.Context, s logical.Sto
if resp != nil {
return "", resp.Error()
}

// Verify the configured admin API key is for the same account that is configured for the engine
apiKeyDetails, err := iam.GetAPIKeyDetails(token, config.APIKey)
if err != nil {
b.Logger().Error("error obtaining details about the configured admin API key", "error", err)
return "", err
}

if apiKeyDetails.AccountID != config.Account {
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)
b.Logger().Error("error: configuration account mismatch", "error", err)
return "", err
}

b.adminToken = token
b.adminTokenExpiry = adminTokenInfo.Expiry
return b.adminToken, nil
Expand Down
43 changes: 39 additions & 4 deletions iam_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
getAccessGroup = "/v2/groups/%s"
v1APIKeys = "/v1/apikeys"
v1APIKeysID = v1APIKeys + "/%s"
v1APIKeyDetails = "/v1/apikeys/details"
identity = "/identity"
identityToken = "/identity/token"
)
Expand Down Expand Up @@ -63,6 +64,12 @@ type APIKeyV1Response struct {
ID string `json:"id"`
}

type APIKeyDetailsResponse struct {
ID string `json:"id"`
IAMID string `json:"iam_id"`
AccountID string `json:"account_id"`
}

type iamHelper interface {
ObtainToken(apiKey string) (string, error)
VerifyToken(ctx context.Context, token string) (*tokenInfo, *logical.Response)
Expand All @@ -71,8 +78,9 @@ type iamHelper interface {
CreateServiceID(iamToken, accountID, roleName string) (iamID, identifier string, err error)
DeleteServiceID(iamToken, identifier string) error
AddServiceIDToAccessGroup(iamToken, iamID, group string) error
CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error)
CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error)
DeleteAPIKey(iamToken, apiKeyID string) error
GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error)
Init(iamEndpoint string)
Cleanup()
}
Expand Down Expand Up @@ -340,12 +348,12 @@ func (h *ibmCloudHelper) AddServiceIDToAccessGroup(iamToken string, iamID string
return nil
}

func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error) {
func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error) {
requestBody, err := json.Marshal(map[string]interface{}{
"name": fmt.Sprintf("vault-generated-%s", roleName),
"name": name,
"iam_id": IAMid,
"account_id": accountID,
"description": fmt.Sprintf("Generated by Vault's secret engine for IBM Cloud credentials using Vault role %s.", roleName),
"description": description,
"store_value": false,
})
if err != nil {
Expand Down Expand Up @@ -403,6 +411,33 @@ func (h *ibmCloudHelper) DeleteAPIKey(iamToken, apiKeyID string) error {
return nil
}

func (h *ibmCloudHelper) GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error) {
r, err := http.NewRequest(http.MethodGet, h.getURL(v1APIKeyDetails), nil)
if err != nil {
return nil, errwrap.Wrapf("failed creating http request: {{err}}", err)
}

r.Header.Set("Authorization", "Bearer "+iamToken)
r.Header.Set("IAM-Apikey", apiKeyValue)
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
body, httpStatus, err := httpRequest(h.httpClient, r)
if err != nil {
return nil, err
}

keyDetails := new(APIKeyDetailsResponse)

if err := json.Unmarshal(body, &keyDetails); err != nil {
return nil, err
}

if httpStatus != 200 {
return nil, fmt.Errorf("unexpected http status code: %v with response %v", httpStatus, string(body))
}
return keyDetails, nil
}

func (h *ibmCloudHelper) DeleteServiceID(iamToken, identifier string) error {
r, err := http.NewRequest(http.MethodDelete, h.getURL(serviceIDDetails, identifier), nil)
if err != nil {
Expand Down
23 changes: 19 additions & 4 deletions mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions path_config_rotate_root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package ibmcloudsecrets

import (
"context"
"errors"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)

func pathConfigRotateRoot(b *ibmCloudSecretBackend) *framework.Path {
return &framework.Path{
Pattern: "config/rotate-root",

Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathConfigRotateRootWrite,
},
},

HelpSynopsis: pathConfigRotateRootHelpSyn,
HelpDescription: pathConfigRotateRootHelpDesc,
}
}

func (b *ibmCloudSecretBackend) pathConfigRotateRootWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {

config, errResp := b.getConfig(ctx, req.Storage)
if errResp != nil {
return errResp, nil
}

if config == nil || config.APIKey == "" {
return nil, errors.New("no API key was set in the configuration")
}

iam, resp := b.getIAMHelper(ctx, req.Storage)
if resp != nil {
b.Logger().Error("failed to retrieve an IAM helper", "error", resp.Error())
return resp, nil
}

adminToken, err := b.getAdminToken(ctx, req.Storage)
if err != nil {
b.Logger().Error("error obtaining the token for the configured API key", "error", err)
return nil, err
}

oldKeyDetails, err := iam.GetAPIKeyDetails(adminToken, config.APIKey)
if err != nil {
b.Logger().Error("error obtaining details about the current API key", "error", err)
return nil, err
}

// with old key, verify account == acount from the config
keyName := "vault-generated-root-credential"
keyDescription := "Generated by Vault's secret engine for IBM Cloud credentials during root key rotation."

// Generate a new service account key
newAPIKey, err := iam.CreateAPIKey(adminToken, oldKeyDetails.IAMID, oldKeyDetails.AccountID, keyName, keyDescription)
if err != nil {
return nil, err
}

// Update the configuration with the new key
config.APIKey = newAPIKey.APIKey
entry, err := logical.StorageEntryJSON("config", config)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}

// Reset the backend to pick up the new key
b.reset()

// Delete the old API key
err = iam.DeleteAPIKey(adminToken, oldKeyDetails.ID)
if err != nil {
errResponse := logical.ErrorResponse("error deleting API key %s after successfully rotating the API key to key %s: %s", oldKeyDetails.ID, newAPIKey.ID, err)
return errResponse, err
}

return &logical.Response{
Data: map[string]interface{}{
apiKeyID: newAPIKey.ID,
},
}, nil
}

const pathConfigRotateRootHelpSyn = `
Request to rotate the IBM Cloud credentials used by Vault
`

const pathConfigRotateRootHelpDesc = `
This path attempts to rotate the IBM Cloud API key used by Vault
for this mount. It does this by generating a new key for the user or service ID,
replacing the internal value, and then deleting the old API key.
Note that it does not create a new service ID or user account, only a new
API key on the same IAM ID as the existing key.
`
Loading

0 comments on commit 04bfc90

Please sign in to comment.