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

CLOUDP-167207: Validate GCP Service Account Key #1008

Merged
merged 1 commit into from
Jul 4, 2023
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
78 changes: 78 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 {
helderjs marked this conversation as resolved.
Show resolved Hide resolved
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.Spec.EncryptionAtRest); err != nil {
return err
}

return nil
}

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

return err
}

func encryptionAtRest(encryption *mdbv1.EncryptionAtRest) error {
if encryption != nil &&
encryption.GoogleCloudKms.Enabled != nil &&
*encryption.GoogleCloudKms.Enabled {
if encryption.GoogleCloudKms.ServiceAccountKey == "" {
return fmt.Errorf("missing Google Service Account Key but GCP KMS is enabled")
}
if err := gceServiceAccountKey(encryption.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
}
josvazg marked this conversation as resolved.
Show resolved Hide resolved

func assertParsePrivateKey(key []byte) error {
_, err := x509.ParsePKCS1PrivateKey(key)
if err != nil && strings.Contains(err.Error(), "ParsePKCS8PrivateKey") {
_, err = x509.ParsePKCS8PrivateKey(key)
}
return err
}
94 changes: 94 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,96 @@ 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 testEncryptionAtRest(enabled bool) *mdbv1.EncryptionAtRest {
flag := enabled
return &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) {
assert.NoError(t, encryptionAtRest(&mdbv1.EncryptionAtRest{}))
})

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

t.Run("google service account key validation succeeds if encryption is enabled but the key is empty", func(t *testing.T) {
enc := testEncryptionAtRest(true)
enc.GoogleCloudKms.ServiceAccountKey = ""
assert.ErrorContains(t, encryptionAtRest(enc), "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) {
enc := testEncryptionAtRest(true)
enc.GoogleCloudKms.ServiceAccountKey = sampleSAKey
assert.NoError(t, encryptionAtRest(enc))
})

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

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

t.Run("google service account key validation fails for an empty array json as key", func(t *testing.T) {
enc := testEncryptionAtRest(true)
enc.GoogleCloudKms.ServiceAccountKey = "[]"
assert.ErrorContains(t, encryptionAtRest(enc), "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) {
enc := testEncryptionAtRest(true)
enc.GoogleCloudKms.ServiceAccountKey = `{"type":true}`
assert.ErrorContains(t, encryptionAtRest(enc), "cannot unmarshal bool")
})

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

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