From c4a9e339f8f33e442a7228f710870649c1077b04 Mon Sep 17 00:00:00 2001 From: Maxim Goncharenko <kacejot@fex.net> Date: Tue, 14 May 2019 18:10:25 +0300 Subject: [PATCH] Implemented Validation Pattern base. Updated Webhooks registration logic. Updated project for using TLS package --- init.go | 20 +-- kubeclient/certificates.go | 24 +-- kubeclient/kubeclient.go | 2 +- main.go | 16 +- pkg/policyengine/mutation.go | 7 +- pkg/policyengine/mutation/checkRules.go | 44 ----- pkg/policyengine/mutation/patches.go | 1 - pkg/policyengine/mutation/utils.go | 39 ++++ pkg/policyengine/policyengine.go | 10 +- pkg/policyengine/validation.go | 99 ++++++++++- pkg/webhooks/registration.go | 167 +++++++++++------ pkg/webhooks/server.go | 227 ++++++++++++------------ pkg/webhooks/utils.go | 18 +- policycontroller/processPolicy.go | 2 +- 14 files changed, 410 insertions(+), 266 deletions(-) delete mode 100644 pkg/policyengine/mutation/checkRules.go diff --git a/init.go b/init.go index f3c7913d92..358e7b2d29 100644 --- a/init.go +++ b/init.go @@ -7,7 +7,7 @@ import ( "github.com/nirmata/kube-policy/config" "github.com/nirmata/kube-policy/kubeclient" - "github.com/nirmata/kube-policy/utils" + "github.com/nirmata/kube-policy/pkg/tls" rest "k8s.io/client-go/rest" clientcmd "k8s.io/client-go/tools/clientcmd" @@ -23,27 +23,23 @@ func createClientConfig(kubeconfig string) (*rest.Config, error) { } } -func initTlsPemPair(certFile, keyFile string, clientConfig *rest.Config, kubeclient *kubeclient.KubeClient) (*utils.TlsPemPair, error) { - var tlsPair *utils.TlsPemPair +func initTlsPemPair(certFile, keyFile string, clientConfig *rest.Config, kubeclient *kubeclient.KubeClient) (*tls.TlsPemPair, error) { + var tlsPair *tls.TlsPemPair if certFile != "" || keyFile != "" { tlsPair = tlsPairFromFiles(certFile, keyFile) } var err error if tlsPair != nil { - log.Print("Using given TLS key/certificate pair") return tlsPair, nil } else { tlsPair, err = tlsPairFromCluster(clientConfig, kubeclient) - if err == nil { - log.Printf("Using TLS key/certificate from cluster") - } return tlsPair, err } } // Loads PEM private key and TLS certificate from given files -func tlsPairFromFiles(certFile, keyFile string) *utils.TlsPemPair { +func tlsPairFromFiles(certFile, keyFile string) *tls.TlsPemPair { if certFile == "" || keyFile == "" { return nil } @@ -60,7 +56,7 @@ func tlsPairFromFiles(certFile, keyFile string) *utils.TlsPemPair { return nil } - return &utils.TlsPemPair{ + return &tls.TlsPemPair{ Certificate: certContent, PrivateKey: keyContent, } @@ -69,19 +65,19 @@ func tlsPairFromFiles(certFile, keyFile string) *utils.TlsPemPair { // Loads or creates PEM private key and TLS certificate for webhook server. // Created pair is stored in cluster's secret. // Returns struct with key/certificate pair. -func tlsPairFromCluster(configuration *rest.Config, client *kubeclient.KubeClient) (*utils.TlsPemPair, error) { +func tlsPairFromCluster(configuration *rest.Config, client *kubeclient.KubeClient) (*tls.TlsPemPair, error) { apiServerUrl, err := url.Parse(configuration.Host) if err != nil { return nil, err } - certProps := utils.TlsCertificateProps{ + certProps := tls.TlsCertificateProps{ Service: config.WebhookServiceName, Namespace: config.KubePolicyNamespace, ApiServerHost: apiServerUrl.Hostname(), } tlsPair := client.ReadTlsPair(certProps) - if utils.IsTlsPairShouldBeUpdated(tlsPair) { + if tls.IsTlsPairShouldBeUpdated(tlsPair) { log.Printf("Generating new key/certificate pair for TLS") tlsPair, err = client.GenerateTlsPemPair(certProps) if err != nil { diff --git a/kubeclient/certificates.go b/kubeclient/certificates.go index 2c5666cc68..9626c1004f 100644 --- a/kubeclient/certificates.go +++ b/kubeclient/certificates.go @@ -5,22 +5,22 @@ import ( "fmt" "time" - "github.com/nirmata/kube-policy/utils" - + tls "github.com/nirmata/kube-policy/pkg/tls" certificates "k8s.io/api/certificates/v1beta1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Issues TLS certificate for webhook server using given PEM private key // Returns signed and approved TLS certificate in PEM format -func (kc *KubeClient) GenerateTlsPemPair(props utils.TlsCertificateProps) (*utils.TlsPemPair, error) { - privateKey, err := utils.TlsGeneratePrivateKey() +func (kc *KubeClient) GenerateTlsPemPair(props tls.TlsCertificateProps) (*tls.TlsPemPair, error) { + privateKey, err := tls.TlsGeneratePrivateKey() if err != nil { return nil, err } - certRequest, err := utils.TlsCertificateGenerateRequest(privateKey, props) + certRequest, err := tls.TlsCertificateGenerateRequest(privateKey, props) if err != nil { return nil, errors.New(fmt.Sprintf("Unable to create certificate request: %v", err)) } @@ -35,9 +35,9 @@ func (kc *KubeClient) GenerateTlsPemPair(props utils.TlsCertificateProps) (*util return nil, errors.New(fmt.Sprintf("Unable to fetch certificate from request: %v", err)) } - return &utils.TlsPemPair{ + return &tls.TlsPemPair{ Certificate: tlsCert, - PrivateKey: utils.TlsPrivateKeyToPem(privateKey), + PrivateKey: tls.TlsPrivateKeyToPem(privateKey), }, nil } @@ -111,7 +111,7 @@ const privateKeyField string = "privateKey" const certificateField string = "certificate" // Reads the pair of TLS certificate and key from the specified secret. -func (kc *KubeClient) ReadTlsPair(props utils.TlsCertificateProps) *utils.TlsPemPair { +func (kc *KubeClient) ReadTlsPair(props tls.TlsCertificateProps) *tls.TlsPemPair { name := generateSecretName(props) secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, metav1.GetOptions{}) if err != nil { @@ -119,7 +119,7 @@ func (kc *KubeClient) ReadTlsPair(props utils.TlsCertificateProps) *utils.TlsPem return nil } - pemPair := utils.TlsPemPair{ + pemPair := tls.TlsPemPair{ Certificate: secret.Data[certificateField], PrivateKey: secret.Data[privateKeyField], } @@ -136,7 +136,7 @@ func (kc *KubeClient) ReadTlsPair(props utils.TlsCertificateProps) *utils.TlsPem // Writes the pair of TLS certificate and key to the specified secret. // Updates existing secret or creates new one. -func (kc *KubeClient) WriteTlsPair(props utils.TlsCertificateProps, pemPair *utils.TlsPemPair) error { +func (kc *KubeClient) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPemPair) error { name := generateSecretName(props) secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, metav1.GetOptions{}) @@ -176,6 +176,6 @@ func (kc *KubeClient) WriteTlsPair(props utils.TlsCertificateProps, pemPair *uti return err } -func generateSecretName(props utils.TlsCertificateProps) string { - return utils.GenerateInClusterServiceName(props) + ".kube-policy-tls-pair" +func generateSecretName(props tls.TlsCertificateProps) string { + return tls.GenerateInClusterServiceName(props) + ".kube-policy-tls-pair" } diff --git a/kubeclient/kubeclient.go b/kubeclient/kubeclient.go index eca5f406b6..d4cd0220d2 100644 --- a/kubeclient/kubeclient.go +++ b/kubeclient/kubeclient.go @@ -29,7 +29,7 @@ type KubeClient struct { // Checks parameters and creates new instance of KubeClient func NewKubeClient(config *rest.Config, logger *log.Logger) (*KubeClient, error) { if logger == nil { - logger = log.New(os.Stdout, "Kubernetes client: ", log.LstdFlags|log.Lshortfile) + logger = log.New(os.Stdout, "Kubernetes client: ", log.LstdFlags) } client, err := kubernetes.NewForConfig(config) diff --git a/main.go b/main.go index 8eac1b64f1..e5f516fa89 100644 --- a/main.go +++ b/main.go @@ -68,18 +68,26 @@ func main() { if err != nil { log.Fatalf("Unable to create webhook server: %v\n", err) } - server.RunAsync() + + webhookRegistrationClient, err := webhooks.NewWebhookRegistrationClient(clientConfig, kubeclient) + if err != nil { + log.Fatalf("Unable to register admission webhooks on cluster: %v\n", err) + } stopCh := signals.SetupSignalHandler() + policyInformerFactory.Start(stopCh) - if err = eventController.Run(stopCh); err != nil { - log.Fatalf("Error running EventController: %v\n", err) - } + eventController.Run(stopCh) if err = policyController.Run(stopCh); err != nil { log.Fatalf("Error running PolicyController: %v\n", err) } + if err = webhookRegistrationClient.Register(); err != nil { + log.Fatalf("Failed registering Admission Webhooks: %v\n", err) + } + + server.RunAsync() <-stopCh server.Stop() } diff --git a/pkg/policyengine/mutation.go b/pkg/policyengine/mutation.go index 75f20193b3..4ab71befcf 100644 --- a/pkg/policyengine/mutation.go +++ b/pkg/policyengine/mutation.go @@ -22,7 +22,7 @@ func (p *policyEngine) Mutate(policy kubepolicy.Policy, rawResource []byte) []mu continue } - ok, err := mutation.IsRuleApplicableToResource(rawResource, rule.ResourceDescription) + ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription) if err != nil { p.logger.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) continue @@ -33,6 +33,10 @@ func (p *policyEngine) Mutate(policy kubepolicy.Policy, rawResource []byte) []mu continue } + if rule.Mutation == nil { + continue + } + // Process Overlay if rule.Mutation.Overlay != nil { @@ -54,7 +58,6 @@ func (p *policyEngine) Mutate(policy kubepolicy.Policy, rawResource []byte) []mu policyPatches = append(policyPatches, processedPatches...) } } - } return policyPatches diff --git a/pkg/policyengine/mutation/checkRules.go b/pkg/policyengine/mutation/checkRules.go deleted file mode 100644 index bcd73a0840..0000000000 --- a/pkg/policyengine/mutation/checkRules.go +++ /dev/null @@ -1,44 +0,0 @@ -package mutation - -import ( - "github.com/minio/minio/pkg/wildcard" - types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// kind is the type of object being manipulated -// Checks requests kind, name and labels to fit the policy -func IsRuleApplicableToResource(resourceRaw []byte, description types.ResourceDescription) (bool, error) { - kind := ParseKindFromObject(resourceRaw) - if description.Kind != kind { - return false, nil - } - - if resourceRaw != nil { - meta := ParseMetadataFromObject(resourceRaw) - name := ParseNameFromObject(resourceRaw) - - if description.Name != nil { - - if !wildcard.Match(*description.Name, name) { - return false, nil - } - } - - if description.Selector != nil { - selector, err := metav1.LabelSelectorAsSelector(description.Selector) - - if err != nil { - return false, err - } - - labelMap := ParseLabelsFromMetadata(meta) - - if !selector.Matches(labelMap) { - return false, nil - } - - } - } - return true, nil -} diff --git a/pkg/policyengine/mutation/patches.go b/pkg/policyengine/mutation/patches.go index 0b3015020d..bf86a73418 100644 --- a/pkg/policyengine/mutation/patches.go +++ b/pkg/policyengine/mutation/patches.go @@ -10,7 +10,6 @@ import ( type PatchBytes []byte -// Test patches on given document according to given sets. // Returns array from separate patches that can be applied to the document // Returns error ONLY in case when creation of resource should be denied. func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, error) { diff --git a/pkg/policyengine/mutation/utils.go b/pkg/policyengine/mutation/utils.go index 5abc2ee03c..eb1daf2156 100644 --- a/pkg/policyengine/mutation/utils.go +++ b/pkg/policyengine/mutation/utils.go @@ -4,6 +4,9 @@ import ( "encoding/json" "strings" + "github.com/minio/minio/pkg/wildcard" + kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" ) @@ -65,3 +68,39 @@ func ParseRegexPolicyResourceName(policyResourceName string) (string, bool) { } return strings.Trim(regex[1], " "), true } + +// ResourceMeetsRules checks requests kind, name and labels to fit the policy +func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescription) (bool, error) { + kind := ParseKindFromObject(resourceRaw) + if description.Kind != kind { + return false, nil + } + + if resourceRaw != nil { + meta := ParseMetadataFromObject(resourceRaw) + name := ParseNameFromObject(resourceRaw) + + if description.Name != nil { + + if !wildcard.Match(*description.Name, name) { + return false, nil + } + } + + if description.Selector != nil { + selector, err := metav1.LabelSelectorAsSelector(description.Selector) + + if err != nil { + return false, err + } + + labelMap := ParseLabelsFromMetadata(meta) + + if !selector.Matches(labelMap) { + return false, nil + } + + } + } + return true, nil +} diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go index cbe8367d78..76936b9913 100644 --- a/pkg/policyengine/policyengine.go +++ b/pkg/policyengine/policyengine.go @@ -12,15 +12,14 @@ import ( ) type PolicyEngine interface { - // ProcessMutation should be called from admission contoller + // Mutate should be called from admission contoller // when there is an creation / update of the resource // ProcessMutation(policy types.Policy, rawResource []byte) (patchBytes []byte, events []Events, err error) Mutate(policy types.Policy, rawResource []byte) []mutation.PatchBytes - // ProcessValidation should be called from admission contoller + // Validate should be called from admission contoller // when there is an creation / update of the resource - // TODO: Change name to Validate - ProcessValidation(policy types.Policy, rawResource []byte) + Validate(policy types.Policy, rawResource []byte) bool // ProcessExisting should be called from policy controller // when there is an create / update of the policy @@ -36,6 +35,7 @@ type policyEngine struct { logger *log.Logger } +// NewPolicyEngine creates new instance of policyEngine func NewPolicyEngine(kubeClient *kubeClient.KubeClient, logger *log.Logger) PolicyEngine { return &policyEngine{ kubeClient: kubeClient, @@ -54,7 +54,7 @@ func (p *policyEngine) ProcessExisting(policy types.Policy, rawResource []byte) continue } - if ok, err := mutation.IsRuleApplicableToResource(rawResource, rule.ResourceDescription); !ok { + if ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription); !ok { p.logger.Printf("Rule %s of policy %s is not applicable to the request", rule.Name, policy.Name) return nil, nil, err } diff --git a/pkg/policyengine/validation.go b/pkg/policyengine/validation.go index 282a4496a4..acd4a83bc9 100644 --- a/pkg/policyengine/validation.go +++ b/pkg/policyengine/validation.go @@ -1,5 +1,100 @@ package policyengine -import types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" +import ( + "encoding/json" + "fmt" -func (p *policyEngine) ProcessValidation(policy types.Policy, rawResource []byte) {} + kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kube-policy/pkg/policyengine/mutation" +) + +func (p *policyEngine) Validate(policy kubepolicy.Policy, rawResource []byte) bool { + var resource interface{} + json.Unmarshal(rawResource, &resource) + + allowed := true + for i, rule := range policy.Spec.Rules { + + // Checks for preconditions + // TODO: Rework PolicyEngine interface that it receives not a policy, but mutation object for + // Mutate, validation for Validate and so on. It will allow to bring this checks outside of PolicyEngine + // to common part as far as they present for all: mutation, validation, generation + + err := rule.Validate() + if err != nil { + p.logger.Printf("Rule has invalid structure: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) + continue + } + + ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription) + if err != nil { + p.logger.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) + continue + } + + if !ok { + p.logger.Printf("Rule is not applicable t the request: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) + continue + } + + if rule.Validation == nil { + continue + } + + if err := traverseAndValidate(resource, rule.Validation.Pattern); err != nil { + p.logger.Printf("Validation with the rule %s has failed %s: %s\n", rule.Name, err.Error(), *rule.Validation.Message) + allowed = false + } else { + p.logger.Printf("Validation rule %s is successful %s: %s\n", rule.Name, err.Error(), *rule.Validation.Message) + } + } + + return allowed +} + +func traverseAndValidate(resourcePart, patternPart interface{}) error { + switch pattern := patternPart.(type) { + case map[string]interface{}: + dictionary, ok := resourcePart.(map[string]interface{}) + + if !ok { + return fmt.Errorf("Validating error: expected %T, found %T", patternPart, resourcePart) + } + + var err error + for key, value := range pattern { + err = traverseAndValidate(dictionary[key], value) + } + return err + + case []interface{}: + array, ok := resourcePart.([]interface{}) + + if !ok { + return fmt.Errorf("Validating error: expected %T, found %T", patternPart, resourcePart) + } + + var err error + for i, value := range pattern { + err = traverseAndValidate(array[i], value) + } + return err + case string: + str := resourcePart.(string) + if !checkForWildcard(str, pattern) { + return fmt.Errorf("Value %s has not passed wildcard check %s", str, pattern) + } + default: + return fmt.Errorf("Received unknown type: %T", patternPart) + } + + return nil +} + +func checkForWildcard(value, pattern string) bool { + return value == pattern +} + +func checkForOperator(value int, pattern string) bool { + return true +} diff --git a/pkg/webhooks/registration.go b/pkg/webhooks/registration.go index dda450dad7..6fe8a64f2b 100644 --- a/pkg/webhooks/registration.go +++ b/pkg/webhooks/registration.go @@ -13,32 +13,48 @@ import ( rest "k8s.io/client-go/rest" ) -type MutationWebhookRegistration struct { +// WebhookRegistrationClient is client for registration webhooks on cluster +type WebhookRegistrationClient struct { registrationClient *admregclient.AdmissionregistrationV1beta1Client kubeclient *kubeclient.KubeClient clientConfig *rest.Config } -func NewMutationWebhookRegistration(clientConfig *rest.Config, kubeclient *kubeclient.KubeClient) (*MutationWebhookRegistration, error) { +// NewWebhookRegistrationClient creates new WebhookRegistrationClient instance +func NewWebhookRegistrationClient(clientConfig *rest.Config, kubeclient *kubeclient.KubeClient) (*WebhookRegistrationClient, error) { registrationClient, err := admregclient.NewForConfig(clientConfig) if err != nil { return nil, err } - return &MutationWebhookRegistration{ + return &WebhookRegistrationClient{ registrationClient: registrationClient, kubeclient: kubeclient, clientConfig: clientConfig, }, nil } -func (mwr *MutationWebhookRegistration) Register() error { - webhookConfig, err := mwr.constructWebhookConfig(mwr.clientConfig) +// Register creates admission webhooks configs on cluster +func (wrc *WebhookRegistrationClient) Register() error { + // For the case if cluster already has this configs + wrc.Deregister() + + mutatingWebhookConfig, err := wrc.constructMutatingWebhookConfig(wrc.clientConfig) if err != nil { return err } - _, err = mwr.registrationClient.MutatingWebhookConfigurations().Create(webhookConfig) + _, err = wrc.registrationClient.MutatingWebhookConfigurations().Create(mutatingWebhookConfig) + if err != nil { + return err + } + + validationWebhookConfig, err := wrc.constructValidatingWebhookConfig(wrc.clientConfig) + if err != nil { + return err + } + + _, err = wrc.registrationClient.ValidatingWebhookConfigurations().Create(validationWebhookConfig) if err != nil { return err } @@ -46,70 +62,109 @@ func (mwr *MutationWebhookRegistration) Register() error { return nil } -func (mwr *MutationWebhookRegistration) Deregister() error { - return mwr.registrationClient.MutatingWebhookConfigurations().Delete(config.MutationWebhookName, &meta.DeleteOptions{}) +// Deregister deletes webhook configs from cluster +// This function does not fail on error: +// Register will fail if the config exists, so there is no need to fail on error +func (wrc *WebhookRegistrationClient) Deregister() { + wrc.registrationClient.MutatingWebhookConfigurations().Delete(config.MutatingWebhookConfigurationName, &meta.DeleteOptions{}) + wrc.registrationClient.ValidatingWebhookConfigurations().Delete(config.ValidatingWebhookConfigurationName, &meta.DeleteOptions{}) } -func (mwr *MutationWebhookRegistration) constructWebhookConfig(configuration *rest.Config) (*admregapi.MutatingWebhookConfiguration, error) { - caData := ExtractCA(configuration) +func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configuration *rest.Config) (*admregapi.MutatingWebhookConfiguration, error) { + caData := extractCA(configuration) if len(caData) == 0 { return nil, errors.New("Unable to extract CA data from configuration") } - kubePolicyDeployment, err := mwr.kubeclient.GetKubePolicyDeployment() - - if err != nil { - return nil, err - } - return &admregapi.MutatingWebhookConfiguration{ ObjectMeta: meta.ObjectMeta{ - Name: config.WebhookConfigName, - Labels: config.WebhookConfigLabels, + Name: config.MutatingWebhookConfigurationName, + Labels: config.KubePolicyAppLabels, OwnerReferences: []meta.OwnerReference{ - meta.OwnerReference{ - APIVersion: config.DeploymentAPIVersion, - Kind: config.DeploymentKind, - Name: kubePolicyDeployment.ObjectMeta.Name, - UID: kubePolicyDeployment.ObjectMeta.UID, - }, + wrc.constructOwner(), }, }, Webhooks: []admregapi.Webhook{ - admregapi.Webhook{ - Name: config.MutationWebhookName, - ClientConfig: admregapi.WebhookClientConfig{ - Service: &admregapi.ServiceReference{ - Namespace: config.KubePolicyNamespace, - Name: config.WebhookServiceName, - Path: &config.WebhookServicePath, - }, - CABundle: caData, - }, - Rules: []admregapi.RuleWithOperations{ - admregapi.RuleWithOperations{ - Operations: []admregapi.OperationType{ - admregapi.Create, - }, - Rule: admregapi.Rule{ - APIGroups: []string{ - "*", - }, - APIVersions: []string{ - "*", - }, - Resources: []string{ - "*/*", - }, - }, - }, - }, - }, + constructWebhook( + config.MutatingWebhookName, + config.MutatingWebhookServicePath, + caData), }, }, nil } -func ExtractCA(config *rest.Config) (result []byte) { +func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configuration *rest.Config) (*admregapi.ValidatingWebhookConfiguration, error) { + caData := extractCA(configuration) + if len(caData) == 0 { + return nil, errors.New("Unable to extract CA data from configuration") + } + + return &admregapi.ValidatingWebhookConfiguration{ + ObjectMeta: meta.ObjectMeta{ + Name: config.ValidatingWebhookConfigurationName, + Labels: config.KubePolicyAppLabels, + OwnerReferences: []meta.OwnerReference{ + wrc.constructOwner(), + }, + }, + Webhooks: []admregapi.Webhook{ + constructWebhook( + config.ValidatingWebhookName, + config.ValidatingWebhookServicePath, + caData), + }, + }, nil +} + +func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook { + return admregapi.Webhook{ + Name: name, + ClientConfig: admregapi.WebhookClientConfig{ + Service: &admregapi.ServiceReference{ + Namespace: config.KubePolicyNamespace, + Name: config.WebhookServiceName, + Path: &servicePath, + }, + CABundle: caData, + }, + Rules: []admregapi.RuleWithOperations{ + admregapi.RuleWithOperations{ + Operations: []admregapi.OperationType{ + admregapi.Create, + }, + Rule: admregapi.Rule{ + APIGroups: []string{ + "*", + }, + APIVersions: []string{ + "*", + }, + Resources: []string{ + "*/*", + }, + }, + }, + }, + } +} + +func (wrc *WebhookRegistrationClient) constructOwner() meta.OwnerReference { + kubePolicyDeployment, err := wrc.kubeclient.GetKubePolicyDeployment() + + if err != nil { + return meta.OwnerReference{} + } + + return meta.OwnerReference{ + APIVersion: config.DeploymentAPIVersion, + Kind: config.DeploymentKind, + Name: kubePolicyDeployment.ObjectMeta.Name, + UID: kubePolicyDeployment.ObjectMeta.UID, + } +} + +// ExtractCA used for extraction CA from config +func extractCA(config *rest.Config) (result []byte) { fileName := config.TLSClientConfig.CAFile if fileName != "" { @@ -120,7 +175,7 @@ func ExtractCA(config *rest.Config) (result []byte) { } return result - } else { - return config.TLSClientConfig.CAData } + + return config.TLSClientConfig.CAData } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 6a91b69ef6..637da97e27 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -10,16 +10,14 @@ import ( "log" "net/http" "os" - "sort" "time" "github.com/nirmata/kube-policy/config" "github.com/nirmata/kube-policy/kubeclient" - kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" policylister "github.com/nirmata/kube-policy/pkg/client/listers/policy/v1alpha1" "github.com/nirmata/kube-policy/pkg/policyengine" "github.com/nirmata/kube-policy/pkg/policyengine/mutation" - "github.com/nirmata/kube-policy/utils" + tlsutils "github.com/nirmata/kube-policy/pkg/tls" v1beta1 "k8s.io/api/admission/v1beta1" "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -37,12 +35,12 @@ type WebhookServer struct { // NewWebhookServer creates new instance of WebhookServer accordingly to given configuration // Policy Controller and Kubernetes Client should be initialized in configuration func NewWebhookServer( - tlsPair *utils.TlsPemPair, - kubeclient *kubeclient.KubeClient, + tlsPair *tlsutils.TlsPemPair, + kubeClient *kubeclient.KubeClient, policyLister policylister.PolicyLister, logger *log.Logger) (*WebhookServer, error) { if logger == nil { - logger = log.New(os.Stdout, "HTTPS Server: ", log.LstdFlags|log.Lshortfile) + logger = log.New(os.Stdout, "Webhook Server: ", log.LstdFlags) } if tlsPair == nil { @@ -55,7 +53,7 @@ func NewWebhookServer( return nil, err } tlsConfig.Certificates = []tls.Certificate{pair} - policyEngine := policyengine.NewPolicyEngine(kubeclient, logger) + policyEngine := policyengine.NewPolicyEngine(kubeClient, logger) ws := &WebhookServer{ policyEngine: policyEngine, @@ -64,7 +62,8 @@ func NewWebhookServer( } mux := http.NewServeMux() - mux.HandleFunc(config.WebhookServicePath, ws.serve) + mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve) + mux.HandleFunc(config.ValidatingWebhookServicePath, ws.serve) ws.server = http.Server{ Addr: ":443", // Listen on port for HTTPS requests @@ -80,44 +79,125 @@ func NewWebhookServer( // Main server endpoint for all requests func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == config.WebhookServicePath { - admissionReview := ws.parseAdmissionReview(r, w) - if admissionReview == nil { - return - } + admissionReview := ws.bodyToAdmissionReview(r, w) + if admissionReview == nil { + return + } - var admissionResponse *v1beta1.AdmissionResponse - if AdmissionIsRequired(admissionReview.Request) { - admissionResponse = ws.Mutate(admissionReview.Request) - } + admissionReview.Response = &v1beta1.AdmissionResponse{ + Allowed: true, + } - if admissionResponse == nil { - admissionResponse = &v1beta1.AdmissionResponse{ - Allowed: true, - } + if KindIsSupported(admissionReview.Request.Kind.Kind) { + switch r.URL.Path { + case config.MutatingWebhookServicePath: + admissionReview.Response = ws.HandleMutation(admissionReview.Request) + case config.ValidatingWebhookServicePath: + admissionReview.Response = ws.HandleValidation(admissionReview.Request) } + } - admissionReview.Response = admissionResponse - admissionReview.Response.UID = admissionReview.Request.UID + admissionReview.Response.UID = admissionReview.Request.UID - responseJson, err := json.Marshal(admissionReview) - if err != nil { - http.Error(w, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError) - return - } + responseJson, err := json.Marshal(admissionReview) + if err != nil { + http.Error(w, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError) + return + } - ws.logger.Printf("Response body\n:%v", string(responseJson)) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err := w.Write(responseJson); err != nil { - http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) - } - } else { - http.Error(w, fmt.Sprintf("Unexpected method path: %v", r.URL.Path), http.StatusNotFound) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write(responseJson); err != nil { + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } } +// RunAsync TLS server in separate thread and returns control immediately +func (ws *WebhookServer) RunAsync() { + go func(ws *WebhookServer) { + err := ws.server.ListenAndServeTLS("", "") + if err != nil { + ws.logger.Fatal(err) + } + }(ws) + + ws.logger.Printf("Started Webhook Server") +} + +// Stop TLS server and returns control after the server is shut down +func (ws *WebhookServer) Stop() { + err := ws.server.Shutdown(context.Background()) + if err != nil { + // Error from closing listeners, or context timeout: + ws.logger.Printf("Server Shutdown error: %v", err) + ws.server.Close() + } +} + +// HandleMutation handles mutating webhook admission request +func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { + ws.logger.Printf("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + + policies, err := ws.policyLister.List(labels.NewSelector()) + if err != nil { + utilruntime.HandleError(err) + return nil + } + + var allPatches []mutation.PatchBytes + for _, policy := range policies { + ws.logger.Printf("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) + + policyPatches := ws.policyEngine.Mutate(*policy, request.Object.Raw) + allPatches = append(allPatches, policyPatches...) + + if len(policyPatches) > 0 { + namespace := mutation.ParseNamespaceFromObject(request.Object.Raw) + name := mutation.ParseNameFromObject(request.Object.Raw) + ws.logger.Printf("Policy %s applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) + } + } + + patchType := v1beta1.PatchTypeJSONPatch + return &v1beta1.AdmissionResponse{ + Allowed: true, + Patch: mutation.JoinPatches(allPatches), + PatchType: &patchType, + } +} + +// HandleValidation handles validating webhook admission request +func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { + ws.logger.Printf("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + + policies, err := ws.policyLister.List(labels.NewSelector()) + if err != nil { + utilruntime.HandleError(err) + return nil + } + + allowed := true + for _, policy := range policies { + ws.logger.Printf("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) + + if ok := ws.policyEngine.Validate(*policy, request.Object.Raw); !ok { + ws.logger.Printf("Validation has failed: %v\n", err) + utilruntime.HandleError(err) + allowed = false + } else { + ws.logger.Println("Validation is successful") + } + } + + return &v1beta1.AdmissionResponse{ + Allowed: allowed, + } +} + +// bodyToAdmissionReview creates AdmissionReview object from request body // Answers to the http.ResponseWriter if request is not valid -func (ws *WebhookServer) parseAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview { +func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview { var body []byte if request.Body != nil { if data, err := ioutil.ReadAll(request.Body); err == nil { @@ -143,81 +223,6 @@ func (ws *WebhookServer) parseAdmissionReview(request *http.Request, writer http http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed) return nil } else { - ws.logger.Printf("Request body:\n%v", string(body)) return admissionReview } } - -// Runs TLS server in separate thread and returns control immediately -func (ws *WebhookServer) RunAsync() { - go func(ws *WebhookServer) { - err := ws.server.ListenAndServeTLS("", "") - if err != nil { - ws.logger.Fatal(err) - } - }(ws) -} - -// Stops TLS server and returns control after the server is shut down -func (ws *WebhookServer) Stop() { - err := ws.server.Shutdown(context.Background()) - if err != nil { - // Error from closing listeners, or context timeout: - ws.logger.Printf("Server Shutdown error: %v", err) - ws.server.Close() - } -} - -func (ws *WebhookServer) Mutate(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - ws.logger.Printf("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation, request.UserInfo) - - policies, err := ws.getPolicies() - if err != nil { - utilruntime.HandleError(err) - return nil - } - if len(policies) == 0 { - return nil - } - - var allPatches []mutation.PatchBytes - for _, policy := range policies { - ws.logger.Printf("Applying policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - - policyPatches := ws.policyEngine.Mutate(policy, request.Object.Raw) - allPatches = append(allPatches, policyPatches...) - - if len(policyPatches) > 0 { - namespace := mutation.ParseNamespaceFromObject(request.Object.Raw) - name := mutation.ParseNameFromObject(request.Object.Raw) - ws.logger.Printf("Policy %s applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) - } - } - - patchType := v1beta1.PatchTypeJSONPatch - return &v1beta1.AdmissionResponse{ - Allowed: true, - Patch: mutation.JoinPatches(allPatches), - PatchType: &patchType, - } -} - -func (ws *WebhookServer) getPolicies() ([]kubepolicy.Policy, error) { - selector := labels.NewSelector() - cachedPolicies, err := ws.policyLister.List(selector) - if err != nil { - ws.logger.Printf("Error: %v", err) - return nil, err - } - - var policies []kubepolicy.Policy - for _, elem := range cachedPolicies { - policies = append(policies, *elem.DeepCopy()) - } - - sort.Slice(policies, func(i, j int) bool { - return policies[i].CreationTimestamp.Time.Before(policies[j].CreationTimestamp.Time) - }) - return policies, nil -} diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go index a3323ff917..da970e58c9 100644 --- a/pkg/webhooks/utils.go +++ b/pkg/webhooks/utils.go @@ -2,12 +2,11 @@ package webhooks import ( kubeclient "github.com/nirmata/kube-policy/kubeclient" - types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" - mutation "github.com/nirmata/kube-policy/pkg/policyengine/mutation" - "k8s.io/api/admission/v1beta1" ) -func kindIsSupported(kind string) bool { +// KindIsSupported checks kind to be prensent in +// SupportedKinds defined in config +func KindIsSupported(kind string) bool { for _, k := range kubeclient.GetSupportedKinds() { if k == kind { return true @@ -15,14 +14,3 @@ func kindIsSupported(kind string) bool { } return false } - -// Checks for admission if kind is supported -func AdmissionIsRequired(request *v1beta1.AdmissionRequest) bool { - // Here you can make additional hardcoded checks - return kindIsSupported(request.Kind.Kind) -} - -// Checks requests kind, name and labels to fit the policy -func IsRuleApplicableToRequest(policyResource types.ResourceDescription, request *v1beta1.AdmissionRequest) (bool, error) { - return mutation.IsRuleApplicableToResource(request.Object.Raw, policyResource) -} diff --git a/policycontroller/processPolicy.go b/policycontroller/processPolicy.go index a707f7325d..7165361cce 100644 --- a/policycontroller/processPolicy.go +++ b/policycontroller/processPolicy.go @@ -97,7 +97,7 @@ func (pc *PolicyController) filterResourceByRule(rule types.Rule) ([]runtime.Obj } // filter the resource by name and label - if ok, _ := mutation.IsRuleApplicableToResource(rawResource, rule.ResourceDescription); ok { + if ok, _ := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription); ok { targetResources = append(targetResources, resource) } }