-
Notifications
You must be signed in to change notification settings - Fork 496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce the gcp_kms
KeyManager plugin
#3410
Conversation
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
conf/server/server_full.conf
Outdated
@@ -267,6 +267,24 @@ plugins { | |||
# } | |||
# } | |||
|
|||
# KeyManager "gcp_kms": A key manager for signing SVIDs which generates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# KeyManager "gcp_kms": A key manager for signing SVIDs which generates | |
# KeyManager "gcp_kms": A key manager for signing SVIDs which generates |
conf/server/server_full.conf
Outdated
# | ||
# key_policy_file: A file path location to a custom IAM Policy (v3) | ||
# in JSON format to be attached to created CryptoKeys. | ||
# key_policy_file = "custom-gcp-kms-policy.json" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file is going to be loaded at start and never updated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the file will be read each time that Configure is called.
# KeyManager "gcp_kms": A key manager for signing SVIDs which generates | ||
# and stores keys in Google Cloud KMS. | ||
# KeyManager "gcp_kms" { | ||
# plugin_data = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in another plugins we provides a way to configure different service account to be used, for example service_account_file
that allows to set a different credentials,
Maybe we can add something like this here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the plugin supports that already. I just need to add the example here.
|
||
The plugin requires to have in the configured key ring the following IAM permissions granted to the authenticated service account: | ||
``` | ||
cloudkms.cryptoKeys.* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what it means .*
we really need all cryptoKeys permissions?
can you instead set the ones we really need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we need all the permissions over cryptoKeys (cloudkms.cryptoKeys.create, cloudkms.cryptoKeys.get, cloudkms.cryptoKeys.getIamPolicy, cloudkms.cryptoKeys.list, cloudkms.cryptoKeys.setIamPolicy, cloudkms.cryptoKeys.update).
} | ||
} | ||
|
||
// scheduleDestroyTask ia a long running task that schedules the destruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// scheduleDestroyTask ia a long running task that schedules the destruction | |
// scheduleDestroyTask is a long running task that schedules the destruction |
// The GetCryptoKeyVersion call failed. Log this and re-enqueue | ||
// the CryptoKey for destruction. Hopefully, this is a | ||
// recoverable error. | ||
log.Error("Could not get the CryptoKeyVersion while trying to schedule it for destruction") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you log the error here?
|
||
for _, e := range keyEntries { | ||
p.entries[e.publicKey.Id] = *e | ||
p.log.Debug("Cloud KMS key loaded", cryptoKeyVersionNameTag, e.cryptoKeyVersion.Name, algorithmTag, e.cryptoKeyVersion.Algorithm) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a general comment, I think log will already have the plugin name as a fields so Cloud KMS is not required as part of the logs.
return "", status.Errorf(codes.Internal, "failed to generate ID for server: %v", err) | ||
} | ||
|
||
err = os.WriteFile(idPath, []byte(id), 0600) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this plugin may works on windows too, is that permission lvl enough there?
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
…ng a new CryptoKey. Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
This is ready to review, but more tests are on the way. |
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
…Cloud KMS API Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some more comments, I still need to take a look to unit tests
conf/server/server_full.conf
Outdated
# key_metadata_file: A file path location where information about | ||
# generated keys will be persisted. | ||
# key_metadata_file = "./file_path" | ||
# | ||
# key_policy_file: A file path location to a custom IAM Policy (v3) | ||
# in JSON format to be attached to created CryptoKeys. | ||
# key_policy_file = "custom-gcp-kms-policy.json" | ||
# | ||
# key_ring: Resource ID of the key ring where the keys managed by this | ||
# plugin reside. | ||
# key_ring = "projects/project/locations/location/keyRings/key-ring" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# key_metadata_file: A file path location where information about | |
# generated keys will be persisted. | |
# key_metadata_file = "./file_path" | |
# | |
# key_policy_file: A file path location to a custom IAM Policy (v3) | |
# in JSON format to be attached to created CryptoKeys. | |
# key_policy_file = "custom-gcp-kms-policy.json" | |
# | |
# key_ring: Resource ID of the key ring where the keys managed by this | |
# plugin reside. | |
# key_ring = "projects/project/locations/location/keyRings/key-ring" | |
# # key_metadata_file: A file path location where information about | |
# # generated keys will be persisted. | |
# # key_metadata_file = "./file_path" | |
# | |
# # key_policy_file: A file path location to a custom IAM Policy (v3) | |
# # in JSON format to be attached to created CryptoKeys. | |
# # key_policy_file = "custom-gcp-kms-policy.json" | |
# | |
# # key_ring: Resource ID of the key ring where the keys managed by this | |
# # plugin reside. | |
# # key_ring = "projects/project/locations/location/keyRings/key-ring" |
| Key | Type | Required | Description | Default | | ||
| --- | ---- | -------- | ----------- | ------- | | ||
| key_policy_file | string | no | A file path location to a custom [IAM Policy (v3)](https://cloud.google.com/pubsub/docs/reference/rpc/google.iam.v1#google.iam.v1.Policy) in JSON format to be attached to created CryptoKeys. | "" | | ||
| key_metadata_file | string | yes | A file path location where key metadata used by the plugin will be persisted. | "" | | ||
| key_ring | string | yes | Resource ID of the key ring where the keys managed by this plugin reside, in the format projects/\*/locations/\*/keyRings/\* | "" | | ||
| service_account_file | string | no | Path to the service account file used to authenticate with the Cloud KMS API. | Value of `GOOGLE_APPLICATION_CREDENTIALS ` environment variable. | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you make this table consistent with our new standards?
@@ -0,0 +1,108 @@ | |||
# Server plugin: KeyManager "gcp_kms" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: we are going to add a md linter, as part of this linter we are going to force the lines to a max of 80 characters, it will be great if you apply that here
### Required permissions | ||
|
||
The plugin requires to have in the configured key ring the following IAM permissions granted to the authenticated service account: | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all code pieces must have a type defined, what do you think to set it as text
?
``` | ||
KeyManager "gcp_kms" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
``` | |
KeyManager "gcp_kms" { | |
```hcl | |
KeyManager "gcp_kms" { |
return fakeCryptoKeyVersion.privateKey.Sign(rand.Reader, digest, opts) | ||
} | ||
|
||
cryptoKeyName := path.Dir(path.Dir(signReq.Name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why double path.Dir is required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need the CryptoKey name here. The signing request comes with the CryptoKeyVersion name, e.g.: projects/project-name/locations/location-name/keyRings/key-ring-name/spire-key-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee-spireKeyID-1/cryptoKeyVersions/1
, so we need to get go up two levels to be at the CryptoKey level, in this case: projects/project-name/locations/location-name/keyRings/key-ring-name/spire-key-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee-spireKeyID-1
.
return nil, status.Error(codes.Internal, "cryptoKey is nil") | ||
} | ||
|
||
it := kf.kmsClient.ListCryptoKeyVersions(ctx, &kmspb.ListCryptoKeyVersionsRequest{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: can you add a comment, about this List will return only latest versions? (we keep only 2 actives for now)
p.entriesMtx.RLock() | ||
defer p.entriesMtx.RUnlock() | ||
|
||
keyEntry, hasKey := p.entries[req.KeyId] | ||
if !hasKey { | ||
return nil, status.Errorf(codes.NotFound, "key %q not found", req.KeyId) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may we unlock as soon as we get the entry?
Sings can be a slow process, and I'm worried to unlocking entries until we finish the signing can result in some unexpected locks,
maybe you can add a getEntry(keyID string) (entry, ok) {}
and add lock there
if err := p.enqueueDestruction(cryptoKeyVersion.Name); err != nil { | ||
p.log.With(cryptoKeyNameTag, cryptoKey.Name).Error("Failed to enqueue CryptoKeyVersion for destruction", reasonTag, err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if we are not able to enqueue destruction but continue here?
we can get into a situation where we failed to enqueue all versions, but at the end we marke cryptoKey as inactive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Calling setInactive after detecting that there are no more CryptoKeyVersions at this point assumed that all versions could be enqueued for destruction, but that may not always be the case if there was an error. Since we already call setInactive if the returned CryptoKey doesn't have enabled CryptoKeyVersions, a safer approach would be to just leave that and remove this one.
defer p.entriesMtx.RUnlock() | ||
var errs []string | ||
for _, entry := range p.entries { | ||
entry.cryptoKey.Labels[labelNameLastUpdate] = fmt.Sprint(p.hooks.clk.Now().Unix()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function has a RLock, but it is setting things here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
|
||
If the _Key Metadata File_ is not found during server startup, the file is recreated, with a new auto-generated server ID. Consequently, if the file is lost, the plugin will not be able to identify keys that it has previously managed and will recreate new keys on demand. | ||
|
||
When an active key is rotated, the old CryptoKeyVersion is scheduled for destruction, and the corresponding CryptoKey is set as inactive. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? The last sentence in "Use of key versions" seems contradictory and makes a little more sense.
Since we already have that sentence, maybe we can just remove this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this. This paragraph was written when the plugin didn't manage multiple CryptoKeyVersions.
| Key | Type | Required | Description | Default | | ||
| --- | ---- | -------- | ----------- | ------- | | ||
| key_policy_file | string | no | A file path location to a custom [IAM Policy (v3)](https://cloud.google.com/pubsub/docs/reference/rpc/google.iam.v1#google.iam.v1.Policy) in JSON format to be attached to created CryptoKeys. | "" | | ||
| key_metadata_file | string | yes | A file path location where key metadata used by the plugin will be persisted. | "" | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| key_metadata_file | string | yes | A file path location where key metadata used by the plugin will be persisted. | "" | | |
| key_metadata_file | string | yes | A file path location where key metadata used by the plugin will be persisted. See "[Management of keys](#management-of-keys)" for more information. | "" | |
When I read this table, the first question I had was "what is this and why do I need it" .. the section below explains it well ❤️
|
||
### Required permissions | ||
|
||
The plugin requires to have in the configured key ring the following IAM permissions granted to the authenticated service account: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plugin requires to have in the configured key ring the following IAM permissions granted to the authenticated service account: | |
The plugin requires the following IAM permissions be granted to the authenticated service account in the configured key ring: |
Here's an alternative, I'm not sure if it's more or less confusing 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I struggled a lot with that phrase! The suggested change looks a lot better to me.
// with the CryptoKey, CryptoKeyVersion and public key. | ||
func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keymanagerv1.KeyType) (*keyEntry, error) { | ||
p.entriesMtx.Lock() | ||
defer p.entriesMtx.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming I understand this correctly ... we'll create a key here when SPIRE prepares a new one ... but, we take a write lock here to do it, so the act of preparing a new key will block signing operations on our other (currently active) key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. The lock window could be reduced using a function that would update the cache holding the write lock only in that function. This function could then call it when it updates the key entries cache. I'll make that change.
p.log.Error("Failed to enqueue CryptoKeyVersion for destruction", reasonTag, err) | ||
} | ||
|
||
return &newKeyEntry, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're effectively branching all of createKey
on whether we have an existing crypto key or not, it might be easier to read if it were broken out into two further functions
listCryptoKeysErr error | ||
describeKeyErr error | ||
getPublicKeyErr error | ||
}{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: can you add test cases to:
- failed to get or create serverID
- ServiceAccountFile is used
- failed to create KMS client
if tt.expectCode != codes.OK { | ||
return | ||
} | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little worried about we are not testing the actual state of this configured plugin, like if it is really getting (and storing) keys? what happens with stored config? etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I'll extend the assertions to assert the config settings and keys loaded.
{ | ||
CryptoKey: &kmspb.CryptoKey{ | ||
Name: cryptoKeyName1, | ||
VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, | ||
}, | ||
fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ | ||
"1": { | ||
publicKey: pubKey, | ||
CryptoKeyVersion: &kmspb.CryptoKeyVersion{ | ||
Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, | ||
Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
CryptoKey: &kmspb.CryptoKey{ | ||
Name: cryptoKeyName1, | ||
VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, | ||
}, | ||
fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ | ||
"2": { | ||
publicKey: pubKey, | ||
CryptoKeyVersion: &kmspb.CryptoKeyVersion{ | ||
Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, | ||
Name: fmt.Sprintf("%s/cryptoKeyVersions/2", cryptoKeyName1), | ||
}, | ||
}, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why this "duplicated" keys are reuqired? are you thinking on test that we keep only one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was duplicated without intention, removed.
ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) | ||
|
||
// This is so dispose aliases blocks on init and allows to test dispose keys isolated | ||
ts.plugin.hooks.disposeCryptoKeysSignal = make(chan error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make(chan error, 1)?
require.NotNil(t, err) | ||
require.Equal(t, tt.err, err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.EqualError(t, err, tt.err)?
require.NotNil(t, err) | ||
require.Equal(t, tt.expectError, err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.NotNil(t, err) | |
require.Equal(t, tt.expectError, err.Error()) | |
require.EqualError(t, err, err.Error()) |
require.Error(t, err) | ||
require.Equal(t, err.Error(), tt.err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.Error(t, err) | |
require.Equal(t, err.Error(), tt.err) | |
require.EqualError(t, err, tt.err) |
|
||
err = ts.plugin.setIamPolicy(ctx, cryptoKeyName1) | ||
if tt.expectError != "" { | ||
require.Error(t, err, tt.expectError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.Error(t, err, tt.expectError) | |
require.EqualError(t, err, tt.expectError) |
}, | ||
}, | ||
signatureCrc32C: &wrapperspb.Int64Value{Value: 1}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a test case with unsupported sign type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I've added a test case to test an unsupported key type in TestGenerateKey.
getPublicKeyErr error | ||
getTokenInfoErr error | ||
updateCryptoKeyErr error | ||
}{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont know if this is the place, but, can you add a test case to validate what happens if we are not able to enqueue items for destruction? and verify that cryptoKey is disabled "only" when it does not contain vesions?
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
Signed-off-by: Agustín Martínez Fayó <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
* Introduce the gcp_kms plugin Signed-off-by: Agustín Martínez Fayó <[email protected]>
This PR introduces the
gcp_kms
KeyManager plugin, that uses the Google Cloud Key Management Service.Fixes #3194.