Skip to content
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

Support GCP KMS credentials using decryption secretRef #635

Merged
merged 2 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading