Skip to content

Commit 76e3c8d

Browse files
Cleanup keys with same SPIRE key id (#5058)
Signed-off-by: Matteo Kamm <[email protected]>
1 parent 5ecbc90 commit 76e3c8d

File tree

4 files changed

+213
-70
lines changed

4 files changed

+213
-70
lines changed

Diff for: pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go

+83-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
10+
"github.com/andres-erbsen/clock"
1011
"github.com/gofrs/uuid/v5"
1112
"github.com/hashicorp/go-hclog"
1213
"github.com/hashicorp/hcl"
@@ -18,6 +19,7 @@ import (
1819
"google.golang.org/grpc/status"
1920
"os"
2021
"sync"
22+
"time"
2123
)
2224

2325
const (
@@ -41,8 +43,10 @@ type keyEntry struct {
4143
}
4244

4345
type pluginHooks struct {
46+
clk clock.Clock
4447
// Used for testing only.
45-
lookupEnv func(string) (string, bool)
48+
lookupEnv func(string) (string, bool)
49+
scheduleDeleteSignal chan error
4650
}
4751

4852
// Config provides configuration context for the plugin.
@@ -134,6 +138,9 @@ type Plugin struct {
134138
cc *ClientConfig
135139
vc *Client
136140

141+
scheduleDelete chan string
142+
cancelTasks context.CancelFunc
143+
137144
hooks pluginHooks
138145
}
139146

@@ -148,7 +155,9 @@ func newPlugin() *Plugin {
148155
entries: make(map[string]keyEntry),
149156
hooks: pluginHooks{
150157
lookupEnv: os.LookupEnv,
158+
clk: clock.New(),
151159
},
160+
scheduleDelete: make(chan string, 120),
152161
}
153162
}
154163

@@ -214,6 +223,15 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest)
214223

215224
p.setCache(keyEntries)
216225

226+
// Cancel previous tasks in case of re-configure.
227+
if p.cancelTasks != nil {
228+
p.cancelTasks()
229+
}
230+
231+
// start tasks
232+
ctx, p.cancelTasks = context.WithCancel(context.Background())
233+
go p.scheduleDeleteTask(ctx)
234+
217235
return &configv1.ConfigureResponse{}, nil
218236
}
219237

@@ -301,6 +319,61 @@ func (p *Plugin) getEnvOrDefault(envKey, fallback string) string {
301319
return fallback
302320
}
303321

322+
// scheduleDeleteTask is a long-running task that deletes keys that are stale
323+
func (p *Plugin) scheduleDeleteTask(ctx context.Context) {
324+
backoffMin := 1 * time.Second
325+
backoffMax := 60 * time.Second
326+
backoff := backoffMin
327+
328+
for {
329+
select {
330+
case <-ctx.Done():
331+
return
332+
case keyID := <-p.scheduleDelete:
333+
log := p.logger.With("key_id", keyID)
334+
335+
if p.vc == nil {
336+
err := p.genVaultClient()
337+
if err != nil {
338+
log.Error("Failed to generate vault client", "reason", err)
339+
p.notifyDelete(err)
340+
// TODO: Should we re-enqueue here?
341+
}
342+
}
343+
344+
err := p.vc.DeleteKey(ctx, keyID)
345+
346+
if err == nil {
347+
log.Debug("Key deleted")
348+
backoff = backoffMin
349+
p.notifyDelete(nil)
350+
continue
351+
}
352+
353+
// For any other error, log it and re-enqueue the key for deletion as it might be a recoverable error
354+
log.Error("It was not possible to schedule key for deletion. Trying to re-enqueue it for deletion", "reason", err)
355+
356+
select {
357+
case p.scheduleDelete <- keyID:
358+
log.Debug("Key re-enqueued for deletion")
359+
default:
360+
log.Error("Failed to re-enqueue key for deletion")
361+
}
362+
363+
p.notifyDelete(nil)
364+
backoff = min(backoff*2, backoffMax)
365+
p.hooks.clk.Sleep(backoff)
366+
}
367+
}
368+
}
369+
370+
// Used for testing only
371+
func (p *Plugin) notifyDelete(err error) {
372+
if p.hooks.scheduleDeleteSignal != nil {
373+
p.hooks.scheduleDeleteSignal <- err
374+
}
375+
}
376+
304377
func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyRequest) (*keymanagerv1.GenerateKeyResponse, error) {
305378
if req.KeyId == "" {
306379
return nil, status.Error(codes.InvalidArgument, "key id is required")
@@ -318,6 +391,15 @@ func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyR
318391
return nil, err
319392
}
320393

394+
if keyEntry, ok := p.getKeyEntry(spireKeyID); ok {
395+
select {
396+
case p.scheduleDelete <- keyEntry.KeyName:
397+
p.logger.Debug("Key enqueued for deletion", "key_name", keyEntry.KeyName)
398+
default:
399+
p.logger.Error("Failed to enqueue key for deletion", "key_name", keyEntry.KeyName)
400+
}
401+
}
402+
321403
p.setKeyEntry(spireKeyID, *newKeyEntry)
322404

323405
return &keymanagerv1.GenerateKeyResponse{

Diff for: pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,22 @@ func TestPluginGenerateKey(t *testing.T) {
472472
expectCode: codes.Internal,
473473
expectMsgPrefix: "keymanager(hashicorp_vault): unable to decode PEM key",
474474
},
475+
{
476+
name: "Generate key with existing SPIRE key id",
477+
id: "x509-CA-A",
478+
keyType: keymanager.ECP256,
479+
config: successfulConfig,
480+
authMethod: TOKEN,
481+
fakeServer: func() *FakeVaultServerConfig {
482+
fakeServer := setupSuccessFakeVaultServer("test-transit")
483+
fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse)
484+
fakeServer.CertAuthResponse = []byte{}
485+
fakeServer.AppRoleAuthResponse = []byte{}
486+
fakeServer.GetKeysResponse = []byte(testGetKeysResponseOneKey)
487+
488+
return fakeServer
489+
},
490+
},
475491
} {
476492
tt := tt
477493
t.Run(tt.name, func(t *testing.T) {
@@ -484,10 +500,12 @@ func TestPluginGenerateKey(t *testing.T) {
484500
defer s.Close()
485501

486502
p := New()
503+
487504
options := []plugintest.Option{
488505
plugintest.CaptureConfigureError(&err),
489506
plugintest.CoreConfig(catalog.CoreConfig{TrustDomain: spiffeid.RequireTrustDomainFromString("example.org")}),
490507
}
508+
491509
if tt.config != nil {
492510
tt.config.KeyIdentifierFile = createKeyIdentifierFile(t)
493511
tt.config.VaultAddr = fmt.Sprintf("https://%s", addr)
@@ -747,13 +765,19 @@ func setupSuccessFakeVaultServer(transitEnginePath string) *FakeVaultServerConfi
747765
fakeVaultServer.K8sAuthReqEndpoint = "/v1/auth/test-k8s-auth/login"
748766
fakeVaultServer.K8sAuthResponse = []byte(testK8sAuthResponse)
749767

768+
fakeVaultServer.LookupSelfResponseCode = 200
750769
fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponse)
751770
fakeVaultServer.LookupSelfReqEndpoint = "GET /v1/auth/token/lookup-self"
752-
fakeVaultServer.LookupSelfResponseCode = 200
753771

754772
fakeVaultServer.CreateKeyResponseCode = 200
755773
fakeVaultServer.CreateKeyReqEndpoint = fmt.Sprintf("PUT /v1/%s/keys/{id}", transitEnginePath)
756774

775+
fakeVaultServer.DeleteKeyResponseCode = 204
776+
fakeVaultServer.DeleteKeyReqEndpoint = fmt.Sprintf("DELETE /v1/%s/keys/{id}", transitEnginePath)
777+
778+
fakeVaultServer.UpdateKeyConfigurationResponseCode = 204
779+
fakeVaultServer.UpdateKeyConfigurationReqEndpoint = fmt.Sprintf("PUT /v1/%s/keys/{id}/config", transitEnginePath)
780+
757781
fakeVaultServer.GetKeyResponseCode = 200
758782
fakeVaultServer.GetKeyReqEndpoint = fmt.Sprintf("GET /v1/%s/keys/{id}", transitEnginePath)
759783
fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256)

Diff for: pkg/server/plugin/keymanager/hashicorpvault/vault_client.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,28 @@ func (c *Client) CreateKey(ctx context.Context, keyName string, keyType TransitK
389389
return nil
390390
}
391391

392-
// SignData signs the data using the transit engine key with the provided spire key id.
392+
// DeleteKey deletes a key in the specified transit secret engine
393+
// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#update-key-configuration and https://developer.hashicorp.com/vault/api-docs/secret/transit#delete-key
394+
func (c *Client) DeleteKey(ctx context.Context, keyName string) error {
395+
arguments := map[string]interface{}{
396+
"deletion_allowed": "true",
397+
}
398+
399+
// First, we need to enable deletion of the key. This is disabled by default.
400+
_, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/keys/%s/config", c.clientParams.TransitEnginePath, keyName), arguments)
401+
if err != nil {
402+
return status.Errorf(codes.Internal, "failed to enable deletion of transit engine key: %v", err)
403+
}
404+
405+
_, err = c.vaultClient.Logical().DeleteWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, keyName))
406+
if err != nil {
407+
return status.Errorf(codes.Internal, "failed to delete transit engine key: %v", err)
408+
}
409+
410+
return nil
411+
}
412+
413+
// SignData signs the data using the transit engine key with the key name.
393414
// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#sign-data
394415
func (c *Client) SignData(ctx context.Context, keyName string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) {
395416
encodedData := base64.StdEncoding.EncodeToString(data)

0 commit comments

Comments
 (0)