Skip to content

Commit

Permalink
Add ClusterPolicy controller and automatically exclude chart-operator…
Browse files Browse the repository at this point in the history
… from custom policies (#29)

* Add ClusterPolicy controller and automatically exclude chart-operator from custom policies

Signed-off-by: Franco <[email protected]>
  • Loading branch information
fhielpos authored Oct 30, 2023
1 parent 1cc7ddb commit 5729033
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 81 deletions.
6 changes: 5 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ workflows:
app_catalog: "giantswarm-catalog"
app_catalog_test: "giantswarm-test-catalog"
chart: "kyverno-policy-operator"
requires:
- push-kyverno-policy-operator-to-docker
# Trigger job on git tag.
filters:
tags:
Expand All @@ -72,11 +74,13 @@ workflows:
app_catalog: "control-plane-catalog"
app_catalog_test: "control-plane-test-catalog"
chart: "kyverno-policy-operator"
requires:
- push-kyverno-policy-operator-to-docker
# Trigger job on git tag.
filters:
tags:
only: /^v.*/

# Push to collections
- architect/push-to-app-collection:
name: aws-app-collection
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Add `ClusterPolicy` controller to automatically exclude `chart-operator` ServiceAccount from custom policies.

## [0.0.3] - 2023-10-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/go-logr/logr v1.2.4
github.com/kyverno/kyverno v1.10.3
k8s.io/api v0.28.2
k8s.io/apimachinery v0.28.2
k8s.io/client-go v0.28.2
sigs.k8s.io/controller-runtime v0.15.1
Expand Down Expand Up @@ -264,7 +265,6 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
k8s.io/api v0.28.2 // indirect
k8s.io/apiextensions-apiserver v0.28.2 // indirect
k8s.io/component-base v0.28.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions helm/kyverno-policy-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ spec:
args:
{{- if .Values.policyOperator.destinationNamespace }}
- --destination-namespace={{ .Values.policyOperator.destinationNamespace }}
{{- end }}
{{- if .Values.policyOperator.chartOperatorExcemptedKinds }}
- --chart-operator-excempted-kinds={{ .Values.policyOperator.chartOperatorExcemptedKinds | join "," }}
{{- end }}
- --background-mode={{ .Values.policyOperator.exceptionBackgroundMode }}
ports:
Expand Down
3 changes: 3 additions & 0 deletions helm/kyverno-policy-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ policyOperator:
destinationNamespace: "policy-exceptions"
# Apply the generated PolicyExceptions also in Kyverno background scans. Changes audit results from fail to skip.
exceptionBackgroundMode: true
chartOperatorExcemptedKinds:
- PolicyException
- Namespace
184 changes: 184 additions & 0 deletions internal/controller/clusterpolicy_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"context"
"fmt"

kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

apierrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

// ClusterPolicyReconciler reconciles a ClusterPolicy object
type ClusterPolicyReconciler struct {
client.Client
Scheme *runtime.Scheme
Log logr.Logger
ExceptionList map[string]kyvernov1.ClusterPolicy
ExceptionKinds []string
}

//+kubebuilder:rbac:groups=kyverno.io,resources=clusterpolicies,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=kyverno.io,resources=clusterpolicies/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=kyverno.io,resources=clusterpolicies/finalizers,verbs=update

func (r *ClusterPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
_ = r.Log.WithValues("clusterpolicy", req.NamespacedName)

var clusterPolicy kyvernov1.ClusterPolicy

if err := r.Get(ctx, req.NamespacedName, &clusterPolicy); err != nil {
// Error fetching the report

// Check if the ClusterPolicy was deleted
if apierrors.IsNotFound(err) {
// Ignore
return ctrl.Result{}, nil
}

log.Log.Error(err, "unable to fetch ClusterPolicy")
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Check if the Policy has validate rules
if !clusterPolicy.HasValidate() {
return ctrl.Result{}, nil
}

// Inspect the Rules
for _, rule := range clusterPolicy.Spec.Rules {
// Check if the rule has a validate section
if rule.HasValidate() {
for _, kind := range rule.MatchResources.GetKinds() {
// Check for Namespace validation
for _, destinationKind := range r.ExceptionKinds {
if kind == destinationKind {
// Append exception to PolicyException
if _, exists := r.ExceptionList[clusterPolicy.Name]; !exists {
r.ExceptionList[clusterPolicy.Name] = clusterPolicy

// Template Kyverno Polex
policyException := kyvernov2alpha1.PolicyException{}

// Set namespace
policyException.Namespace = "giantswarm"

// Set name
policyException.Name = "chart-operator-generated-sa-bypass"

// Set labels
policyException.Labels = make(map[string]string)
policyException.Labels["app.kubernetes.io/managed-by"] = "kyverno-policy-operator"

// Set Background behaviour to false since this Polex is using Subjects
background := false
policyException.Spec.Background = &background

// Set Spec.Match.All
policyException.Spec.Match.All = templateResourceFilters(r.ExceptionKinds)

// Set .Spec.Exceptions
newExceptions := translatePoliciesToExceptions(r.ExceptionList)
policyException.Spec.Exceptions = newExceptions

// Patch PolicyException Kinds
gvks, unversioned, err := r.Scheme.ObjectKinds(&policyException)
if err != nil {
return ctrl.Result{}, err
}
if !unversioned && len(gvks) == 1 {
policyException.SetGroupVersionKind(gvks[0])
}

if err := r.CreateOrUpdate(ctx, &policyException); err != nil {
log.Log.Error(err, "Error creating PolicyException")
} else {
log.Log.Info(fmt.Sprintf("ClusterPolicy %s triggered a PolicyException update: %s", clusterPolicy.Name, client.ObjectKeyFromObject(&policyException)))
}

return ctrl.Result{}, nil
}
}
}
}
}
}

return ctrl.Result{}, nil
}

// CreateOrUpdate attempts first to patch the object given but if an IsNotFound error
// is returned it instead creates the resource.
func (r *ClusterPolicyReconciler) CreateOrUpdate(ctx context.Context, obj client.Object) error {
existingObj := unstructured.Unstructured{}
existingObj.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())

err := r.Get(ctx, client.ObjectKeyFromObject(obj), &existingObj)
switch {
case err == nil:
// Update:
obj.SetResourceVersion(existingObj.GetResourceVersion())
obj.SetUID(existingObj.GetUID())
return r.Patch(ctx, obj, client.MergeFrom(existingObj.DeepCopy()))
case errors.IsNotFound(err):
// Create:
return r.Create(ctx, obj)
default:
return err
}
}

func templateResourceFilters(kinds []string) kyvernov1.ResourceFilters {
var resourceFilters kyvernov1.ResourceFilters
trasnlatedResourceFilter := kyvernov1.ResourceFilter{
UserInfo: kyvernov1.UserInfo{
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "chart-operator",
Namespace: "giantswarm",
}},
},
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: kinds,
Operations: []kyvernov1.AdmissionOperation{"CREATE", "UPDATE"},
},
}
resourceFilters = append(resourceFilters, trasnlatedResourceFilter)

return resourceFilters
}

// SetupWithManager sets up the controller with the Manager.
func (r *ClusterPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&kyvernov1.ClusterPolicy{}).
Complete(r)
}
100 changes: 21 additions & 79 deletions internal/controller/policyexception_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
Expand Down Expand Up @@ -78,6 +80,7 @@ func (r *PolicyExceptionReconciler) Reconcile(ctx context.Context, req ctrl.Requ

// Create Kyverno exception
// Create a policy map for storing cluster policies to extract rules later
// TODO: Take this block out and move it to utils
policyMap := make(map[string]kyvernov1.ClusterPolicy)
for _, policy := range gsPolicyException.Spec.Policies {
var kyvernoPolicy kyvernov1.ClusterPolicy
Expand Down Expand Up @@ -130,59 +133,25 @@ func (r *PolicyExceptionReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, nil
}

func unorderedEqual(got, want []kyvernov2alpha1.Exception) bool {
// Check Length size first
if len(got) != len(want) {
return false
// CreateOrUpdate attempts first to patch the object given but if an IsNotFound error
// is returned it instead creates the resource.
func (r *PolicyExceptionReconciler) CreateOrUpdate(ctx context.Context, obj client.Object) error {
existingObj := unstructured.Unstructured{}
existingObj.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())

err := r.Get(ctx, client.ObjectKeyFromObject(obj), &existingObj)
switch {
case err == nil:
// Update:
obj.SetResourceVersion(existingObj.GetResourceVersion())
obj.SetUID(existingObj.GetUID())
return r.Patch(ctx, obj, client.MergeFrom(existingObj.DeepCopy()))
case errors.IsNotFound(err):
// Create:
return r.Create(ctx, obj)
default:
return err
}
// Create an exceptions map with the new desired Exceptions
exceptionMap := make(map[string][]string)
for _, exception := range want {
exceptionMap[exception.PolicyName] = exception.RuleNames
}
for _, exception := range got {
// Check if the Policy Name is still present in the new Exceptions
if _, exists := exceptionMap[exception.PolicyName]; !exists {
// The Policy is not present in the new array
// Arrays are not equals, exit
return false
} else {
// Check if the same RuleNames are still present in the new Exceptions
for _, oldRule := range exception.RuleNames {
found := false
// Check against every rule, exit if found
for _, newRule := range exceptionMap[exception.PolicyName] {
if newRule == oldRule {
// Found, break for
found = true
break
}
}
if !found {
// The arrays are not equals, exit
return false
}
// Rules are equals, continue
}
}
}
// Arrays are equals
return true
}

func translateTargetsToResourceFilters(polex giantswarmPolicy.PolicyException) kyvernov1.ResourceFilters {
resourceFilters := kyvernov1.ResourceFilters{}
for _, target := range polex.Spec.Targets {
trasnlatedResourceFilter := kyvernov1.ResourceFilter{
ResourceDescription: kyvernov1.ResourceDescription{
Namespaces: target.Namespaces,
Names: target.Names,
Kinds: generateExceptionKinds(target.Kind),
},
}
resourceFilters = append(resourceFilters, trasnlatedResourceFilter)
}
return resourceFilters
}

// generateKinds creates the subresources necessary for top level controllers like Deployment or StatefulSet
Expand All @@ -205,33 +174,6 @@ func generateExceptionKinds(resourceKind string) []string {
return exceptionKinds
}

// translatePoliciesToExceptions takes a Giant Swarm Policies array and transforms it into a Kyverno Exception array
func translatePoliciesToExceptions(policies map[string]kyvernov1.ClusterPolicy) []kyvernov2alpha1.Exception {
var exceptionArray []kyvernov2alpha1.Exception
for policyName, kyvernoPolicy := range policies {
kyvernoException := kyvernov2alpha1.Exception{
PolicyName: policyName,
RuleNames: generatePolicyRules(kyvernoPolicy),
}
exceptionArray = append(exceptionArray, kyvernoException)
}

return exceptionArray
}

// generatePolicyRules takes a Kyverno Policy name and generates a list of rules owned by that policy
func generatePolicyRules(kyvernoPolicy kyvernov1.ClusterPolicy) []string {
var rulesArray []string
for _, rule := range kyvernoPolicy.Spec.Rules {
rulesArray = append(rulesArray, rule.Name)
}
for _, autogenRule := range kyvernoPolicy.Status.Autogen.Rules {
rulesArray = append(rulesArray, autogenRule.Name)
}

return rulesArray
}

// SetupWithManager sets up the controller with the Manager.
func (r *PolicyExceptionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
Loading

0 comments on commit 5729033

Please sign in to comment.