Allow cleanup ssh dynamic keys host keys (#18939)

* Add ability to clean up host keys for dynamic keys

This adds a new endpoint, tidy/dynamic-keys that removes any stale host
keys still present on the mount. This does not clean up any pending
dynamic key leases and will not remove these keys from systems with
authorized hosts entries created by Vault.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
Alexander Scheel 2023-02-01 10:09:16 -05:00 committed by GitHub
parent 6672d3753f
commit 5d17f9b142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 1 deletions

View File

@ -47,7 +47,7 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
SealWrapStorage: []string{ SealWrapStorage: []string{
caPrivateKey, caPrivateKey,
caPrivateKeyStoragePath, caPrivateKeyStoragePath,
"keys/", keysStoragePrefix,
}, },
}, },
@ -62,6 +62,7 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
pathSign(&b), pathSign(&b),
pathIssue(&b), pathIssue(&b),
pathFetchPublicKey(&b), pathFetchPublicKey(&b),
pathCleanupKeys(&b),
}, },
Secrets: []*framework.Secret{ Secrets: []*framework.Secret{

View File

@ -23,6 +23,8 @@ import (
vaulthttp "github.com/hashicorp/vault/http" vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault" "github.com/hashicorp/vault/vault"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -2404,3 +2406,59 @@ func testCredsWrite(t *testing.T, roleName string, data map[string]interface{},
}, },
} }
} }
func TestBackend_CleanupDynamicHostKeys(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// Running on a clean mount shouldn't do anything.
cleanRequest := &logical.Request{
Operation: logical.DeleteOperation,
Path: "tidy/dynamic-keys",
Storage: config.StorageView,
}
resp, err := b.HandleRequest(context.Background(), cleanRequest)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "0 of 0")
// Write a bunch of bogus entries.
for i := 0; i < 15; i++ {
data := map[string]interface{}{
"host": "localhost",
"key": "nothing-to-see-here",
}
entry, err := logical.StorageEntryJSON(fmt.Sprintf("%vexample-%v", keysStoragePrefix, i), &data)
require.NoError(t, err)
err = config.StorageView.Put(context.Background(), entry)
require.NoError(t, err)
}
// Should now have 15
resp, err = b.HandleRequest(context.Background(), cleanRequest)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "15 of 15")
// Should have none left.
resp, err = b.HandleRequest(context.Background(), cleanRequest)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "0 of 0")
}

View File

@ -0,0 +1,42 @@
package ssh
import (
"context"
"fmt"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const keysStoragePrefix = "keys/"
func pathCleanupKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy/dynamic-keys",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.handleCleanupKeys,
},
HelpSynopsis: `This endpoint removes the stored host keys used for the removed Dynamic Key feature, if present.`,
HelpDescription: `For more information, refer to the API documentation.`,
}
}
func (b *backend) handleCleanupKeys(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
names, err := req.Storage.List(ctx, keysStoragePrefix)
if err != nil {
return nil, fmt.Errorf("unable to list keys for removal: %w", err)
}
for index, name := range names {
keyPath := keysStoragePrefix + name
if err := req.Storage.Delete(ctx, keyPath); err != nil {
return nil, fmt.Errorf("unable to delete key %v of %v: %w", index+1, len(names), err)
}
}
return &logical.Response{
Data: map[string]interface{}{
"message": fmt.Sprintf("Removed %v of %v host keys.", len(names), len(names)),
},
}, nil
}

3
changelog/18939.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Allow removing SSH host keys from the dynamic keys feature.
```

View File

@ -879,3 +879,48 @@ $ curl \
"auth": null "auth": null
} }
``` ```
## Tidy Host Keys
This endpoint removes all existing host keys from Vault, if any are present.
These keys were used with the Dynamic Keys functionality, which were removed
from this engine.
~> Note: This does not clean up any pending dynamic key leases and will not
remove these keys from systems with authorized hosts entries created by
Vault. That must be done manually by an operator, potentially before the
removal of these host keys if they are necessary to access these
systems.<br /><br />
For a more effective cleanup process, it is suggest to stay on Vault 1.12,
manually revoke all dynamic key leases, wait for this to finish, and then
upgrade to Vault 1.13+.
| Method | Path |
| :------- | :----------------------- |
| `DELETE` | `/ssh/tidy/dynamic-keys` |
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
--request DELETE \
http://127.0.0.1:8200/v1/ssh/issue/my-role
```
### Sample Response
```json
{
"request_id": "",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"message": "Removed 15 of 15 host keys."
},
"wrap_info": null,
"warnings": null,
"auth": null
}
```