diff --git a/pkg/controller/validate/validate.go b/pkg/controller/validate/validate.go index cc6eed68e7..40173106dd 100644 --- a/pkg/controller/validate/validate.go +++ b/pkg/controller/validate/validate.go @@ -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" @@ -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 @@ -53,6 +72,10 @@ func Project(project *mdbv1.AtlasProject) error { return err } + if err := encryptionAtRest(project); err != nil { + return err + } + return nil } @@ -196,3 +219,58 @@ 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 && + project.Spec.EncryptionAtRest.GoogleCloudKms.ServiceAccountKey != "" { + 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) + } else { + fmt.Println("url=", rawURL, "err=", 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 +} diff --git a/pkg/controller/validate/validate_test.go b/pkg/controller/validate/validate_test.go index a81a1a421c..6b60b430df 100644 --- a/pkg/controller/validate/validate_test.go +++ b/pkg/controller/validate/validate_test.go @@ -1,6 +1,7 @@ package validate import ( + "fmt" "testing" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" @@ -441,3 +442,99 @@ 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": "619108922856-compute@developer.gserviceaccount.com", + "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": "619108922856-compute@developer.gserviceaccount.com", "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) { + assert.NoError(t, encryptionAtRest(projectWithEncryptionAtRest(true))) + }) + + 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") + }) +}