Skip to content

Commit

Permalink
Merge pull request #16936 from negz/gcskeys
Browse files Browse the repository at this point in the history
Support 'customer supplied encryption keys' in the GCS backend
  • Loading branch information
paddycarver authored Jan 9, 2018
2 parents 17fbb8e + 0118411 commit e4cdbd6
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 12 deletions.
34 changes: 34 additions & 0 deletions backend/remote-state/gcs/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gcs

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -30,6 +31,8 @@ type gcsBackend struct {
prefix string
defaultStateFile string

encryptionKey []byte

projectID string
region string
}
Expand Down Expand Up @@ -65,6 +68,13 @@ func New() backend.Backend {
Default: "",
},

"encryption_key": {
Type: schema.TypeString,
Optional: true,
Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.",
Default: "",
},

"project": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -154,6 +164,30 @@ func (b *gcsBackend) configure(ctx context.Context) error {

b.storageClient = client

key := data.Get("encryption_key").(string)
if key == "" {
key = os.Getenv("GOOGLE_ENCRYPTION_KEY")
}

if key != "" {
kc, _, err := pathorcontents.Read(key)
if err != nil {
return fmt.Errorf("Error loading encryption key: %s", err)
}

// The GCS client expects a customer supplied encryption key to be
// passed in as a 32 byte long byte slice. The byte slice is base64
// encoded before being passed to the API. We take a base64 encoded key
// to remain consistent with the GCS docs.
// https://cloud.google.com/storage/docs/encryption#customer-supplied
// https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181
k, err := base64.StdEncoding.DecodeString(kc)
if err != nil {
return fmt.Errorf("Error decoding encryption key: %s", err)
}
b.encryptionKey = k
}

return nil
}

Expand Down
1 change: 1 addition & 0 deletions backend/remote-state/gcs/backend_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (b *gcsBackend) client(name string) (*remoteClient, error) {
bucketName: b.bucketName,
stateFilePath: b.stateFile(name),
lockFilePath: b.lockFile(name),
encryptionKey: b.encryptionKey,
}, nil
}

Expand Down
60 changes: 49 additions & 11 deletions backend/remote-state/gcs/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import (
"github.com/hashicorp/terraform/state/remote"
)

const noPrefix = ""
const (
noPrefix = ""
noEncryptionKey = ""
)

// See https://cloud.google.com/storage/docs/using-encryption-keys#generating_your_own_encryption_key
var encryptionKey = "yRyCOikXi1ZDNE0xN3yiFsJjg7LGimoLrGFcLZgQoVk="

func TestStateFile(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -52,7 +58,26 @@ func TestRemoteClient(t *testing.T) {
t.Parallel()

bucket := bucketName(t)
be := setupBackend(t, bucket, noPrefix)
be := setupBackend(t, bucket, noPrefix, noEncryptionKey)
defer teardownBackend(t, be, noPrefix)

ss, err := be.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("be.State(%q) = %v", backend.DefaultStateName, err)
}

rs, ok := ss.(*remote.State)
if !ok {
t.Fatalf("be.State(): got a %T, want a *remote.State", ss)
}

remote.TestClient(t, rs.Client)
}
func TestRemoteClientWithEncryption(t *testing.T) {
t.Parallel()

bucket := bucketName(t)
be := setupBackend(t, bucket, noPrefix, encryptionKey)
defer teardownBackend(t, be, noPrefix)

ss, err := be.State(backend.DefaultStateName)
Expand All @@ -72,7 +97,7 @@ func TestRemoteLocks(t *testing.T) {
t.Parallel()

bucket := bucketName(t)
be := setupBackend(t, bucket, noPrefix)
be := setupBackend(t, bucket, noPrefix, noEncryptionKey)
defer teardownBackend(t, be, noPrefix)

remoteClient := func() (remote.Client, error) {
Expand Down Expand Up @@ -106,10 +131,10 @@ func TestBackend(t *testing.T) {

bucket := bucketName(t)

be0 := setupBackend(t, bucket, noPrefix)
be0 := setupBackend(t, bucket, noPrefix, noEncryptionKey)
defer teardownBackend(t, be0, noPrefix)

be1 := setupBackend(t, bucket, noPrefix)
be1 := setupBackend(t, bucket, noPrefix, noEncryptionKey)

backend.TestBackend(t, be0, be1)
}
Expand All @@ -119,16 +144,28 @@ func TestBackendWithPrefix(t *testing.T) {
prefix := "test/prefix"
bucket := bucketName(t)

be0 := setupBackend(t, bucket, prefix)
be0 := setupBackend(t, bucket, prefix, noEncryptionKey)
defer teardownBackend(t, be0, prefix)

be1 := setupBackend(t, bucket, prefix+"/")
be1 := setupBackend(t, bucket, prefix+"/", noEncryptionKey)

backend.TestBackend(t, be0, be1)
}
func TestBackendWithEncryption(t *testing.T) {
t.Parallel()

bucket := bucketName(t)

be0 := setupBackend(t, bucket, noPrefix, encryptionKey)
defer teardownBackend(t, be0, noPrefix)

be1 := setupBackend(t, bucket, noPrefix, encryptionKey)

backend.TestBackend(t, be0, be1)
}

// setupBackend returns a new GCS backend.
func setupBackend(t *testing.T, bucket, prefix string) backend.Backend {
func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend {
t.Helper()

projectID := os.Getenv("GOOGLE_PROJECT")
Expand All @@ -139,9 +176,10 @@ func setupBackend(t *testing.T, bucket, prefix string) backend.Backend {
}

config := map[string]interface{}{
"project": projectID,
"bucket": bucket,
"prefix": prefix,
"project": projectID,
"bucket": bucket,
"prefix": prefix,
"encryption_key": key,
}

b := backend.TestBackendConfig(t, New(), config)
Expand Down
7 changes: 6 additions & 1 deletion backend/remote-state/gcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type remoteClient struct {
bucketName string
stateFilePath string
lockFilePath string
encryptionKey []byte
}

func (c *remoteClient) Get() (payload *remote.Payload, err error) {
Expand Down Expand Up @@ -152,7 +153,11 @@ func (c *remoteClient) lockInfo() (*state.LockInfo, error) {
}

func (c *remoteClient) stateFile() *storage.ObjectHandle {
return c.storageClient.Bucket(c.bucketName).Object(c.stateFilePath)
h := c.storageClient.Bucket(c.bucketName).Object(c.stateFilePath)
if len(c.encryptionKey) > 0 {
return h.Key(c.encryptionKey)
}
return h
}

func (c *remoteClient) stateFileURL() string {
Expand Down
1 change: 1 addition & 0 deletions website/docs/backends/types/gcs.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ The following configuration options are supported:
Since buckets have globally unique names, the project ID is not required to access the bucket during normal operation.
* `region` / `GOOGLE_REGION` - (Optional) The region in which a new bucket is created.
For more information, see [Bucket Locations](https://cloud.google.com/storage/docs/bucket-locations).
* `encryption_key` / `GOOGLE_ENCRYPTION_KEY` - (Optional) A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state. For more information see [Customer Supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied).

0 comments on commit e4cdbd6

Please sign in to comment.