Skip to content

Commit

Permalink
Merge pull request #635 from SomtochiAma/gcp-kms-cred
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddeco committed May 24, 2022
2 parents 2f454b4 + cbb0fc9 commit fec5316
Show file tree
Hide file tree
Showing 14 changed files with 985 additions and 12 deletions.
16 changes: 13 additions & 3 deletions controllers/kustomization_decryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ const (
// DecryptionVaultTokenFileName is the name of the file containing the
// Hashicorp Vault token.
DecryptionVaultTokenFileName = "sops.vault-token"
// DecryptionVaultTokenFileName is the name of the file containing the
// AWS KMS credentials
// DecryptionAWSKmsFile is the name of the file containing the AWS KMS
// credentials.
DecryptionAWSKmsFile = "sops.aws-kms"
// DecryptionAzureAuthFile is the name of the file containing the Azure
// credentials.
DecryptionAzureAuthFile = "sops.azure-kv"

// DecryptionGCPCredsFile is the name of the file containing the GCP
// credentials.
DecryptionGCPCredsFile = "sops.gcp-kms"
// maxEncryptedFileSize is the max allowed file size in bytes of an encrypted
// file.
maxEncryptedFileSize int64 = 5 << 20
Expand Down Expand Up @@ -139,6 +141,9 @@ type KustomizeDecryptor struct {
// azureToken is the Azure credential token used to authenticate towards
// any Azure Key Vault.
azureToken *azkv.Token
// gcpCredsJSON is the JSON credential file of the service account used to
// authenticate towards any GCP KMS.
gcpCredsJSON []byte

// keyServices are the SOPS keyservice.KeyServiceClient's available to the
// decryptor.
Expand Down Expand Up @@ -244,6 +249,10 @@ func (d *KustomizeDecryptor) ImportKeys(ctx context.Context) error {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
}
}
case filepath.Ext(DecryptionGCPCredsFile):
if name == DecryptionGCPCredsFile {
d.gcpCredsJSON = bytes.Trim(value, "\n")
}
}
}
}
Expand Down Expand Up @@ -543,6 +552,7 @@ func (d *KustomizeDecryptor) loadKeyServiceServers() {
intkeyservice.WithGnuPGHome(d.gnuPGHome),
intkeyservice.WithVaultToken(d.vaultToken),
intkeyservice.WithAgeIdentities(d.ageIdentities),
intkeyservice.WithGCPCredsJSON(d.gcpCredsJSON),
}
if d.azureToken != nil {
serverOpts = append(serverOpts, intkeyservice.WithAzureToken{Token: d.azureToken})
Expand Down
23 changes: 23 additions & 0 deletions controllers/kustomization_decryptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,29 @@ aws_session_token: test-token`),
g.Expect(decryptor.awsCredsProvider).ToNot(BeNil())
},
},
{
name: "GCP Service Account key",
decryption: &kustomizev1.Decryption{
Provider: provider,
SecretRef: &meta.LocalObjectReference{
Name: "gcpkms-secret",
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "gcpkms-secret",
Namespace: provider,
},
Data: map[string][]byte{
DecryptionGCPCredsFile: []byte(`{ "client_id": "<client-id>.apps.googleusercontent.com",
"client_secret": "<secret>",
"type": "authorized_user"}`),
},
},
inspectFunc: func(g *GomegaWithT, decryptor *KustomizeDecryptor) {
g.Expect(decryptor.gcpCredsJSON).ToNot(BeNil())
},
},
{
name: "Azure Key Vault token",
decryption: &kustomizev1.Decryption{
Expand Down
1 change: 0 additions & 1 deletion controllers/kustomization_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ func (kg *KustomizeGenerator) WriteFile(dirPath string) error {
if err != nil {
return err
}

return os.WriteFile(kfile, kd, os.ModePerm)
}

Expand Down
23 changes: 23 additions & 0 deletions docs/spec/v1beta2/kustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,29 @@ stringData:
clientId: some-client-id
```

#### GCP KMS Secret entry

To specify credentials for GCP KMS in a Kubernetes Secret, append a `.data`
entry with a fixed `sops.gcp-kms` key and the service account keys as its value.

```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: sops-keys
namespace: default
stringData:
# Exemplary GCP Service Account credentials file
sops.gcp-kms: |
{
"type": "service_account",
"project_id": "<project-id>",
"private_key_id": "<private-key-id>",
"private_key": "<private-key>"
}
```

#### Hashicorp Vault Secret entry

To specify credentials for Hashicorp Vault in a Kubernetes Secret, append a
Expand Down
13 changes: 8 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.17
replace github.com/fluxcd/kustomize-controller/api => ./api

require (
cloud.google.com/go/kms v1.4.0
filippo.io/age v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2
Expand All @@ -27,13 +28,17 @@ require (
github.com/fluxcd/pkg/testserver v0.2.0
github.com/fluxcd/pkg/untar v0.1.0
github.com/fluxcd/source-controller/api v0.24.4
github.com/golang/protobuf v1.5.2
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/hashicorp/vault/api v1.5.0
github.com/onsi/gomega v1.19.0
github.com/ory/dockertest v3.3.5+incompatible
github.com/spf13/pflag v1.0.5
go.mozilla.org/sops/v3 v3.7.3
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
google.golang.org/api v0.74.0
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf
google.golang.org/grpc v1.45.0
k8s.io/api v0.24.0
k8s.io/apiextensions-apiserver v0.24.0
k8s.io/apimachinery v0.24.0
Expand Down Expand Up @@ -63,7 +68,9 @@ replace github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.3
replace github.com/opencontainers/image-spec => github.com/opencontainers/image-spec v1.0.2

require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect
Expand Down Expand Up @@ -120,7 +127,6 @@ require (
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.3.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
Expand Down Expand Up @@ -198,15 +204,12 @@ require (
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/api v0.74.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
Expand Down
11 changes: 9 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
Expand All @@ -41,6 +43,11 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo=
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
Expand Down Expand Up @@ -1094,8 +1101,8 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down
181 changes: 181 additions & 0 deletions internal/sops/gcpkms/keysource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (C) 2022 The Flux authors
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package gcpkms

import (
"context"
"encoding/base64"
"fmt"
"regexp"
"time"

kms "cloud.google.com/go/kms/apiv1"
"google.golang.org/api/option"
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
"google.golang.org/grpc"
)

var (
// gcpkmsTTL is the duration after which a MasterKey requires rotation.
gcpkmsTTL = time.Hour * 24 * 30 * 6
)

// CredentialJSON is the service account keys used for authentication towards
// GCP KMS.
type CredentialJSON []byte

// ApplyToMasterKey configures the CredentialJSON on the provided key.
func (c CredentialJSON) ApplyToMasterKey(key *MasterKey) {
key.credentialJSON = c
}

// MasterKey is a GCP KMS key used to encrypt and decrypt the SOPS
// data key.
// Adapted from https://github.com/mozilla/sops/blob/v3.7.2/gcpkms/keysource.go
// to be able to have fine-grain control over the credentials used to authenticate
// towards GCP KMS.
type MasterKey struct {
// ResourceID is the resource id used to refer to the gcp kms key.
// It can be retrieved using the `gcloud` command.
ResourceID string
// EncryptedKey is the string returned after encrypting with GCP KMS.
EncryptedKey string
// CreationDate is the creation timestamp of the MasterKey. Used
// for NeedsRotation.
CreationDate time.Time

// credentialJSON are the service account keys used to authenticate
// towards GCP KMS.
credentialJSON []byte
// grpcConn can be used to inject a custom GCP client connection.
// Mostly useful for testing at present, to wire the client to a mock
// server.
grpcConn *grpc.ClientConn
}

// MasterKeyFromResourceID creates a new MasterKey with the provided resource
// ID.
func MasterKeyFromResourceID(resourceID string) *MasterKey {
return &MasterKey{
ResourceID: resourceID,
CreationDate: time.Now().UTC(),
}
}

// Encrypt takes a SOPS data key, encrypts it with GCP KMS, and stores the
// result in the EncryptedKey field.
func (key *MasterKey) Encrypt(datakey []byte) error {
cloudkmsService, err := key.newKMSClient()
if err != nil {
return err
}
defer cloudkmsService.Close()

req := &kmspb.EncryptRequest{
Name: key.ResourceID,
Plaintext: datakey,
}
ctx := context.Background()
resp, err := cloudkmsService.Encrypt(ctx, req)
if err != nil {
return fmt.Errorf("failed to encrypt sops data key with GCP KMS: %w", err)
}
key.EncryptedKey = base64.StdEncoding.EncodeToString(resp.Ciphertext)
return nil
}

// SetEncryptedDataKey sets the encrypted data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}

// EncryptedDataKey returns the encrypted data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}

// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}

// Decrypt decrypts the EncryptedKey field with GCP KMS and returns
// the result.
func (key *MasterKey) Decrypt() ([]byte, error) {
service, err := key.newKMSClient()
if err != nil {
return nil, err
}
defer service.Close()

decodedCipher, err := base64.StdEncoding.DecodeString(string(key.EncryptedDataKey()))
if err != nil {
return nil, err
}
req := &kmspb.DecryptRequest{
Name: key.ResourceID,
Ciphertext: decodedCipher,
}
ctx := context.Background()
resp, err := service.Decrypt(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to decrypt sops data key with GCP KMS Key: %w", err)
}

return resp.Plaintext, nil
}

// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate) > (gcpkmsTTL)
}

// ToString converts the key to a string representation.
func (key *MasterKey) ToString() string {
return key.ResourceID
}

// ToMap converts the MasterKey to a map for serialization purposes.
func (key MasterKey) ToMap() map[string]interface{} {
out := make(map[string]interface{})
out["resource_id"] = key.ResourceID
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
out["enc"] = key.EncryptedKey
return out
}

// newKMSClient returns a GCP KMS client configured with the credentialJSON
// and/or grpcConn, falling back to environmental defaults.
// It returns an error if the ResourceID is invalid, or if the client setup
// fails.
func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`)
matches := re.FindStringSubmatch(key.ResourceID)
if matches == nil {
return nil, fmt.Errorf("no valid resourceId found in %q", key.ResourceID)
}

var opts []option.ClientOption
if key.credentialJSON != nil {
opts = append(opts, option.WithCredentialsJSON(key.credentialJSON))
}
if key.grpcConn != nil {
opts = append(opts, option.WithGRPCConn(key.grpcConn))
}

ctx := context.Background()
client, err := kms.NewKeyManagementClient(ctx, opts...)
if err != nil {
return nil, err
}

return client, nil
}

0 comments on commit fec5316

Please sign in to comment.