Skip to content

Commit

Permalink
CLOUDP-167207: Validate GCP Service Account Key
Browse files Browse the repository at this point in the history
Signed-off-by: Jose Vazquez <[email protected]>
  • Loading branch information
josvazg committed Jun 30, 2023
1 parent 2592e19 commit a97952c
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ jobs:
"deployment-ns",
"deployment-wide",
"encryption-at-rest",
"encryption-at-rest-gcp",
"free-tier",
"global-deployment",
"helm-ns",
Expand Down
77 changes: 77 additions & 0 deletions pkg/controller/validate/validate.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package validate

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/url"
"reflect"
"strings"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand All @@ -13,6 +18,20 @@ import (
mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
)

type googleServiceAccountKey struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"` // Expects valid PEM key
ClientEmail string `json:"client_email"` // Expects a valid email
ClientID string `json:"client_id"`
AuthURI string `json:"auth_uri"` // Expects valid URL
TokenURI string `json:"token_uri"` // Expects valid URL
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` // Expects valid URL
ClientX509CertURL string `json:"client_x509_cert_url"` // Expects valid URL
UniverseDomain string `json:"universe_domain"`
}

func DeploymentSpec(deploymentSpec mdbv1.AtlasDeploymentSpec) error {
var err error

Expand Down Expand Up @@ -53,6 +72,10 @@ func Project(project *mdbv1.AtlasProject) error {
return err
}

if err := encryptionAtRest(project); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -196,3 +219,57 @@ func projectCustomRoles(customRoles []mdbv1.CustomRole) error {

return err
}

func encryptionAtRest(project *mdbv1.AtlasProject) error {
if project.Spec.EncryptionAtRest != nil &&
project.Spec.EncryptionAtRest.GoogleCloudKms.Enabled != nil &&
*project.Spec.EncryptionAtRest.GoogleCloudKms.Enabled {
if project.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey == "" {
return fmt.Errorf("missing Google Service Account Key but GCP KMS is enabled")
} else if err := gceServiceAccountKey(project.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey); err != nil {
return fmt.Errorf("failed to validate Google Service Account Key: %w", err)
}
}
return nil
}

func unfilter(key string) string {
return strings.ReplaceAll(key, "\\\\n", "\\n")
}

func gceServiceAccountKey(key string) error {
emptyKey := googleServiceAccountKey{}
gceSAKey := googleServiceAccountKey{}
if err := json.Unmarshal(([]byte)(unfilter(key)), &gceSAKey); err != nil {
return fmt.Errorf("invalid service account key format: %w", err)
}
if emptyKey == gceSAKey {
return fmt.Errorf("invalid empty service account key")
}
for _, rawURL := range []string{gceSAKey.AuthURI,
gceSAKey.TokenURI,
gceSAKey.ClientX509CertURL,
gceSAKey.AuthProviderX509CertURL} {
if _, err := url.ParseRequestURI(rawURL); err != nil {
return fmt.Errorf("invalid URL address %q: %w", rawURL, err)
}
}
block, _ := pem.Decode([]byte(gceSAKey.PrivateKey))
if block == nil || !strings.HasSuffix(block.Type, "PRIVATE KEY") {
return fmt.Errorf("failed to decode PEM block containing a private key")
}

err := assertParsePrivateKey(block.Bytes)
if err != nil {
return fmt.Errorf("failed to parse PEM private key: %w", err)
}
return nil
}

func assertParsePrivateKey(key []byte) error {
_, err := x509.ParsePKCS1PrivateKey(key)
if err != nil && strings.Contains(err.Error(), "ParsePKCS8PrivateKey") {
_, err = x509.ParsePKCS8PrivateKey(key)
}
return err
}
99 changes: 99 additions & 0 deletions pkg/controller/validate/validate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validate

import (
"fmt"
"testing"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
Expand Down Expand Up @@ -441,3 +442,101 @@ func TestBackupScheduleValidation(t *testing.T) {
})
})
}

// this key was removed immediately after download, so don't bother
const sampleSAKey = `{
"type": "service_account",
"project_id": "some-project",
"private_key_id": "dc6c401f0acd0147ca70e3169f579b570583b58f",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCviL4+Bnn759sV\nNrtyexwHtR/5JYzwivupqOMdz1zuscqKfJOo6RXzq7Em3saoxLpHwwJzPq+HX1D+\ndaE0fB2hO0Mfjkcgmro0jBbLnRJgFCx7NwkyDfj8z6i4zx2CxZLiwdqo3Q3hEMNy\n5oZs52tFlTkOALCxa76Aq4eUIblupyhlhETQB1fb4D9+U57b4eeeFRyccwR7Cg0x\nfZUE1udV7YwKphLS8dCbLoqAQ3jmaxv+Qjo8e5Mj5oaMxzRAdOO1VtG9GaLU96Rc\nKGS75w+E/eR3Pm7b2dFo3jba2jvgm3U++EJi5/0zL/TQnOMwNHASPUsYQAhZezB5\nh5/MDwmjAgMBAAECggEAIOccZer33Zipz9GpFD3wVJ+GZUC9KO+cWcJ/A/z1Ggb4\nhLnyQbSjOUAjHjqe+U6a7k2m/WwwIctjlrb85yYmtayymc0lFv75zVS/Bx6jrZ/K\ncLQxxJCq7dSM90tXaEZZkKiusH1zFw9522VLqEk+qdXdUnsdo7wjAuJkMQebRxq8\n1lp3UGqAraBXLRrYUnQwBRezSYh93nZ+u+etjRCBjMYoy06PGDrJG05/FFNgQy1y\nGkBVKnmf9WNyDuX1ePyoXCAvRkwUW5ixTNGoGrRwbwleaFTvYPBlPM+TyCw4rdnU\nzRVNpoCqkf6S9tXjHKmGgwkyMqmYMCOk/5xCSb2p4QKBgQD14xcXIf9lnyHJCllU\nQdUIAmIp9/91Rdjpf/y98LCNrD/cyTvVr0+xSnN9ksBEGwvTIaaBcvdCMNBG8sI9\nsnO8W8GG1Bs80D3XIbJFaGmTmOngvrfie3tbP77wfcPgn659Q0I1+bNZGNVX+WEM\nn+f7rfGPa8Br/zHpQf2gaGWHAwKBgQC2wOrInJ+ndpqU49JVaT6YtQj0OKeLhSpc\n0N5DdW0jjD0WrnhOsLUMPC5V8R5fo4tFPfXVIfE2k8J5xxgopJzGLlHmhxq0ltmF\nbSoM2uHKf0UiRFmVTwZmzDwn+Ym/H9J/6L7Gd86u8kmWfJYFa3OzqJTvw7e+k4kD\nITb/NlEg4QKBgQCfW4AZg/Ur/Ug+LTDbxJa2TCUmog20CYKdQk+hIh6qktoI03qt\n8KKrel8DIVruSMEPIp3xA3twMIarlKWCqucLSkRQh6LndOa/SJ1rElJqUA4zlCdE\n51Z5OwUag8exCoxhrnd4183+jnOmQn89WV1V5dPKacEZvRix3gzsKvyx1QKBgFsH\nlOsAOPYtOapYIHiyx59A7YjYf3wbhJJe55cqcoZ2YCdgGET59/R0NZBRXhO9Xq3K\nwxy6n2/UAdauuPXlqMF+aQUu3rp9OTQgwAVPMZCv/DupWAXrKwEhUgWHYnl03GEi\nCYTKQIUb4lO3EvL4JtWiby1Oi8O9sU2ByectoxOBAoGASm9BXSN8Ru+dP5/E55mb\nd//aQlxlIvROhWSnotGzhyQ6DVk2fRRZQAuTEFVEprBX87gckdzb5cdaomziO9be\nhmtv1ValgmOCnta2AYw1blvfGK7B5FEpFckMniLjWap08aironIImj6ligLWDqc0\nNbdyAvc6N/5qG8gu4f8C2Q4=\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "117865750705662546099",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/619108922856-compute%40developer.gserviceaccount.com",
"universe_domain": "googleapis.com"
}`

const sampleSAKeyOneLine = `{ "type": "service_account", "project_id": "some-project", "private_key_id": "dc6c401f0acd0147ca70e3169f579b570583b58f", "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCviL4+Bnn759sV\\nNrtyexwHtR/5JYzwivupqOMdz1zuscqKfJOo6RXzq7Em3saoxLpHwwJzPq+HX1D+\\ndaE0fB2hO0Mfjkcgmro0jBbLnRJgFCx7NwkyDfj8z6i4zx2CxZLiwdqo3Q3hEMNy\\n5oZs52tFlTkOALCxa76Aq4eUIblupyhlhETQB1fb4D9+U57b4eeeFRyccwR7Cg0x\\nfZUE1udV7YwKphLS8dCbLoqAQ3jmaxv+Qjo8e5Mj5oaMxzRAdOO1VtG9GaLU96Rc\\nKGS75w+E/eR3Pm7b2dFo3jba2jvgm3U++EJi5/0zL/TQnOMwNHASPUsYQAhZezB5\\nh5/MDwmjAgMBAAECggEAIOccZer33Zipz9GpFD3wVJ+GZUC9KO+cWcJ/A/z1Ggb4\\nhLnyQbSjOUAjHjqe+U6a7k2m/WwwIctjlrb85yYmtayymc0lFv75zVS/Bx6jrZ/K\\ncLQxxJCq7dSM90tXaEZZkKiusH1zFw9522VLqEk+qdXdUnsdo7wjAuJkMQebRxq8\\n1lp3UGqAraBXLRrYUnQwBRezSYh93nZ+u+etjRCBjMYoy06PGDrJG05/FFNgQy1y\\nGkBVKnmf9WNyDuX1ePyoXCAvRkwUW5ixTNGoGrRwbwleaFTvYPBlPM+TyCw4rdnU\\nzRVNpoCqkf6S9tXjHKmGgwkyMqmYMCOk/5xCSb2p4QKBgQD14xcXIf9lnyHJCllU\\nQdUIAmIp9/91Rdjpf/y98LCNrD/cyTvVr0+xSnN9ksBEGwvTIaaBcvdCMNBG8sI9\\nsnO8W8GG1Bs80D3XIbJFaGmTmOngvrfie3tbP77wfcPgn659Q0I1+bNZGNVX+WEM\\nn+f7rfGPa8Br/zHpQf2gaGWHAwKBgQC2wOrInJ+ndpqU49JVaT6YtQj0OKeLhSpc\\n0N5DdW0jjD0WrnhOsLUMPC5V8R5fo4tFPfXVIfE2k8J5xxgopJzGLlHmhxq0ltmF\\nbSoM2uHKf0UiRFmVTwZmzDwn+Ym/H9J/6L7Gd86u8kmWfJYFa3OzqJTvw7e+k4kD\\nITb/NlEg4QKBgQCfW4AZg/Ur/Ug+LTDbxJa2TCUmog20CYKdQk+hIh6qktoI03qt\\n8KKrel8DIVruSMEPIp3xA3twMIarlKWCqucLSkRQh6LndOa/SJ1rElJqUA4zlCdE\\n51Z5OwUag8exCoxhrnd4183+jnOmQn89WV1V5dPKacEZvRix3gzsKvyx1QKBgFsH\\nlOsAOPYtOapYIHiyx59A7YjYf3wbhJJe55cqcoZ2YCdgGET59/R0NZBRXhO9Xq3K\\nwxy6n2/UAdauuPXlqMF+aQUu3rp9OTQgwAVPMZCv/DupWAXrKwEhUgWHYnl03GEi\\nCYTKQIUb4lO3EvL4JtWiby1Oi8O9sU2ByectoxOBAoGASm9BXSN8Ru+dP5/E55mb\\nd//aQlxlIvROhWSnotGzhyQ6DVk2fRRZQAuTEFVEprBX87gckdzb5cdaomziO9be\\nhmtv1ValgmOCnta2AYw1blvfGK7B5FEpFckMniLjWap08aironIImj6ligLWDqc0\\nNbdyAvc6N/5qG8gu4f8C2Q4=\\n-----END PRIVATE KEY-----\\n", "client_email": "[email protected]", "client_id": "117865750705662546099", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/619108922856-compute%40developer.gserviceaccount.com", "universe_domain": "googleapis.com"}`

func projectWithEncryptionAtRest(enabled bool) *mdbv1.AtlasProject {
flag := enabled
return &mdbv1.AtlasProject{
Spec: mdbv1.AtlasProjectSpec{
EncryptionAtRest: &mdbv1.EncryptionAtRest{
GoogleCloudKms: mdbv1.GoogleCloudKms{
Enabled: &flag,
ServiceAccountKey: sampleSAKey,
},
},
},
}
}

func withProperUrls(properties string) string {
urls := `"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/619108922856-compute%40developer.gserviceaccount.com"`
return fmt.Sprintf(`{%s, %s}`, urls, properties)
}

func TestEncryptionAtRestValidation(t *testing.T) {
t.Run("google service account key validation succeeds if no encryption at rest is used", func(t *testing.T) {
prj := &mdbv1.AtlasProject{Spec: mdbv1.AtlasProjectSpec{}}
assert.NoError(t, encryptionAtRest(prj))
})

t.Run("google service account key validation succeeds if encryption at rest is disabled", func(t *testing.T) {
assert.NoError(t, encryptionAtRest(projectWithEncryptionAtRest(false)))
})

t.Run("google service account key validation succeeds if encryption is enabled but the key is empty", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = ""
assert.ErrorContains(t, encryptionAtRest(prj), "missing Google Service Account Key but GCP KMS is enabled")
})

t.Run("google service account key validation succeeds for a good key", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = sampleSAKey
assert.NoError(t, encryptionAtRest(prj))
})

t.Run("google service account key validation succeeds for a good key in a single line", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = sampleSAKeyOneLine
assert.NoError(t, encryptionAtRest(prj))
})

t.Run("google service account key validation fails for an empty json key", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = "{}"
assert.ErrorContains(t, encryptionAtRest(prj), "invalid empty service account key")
})

t.Run("google service account key validation fails for an empty array json as key", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = "[]"
assert.ErrorContains(t, encryptionAtRest(prj), "cannot unmarshal array into Go value")
})

t.Run("google service account key validation fails for a json object with a wrong field type", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = `{"type":true}`
assert.ErrorContains(t, encryptionAtRest(prj), "cannot unmarshal bool")
})

t.Run("google service account key validation fails for a bad pem key", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = withProperUrls(`"private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvQblah\n-----END PRIVATE KEY-----\n"`)
assert.ErrorContains(t, encryptionAtRest(prj), "failed to decode PEM")
})

t.Run("google service account key validation fails for a bad URL", func(t *testing.T) {
prj := projectWithEncryptionAtRest(true)
prj.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey = withProperUrls(`"token_uri": "http//badurl.example"`)
assert.ErrorContains(t, encryptionAtRest(prj), "invalid URL address")
})
}
Loading

0 comments on commit a97952c

Please sign in to comment.