diff --git a/Dockerfile b/Dockerfile index c389c098..11b9c4be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,16 +12,16 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go +COPY main.go main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY controller/ controller/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/controllers/general/general_controller.go b/controllers/general/general_controller.go new file mode 100644 index 00000000..a0c54586 --- /dev/null +++ b/controllers/general/general_controller.go @@ -0,0 +1,60 @@ +package general + +import ( + "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + intentv1 "github.com/5GSEC/nimbus/api/v1" +) + +// GeneralController is a struct that holds a Kubernetes client and a WatcherIntent. +type GeneralController struct { + Client client.Client // Client is used to interact with the Kubernetes API. + WatcherIntent *WatcherIntent // WatcherIntent is a custom struct to manage specific operations. +} + +// NewGeneralController creates a new instance of GeneralController. +func NewGeneralController(client client.Client) (*GeneralController, error) { + if client == nil { + // If the client is not provided, return an error. + return nil, fmt.Errorf("GeneralController: Client is nil") + } + + // Create a new WatcherIntent. + watcherIntent, err := NewWatcherIntent(client) + if err != nil { + // If there is an error in creating WatcherIntent, return an error. + return nil, fmt.Errorf("GeneralController: Error creating WatcherIntent: %v", err) + } + + // Return a new GeneralController instance with initialized fields. + return &GeneralController{ + Client: client, + WatcherIntent: watcherIntent, + }, nil +} + +// Reconcile is the method that will be called when there is an update to the resources being watched. +func (gc *GeneralController) Reconcile(ctx context.Context, req ctrl.Request) (*intentv1.SecurityIntent, error) { + if gc == nil { + // If the GeneralController instance is nil, return an error. + return nil, fmt.Errorf("GeneralController is nil") + } + if gc.WatcherIntent == nil { + // If the WatcherIntent is not set, return an error. + return nil, fmt.Errorf("WatcherIntent is nil") + } + + // Call the Reconcile method of WatcherIntent to handle the specific logic. + intent, err := gc.WatcherIntent.Reconcile(ctx, req) + if err != nil { + // If there is an error in reconciliation, return the error. + return nil, fmt.Errorf("Error in WatcherIntent.Reconcile: %v", err) + } + + // Return the intent and nil as error if reconciliation is successful. + return intent, nil +} diff --git a/controllers/general/watch_intent.go b/controllers/general/watch_intent.go new file mode 100644 index 00000000..7ffd7e36 --- /dev/null +++ b/controllers/general/watch_intent.go @@ -0,0 +1,64 @@ +package general + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + intentv1 "github.com/5GSEC/nimbus/api/v1" + "k8s.io/apimachinery/pkg/api/errors" +) + +// WatcherIntent is a struct that holds a Kubernetes client. +type WatcherIntent struct { + Client client.Client // Client to interact with Kubernetes resources. +} + +// NewWatcherIntent creates a new instance of WatcherIntent. +func NewWatcherIntent(client client.Client) (*WatcherIntent, error) { + if client == nil { + // Return an error if the client is not provided. + return nil, fmt.Errorf("WatcherIntent: Client is nil") + } + + // Return a new WatcherIntent instance with the provided client. + return &WatcherIntent{ + Client: client, + }, nil +} + +// Reconcile is the method that handles the reconciliation of the Kubernetes resources. +func (wi *WatcherIntent) Reconcile(ctx context.Context, req ctrl.Request) (*intentv1.SecurityIntent, error) { + log := log.FromContext(ctx) // Get the logger from the context. + + // Check if WatcherIntent or its client is not initialized. + if wi == nil || wi.Client == nil { + fmt.Println("WatcherIntent is nil or Client is nil in Reconcile") + return nil, fmt.Errorf("WatcherIntent or Client is not initialized") + } + + intent := &intentv1.SecurityIntent{} // Create an instance of SecurityIntent. + // Attempt to get the SecurityIntent resource from Kubernetes. + err := wi.Client.Get(ctx, types.NamespacedName{ + Name: req.Name, + Namespace: req.Namespace, + }, intent) + + if err != nil { + // Handle the case where the SecurityIntent resource is not found. + if errors.IsNotFound(err) { + log.Info("SecurityIntent resource not found. Ignoring since object must be deleted") + return nil, nil + } + // Log and return an error if there is a problem getting the SecurityIntent. + log.Error(err, "Failed to get SecurityIntent") + return nil, err + } + + // Return the SecurityIntent instance if found successfully. + return intent, nil +} diff --git a/controllers/policy/network_policy.go b/controllers/policy/network_policy.go new file mode 100644 index 00000000..159ac3c6 --- /dev/null +++ b/controllers/policy/network_policy.go @@ -0,0 +1,115 @@ +package policy + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + intentv1 "github.com/5GSEC/nimbus/api/v1" + ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + kubearmorpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy/api/security.kubearmor.com/v1" + + utils "github.com/5GSEC/nimbus/controllers/utils" +) + +// NetworkPolicyController struct to handle network policies. +type NetworkPolicyController struct { + Client client.Client // Client to interact with Kubernetes API. + Scheme *runtime.Scheme // Scheme defines the runtime scheme of the Kubernetes objects. +} + +// NewNetworkPolicyController creates a new instance of NetworkPolicyController. +func NewNetworkPolicyController(client client.Client, scheme *runtime.Scheme) *NetworkPolicyController { + return &NetworkPolicyController{ + Client: client, + Scheme: scheme, + } +} + +// HandlePolicy processes the network policies defined in the SecurityIntent resource. +func (npc *NetworkPolicyController) HandlePolicy(ctx context.Context, intent *intentv1.SecurityIntent) error { + log := log.FromContext(ctx) // Logger with context. + + // Build and apply/update Cilium Network Policy based on SecurityIntent. + ciliumPolicy := utils.BuildCiliumNetworkPolicySpec(ctx, intent).(*ciliumv2.CiliumNetworkPolicy) + err := utils.ApplyOrUpdatePolicy(ctx, npc.Client, ciliumPolicy, ciliumPolicy.Name) + if err != nil { + log.Error(err, "Failed to apply Cilium Network Policy", "Name", ciliumPolicy.Name) + return err + } + + // If SecurityIntent contains protocol resources, build and apply/update KubeArmor Network Policy. + if containsProtocolResource(intent) { + armorNetPolicy := utils.BuildKubeArmorPolicySpec(ctx, intent, utils.GetPolicyType(utils.IsHostPolicy(intent))).(*kubearmorpolicyv1.KubeArmorPolicy) + err = utils.ApplyOrUpdatePolicy(ctx, npc.Client, armorNetPolicy, armorNetPolicy.Name) + if err != nil { + log.Error(err, "Failed to apply KubeArmor Network Policy", "Name", armorNetPolicy.Name) + return err + } + } + + log.Info("Applied Network Policy", "PolicyName", intent.Name) + return nil +} + +// DeletePolicy removes the network policy associated with the SecurityIntent resource. +func (npc *NetworkPolicyController) DeletePolicy(ctx context.Context, intent *intentv1.SecurityIntent) error { + log := log.FromContext(ctx) + var err error + + // Delete KubeArmor or Cilium Network Policy based on the contents of SecurityIntent. + + if containsProtocolResource(intent) { + err = deleteNetworkPolicy(ctx, npc.Client, "KubeArmorPolicy", intent.Name, intent.Namespace) + if err != nil { + log.Error(err, "Failed to delete KubeArmor Network Policy", "Name", intent.Name) + return err + } + } else { + // Delete Cilium Network Policy by default + err = deleteNetworkPolicy(ctx, npc.Client, "CiliumNetworkPolicy", intent.Name, intent.Namespace) + if err != nil { + log.Error(err, "Failed to delete Cilium Network Policy", "Name", intent.Name) + return err + } + } + + log.Info("Deleted Network Policy", "PolicyName", intent.Name) + return nil +} + +// Additional helper functions for policy creation and deletion. +func createCiliumNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent) *ciliumv2.CiliumNetworkPolicy { + return utils.BuildCiliumNetworkPolicySpec(ctx, intent).(*ciliumv2.CiliumNetworkPolicy) +} + +func createKubeArmorNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent) *kubearmorpolicyv1.KubeArmorPolicy { + return utils.BuildKubeArmorPolicySpec(ctx, intent, "policy").(*kubearmorpolicyv1.KubeArmorPolicy) +} + +func applyCiliumNetworkPolicy(ctx context.Context, c client.Client, policy *ciliumv2.CiliumNetworkPolicy) { + utils.ApplyOrUpdatePolicy(ctx, c, policy, policy.Name) +} + +func applyKubeArmorNetworkPolicy(ctx context.Context, c client.Client, policy *kubearmorpolicyv1.KubeArmorPolicy) { + utils.ApplyOrUpdatePolicy(ctx, c, policy, policy.Name) +} + +// containsProtocolResource checks for the presence of protocol resources in SecurityIntent. +func containsProtocolResource(intent *intentv1.SecurityIntent) bool { + // Iterates through the intent resources to find if 'protocols' key is present. + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "protocols" { + return true + } + } + return false +} + +// deleteNetworkPolicy helps in deleting a specified network policy. +func deleteNetworkPolicy(ctx context.Context, c client.Client, policyType, name, namespace string) error { + // Utilizes utility function to delete the specified network policy. + return utils.DeletePolicy(ctx, c, policyType, name, namespace) +} diff --git a/controllers/policy/policy_controller.go b/controllers/policy/policy_controller.go new file mode 100644 index 00000000..f0ef1e81 --- /dev/null +++ b/controllers/policy/policy_controller.go @@ -0,0 +1,90 @@ +package policy + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + intentv1 "github.com/5GSEC/nimbus/api/v1" + utils "github.com/5GSEC/nimbus/controllers/utils" +) + +// Constant for the finalizer name used in the SecurityIntent resource. +const securityIntentFinalizer = "finalizer.securityintent.intent.security.nimbus.com" + +// PolicyController struct handles different types of policies. +type PolicyController struct { + Client client.Client // Client for interacting with Kubernetes API. + Scheme *runtime.Scheme // Scheme defines the runtime scheme of the Kubernetes objects. + SystemPolicyController *SystemPolicyController // Controller for handling system policies. + NetworkPolicyController *NetworkPolicyController // Controller for handling network policies. +} + +// NewPolicyController creates a new instance of PolicyController. +func NewPolicyController(client client.Client, scheme *runtime.Scheme) *PolicyController { + if client == nil || scheme == nil { + // Print an error and return nil if the client or scheme is not provided. + fmt.Println("PolicyController: Client or Scheme is nil") + return nil + } + + // Initialize and return a new PolicyController with system and network policy controllers. + return &PolicyController{ + Client: client, + Scheme: scheme, + SystemPolicyController: NewSystemPolicyController(client, scheme), + NetworkPolicyController: NewNetworkPolicyController(client, scheme), + } +} + +// Reconcile handles the reconciliation logic for the SecurityIntent resource. +func (pc *PolicyController) Reconcile(ctx context.Context, intent *intentv1.SecurityIntent) error { + log := log.FromContext(ctx) // Logger with context. + log.Info("Processing policy", "Name", intent.Name, "Type", intent.Spec.Intent.Type) + + var err error + + // Switch-case to handle different types of policies based on the intent type. + switch intent.Spec.Intent.Type { + case "system": + log.Info("Handling system policy") + err = pc.SystemPolicyController.HandlePolicy(ctx, intent) + case "network": + log.Info("Handling network policy") + err = pc.NetworkPolicyController.HandlePolicy(ctx, intent) + default: + log.Info("Unknown policy type", "Type", intent.Spec.Intent.Type) + } + + // Handling finalizer logic for clean up during delete operations. + if intent.ObjectMeta.DeletionTimestamp.IsZero() { + // If the resource is not being deleted, add the finalizer if it's not present. + if !utils.ContainsString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) { + intent.ObjectMeta.Finalizers = append(intent.ObjectMeta.Finalizers, securityIntentFinalizer) + err = pc.Client.Update(ctx, intent) + } + } else { + // If the resource is being deleted, process deletion based on policy type and remove finalizer. + if utils.ContainsString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) { + switch intent.Spec.Intent.Type { + case "system": + err = pc.SystemPolicyController.DeletePolicy(ctx, intent) + case "network": + err = pc.NetworkPolicyController.DeletePolicy(ctx, intent) + default: + err = fmt.Errorf("unknown policy type: %s", intent.Spec.Intent.Type) + } + + // Removing the finalizer after handling deletion. + intent.ObjectMeta.Finalizers = utils.RemoveString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) + if updateErr := pc.Client.Update(ctx, intent); updateErr != nil { + return updateErr + } + } + } + + return err +} diff --git a/controllers/policy/system_policy.go b/controllers/policy/system_policy.go new file mode 100644 index 00000000..8bcf92ab --- /dev/null +++ b/controllers/policy/system_policy.go @@ -0,0 +1,99 @@ +package policy + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + utils "github.com/5GSEC/nimbus/controllers/utils" + + intentv1 "github.com/5GSEC/nimbus/api/v1" + kubearmorhostpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy/api/security.kubearmor.com/v1" + kubearmorpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy/api/security.kubearmor.com/v1" +) + +// SystemPolicyController is a struct to handle system policies. +type SystemPolicyController struct { + Client client.Client // Client for interacting with Kubernetes API. + Scheme *runtime.Scheme // Scheme defines the runtime scheme of the Kubernetes objects. +} + +// NewSystemPolicyController creates a new instance of SystemPolicyController. +func NewSystemPolicyController(client client.Client, scheme *runtime.Scheme) *SystemPolicyController { + return &SystemPolicyController{ + Client: client, + Scheme: scheme, + } +} + +// HandlePolicy processes the system policy as defined in SecurityIntent. +func (spc *SystemPolicyController) HandlePolicy(ctx context.Context, intent *intentv1.SecurityIntent) error { + log := log.FromContext(ctx) // Logger with context. + + // Determine if the policy is a HostPolicy. + isHost := utils.IsHostPolicy(intent) + + var err error + if isHost { + // Create and apply a KubeArmorHostPolicy if it's a host policy. + hostPolicy := createKubeArmorHostPolicy(ctx, intent) + err = applyKubeArmorHostPolicy(ctx, spc.Client, hostPolicy) + } else { + // Create and apply a KubeArmorPolicy otherwise. + armorPolicy := createKubeArmorPolicy(ctx, intent) + err = applyKubeArmorPolicy(ctx, spc.Client, armorPolicy) + } + + if err != nil { + log.Error(err, "Failed to apply policy") + return err + } + + log.Info("Applied System Policy", "PolicyType", utils.GetPolicyType(isHost), "PolicyName", intent.Name) + return nil +} + +// DeletePolicy removes the system policy associated with the SecurityIntent resource. +func (spc *SystemPolicyController) DeletePolicy(ctx context.Context, intent *intentv1.SecurityIntent) error { + log := log.FromContext(ctx) + + isHost := utils.IsHostPolicy(intent) + policyType := utils.GetPolicyType(isHost) + + // Delete the system policy. + err := deleteSystemPolicy(ctx, spc.Client, policyType, intent.Name, intent.Namespace) + if err != nil { + log.Error(err, "Failed to delete policy") + return err + } + + log.Info("Deleted System Policy", "PolicyType", policyType, "PolicyName", intent.Name) + return nil +} + +// createKubeArmorHostPolicy(): Creates a KubeArmorHostPolicy object based on the given SecurityIntent +func createKubeArmorHostPolicy(ctx context.Context, intent *intentv1.SecurityIntent) *kubearmorhostpolicyv1.KubeArmorHostPolicy { + return utils.BuildKubeArmorPolicySpec(ctx, intent, "host").(*kubearmorhostpolicyv1.KubeArmorHostPolicy) +} + +// createKubeArmorPolicy creates a KubeArmorPolicy object based on the given SecurityIntent +func createKubeArmorPolicy(ctx context.Context, intent *intentv1.SecurityIntent) *kubearmorpolicyv1.KubeArmorPolicy { + return utils.BuildKubeArmorPolicySpec(ctx, intent, "policy").(*kubearmorpolicyv1.KubeArmorPolicy) +} + +// applyKubeArmorPolicy applies a KubeArmorPolicy to the Kubernetes cluster +func applyKubeArmorPolicy(ctx context.Context, c client.Client, policy *kubearmorpolicyv1.KubeArmorPolicy) error { + return utils.ApplyOrUpdatePolicy(ctx, c, policy, policy.Name) +} + +// applyKubeArmorHostPolicy applies a KubeArmorHostPolicy to the Kubernetes cluster +func applyKubeArmorHostPolicy(ctx context.Context, c client.Client, policy *kubearmorhostpolicyv1.KubeArmorHostPolicy) error { + return utils.ApplyOrUpdatePolicy(ctx, c, policy, policy.Name) +} + +func deleteSystemPolicy(ctx context.Context, c client.Client, policyType, name, namespace string) error { + // Utilizes utility function to delete the specified system policy. + return utils.DeletePolicy(ctx, c, policyType, name, namespace) +} diff --git a/controllers/securityintent_controller.go b/controllers/securityintent_controller.go new file mode 100644 index 00000000..bfbdfbd3 --- /dev/null +++ b/controllers/securityintent_controller.go @@ -0,0 +1,122 @@ +/* +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 controllers + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/5GSEC/nimbus/api/v1" + general "github.com/5GSEC/nimbus/controllers/general" + policy "github.com/5GSEC/nimbus/controllers/policy" +) + +// SecurityIntentReconciler reconciles a SecurityIntent object. +type SecurityIntentReconciler struct { + client.Client + Scheme *runtime.Scheme // Scheme defines the runtime scheme of the Kubernetes objects. + GeneralController *general.GeneralController // GeneralController is a custom controller for general operations. + PolicyController *policy.PolicyController // PolicyController is a custom controller for policy operations. +} + +// NewSecurityIntentReconciler creates a new SecurityIntentReconciler. +func NewSecurityIntentReconciler(client client.Client, scheme *runtime.Scheme) *SecurityIntentReconciler { + // Check if the client is nil. + if client == nil { + fmt.Println("SecurityIntentReconciler: Client is nil") + return nil + } + + // Initialize GeneralController; if failed, return nil. + generalController, err := general.NewGeneralController(client) + if err != nil || generalController == nil { // Check if generalController is nil. + fmt.Println("SecurityIntentReconciler: Failed to initialize GeneralController:", err) + return nil + } + + // Initialize PolicyController; if failed, return nil. + policyController := policy.NewPolicyController(client, scheme) + if policyController == nil { + fmt.Println("SecurityIntentReconciler: Failed to initialize PolicyController") + return nil + } + + // Return a new instance of SecurityIntentReconciler. + return &SecurityIntentReconciler{ + Client: client, + Scheme: scheme, + GeneralController: generalController, + PolicyController: policyController, + } +} + +//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the SecurityIntent object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcil + +// Reconcile handles the reconciliation of the SecurityIntent resources. +func (r *SecurityIntentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // Check if GeneralController or its components are properly initialized. + if r.GeneralController == nil { + fmt.Println("SecurityIntentReconciler: GeneralController is nil") + return ctrl.Result{}, fmt.Errorf("GeneralController is not properly initialized") + } + if r.GeneralController.WatcherIntent == nil { + fmt.Println("SecurityIntentReconciler: WatcherIntent is nil") + return ctrl.Result{}, fmt.Errorf("WatcherIntent is not properly initialized") + } + + // Perform Reconcile logic regardless of the state of GeneralController and WatcherIntent. + intent, err := r.GeneralController.Reconcile(ctx, req) + if err != nil { + return ctrl.Result{}, err + } + + if intent == nil { + return ctrl.Result{}, nil + } + + // Invoke the PolicyController's Reconcile method with the intent. + err = r.PolicyController.Reconcile(ctx, intent) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the reconciler with the provided manager. +func (r *SecurityIntentReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Set up the controller to manage SecurityIntent resources. + return ctrl.NewControllerManagedBy(mgr). + For(&v1.SecurityIntent{}). + Complete(r) +} diff --git a/internal/controller/suite_test.go b/controllers/suite_test.go similarity index 58% rename from internal/controller/suite_test.go rename to controllers/suite_test.go index 680373d4..59066b3c 100644 --- a/internal/controller/suite_test.go +++ b/controllers/suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package controllers import ( "fmt" @@ -39,21 +39,24 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment +var cfg *rest.Config // cfg will hold the Kubernetes rest configuration. +var k8sClient client.Client // k8sClient is the Kubernetes client for the test environment. +var testEnv *envtest.Environment // testEnv is the environment for running the tests. +// TestControllers is the entry point for testing the controllers package. func TestControllers(t *testing.T) { - RegisterFailHandler(Fail) + RegisterFailHandler(Fail) // Register a Ginkgo fail handler. - RunSpecs(t, "Controller Suite") + RunSpecs(t, "Controller Suite") // Run the Ginkgo specs for the 'Controller Suite'. } var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + // Setup for the test suite, executed before any specs are run. + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) // Set up the logger. By("bootstrapping test environment") testEnv = &envtest.Environment{ + // CRDDirectoryPaths specifies the paths to the CRD manifests. CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, @@ -67,24 +70,26 @@ var _ = BeforeSuite(func() { } var err error - // cfg is defined in this file globally. - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) + cfg, err = testEnv.Start() // Start the test environment. + Expect(err).NotTo(HaveOccurred()) // Assert that starting the environment does not produce an error. + Expect(cfg).NotTo(BeNil()) // Assert that the config is not nil. + // Add the intentv1 API scheme to the runtime scheme. err = intentv1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) // Assert that adding the scheme does not produce an error. - //+kubebuilder:scaffold:scheme + // Scaffold additional schemes here if needed. + // Initialize the Kubernetes client for the test environment. k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) // Assert that creating the client does not produce an error. + Expect(k8sClient).NotTo(BeNil()) // Assert that the client is not nil. }) var _ = AfterSuite(func() { + // Teardown for the test suite, executed after all specs have run. By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) + err := testEnv.Stop() // Stop the test environment. + Expect(err).NotTo(HaveOccurred()) // Assert that stopping the environment does not produce an error. }) diff --git a/controllers/utils/utils_policy.go b/controllers/utils/utils_policy.go new file mode 100644 index 00000000..327795f2 --- /dev/null +++ b/controllers/utils/utils_policy.go @@ -0,0 +1,615 @@ +package utils + +import ( + "context" + "fmt" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "reflect" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + intentv1 "github.com/5GSEC/nimbus/api/v1" + ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/policy/api" + kubearmorhostpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy/api/security.kubearmor.com/v1" + kubearmorpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy/api/security.kubearmor.com/v1" +) + +// --------------------------------------------------- +// -------- Creation of Policy Specifications -------- +// --------------------------------------------------- + +// BuildKubeArmorPolicySpec creates a policy specification (either KubeArmorPolicy or KubeArmorHostPolicy) +// based on the provided SecurityIntent and the type of policy. +func BuildKubeArmorPolicySpec(ctx context.Context, intent *intentv1.SecurityIntent, policyType string) interface{} { + log := log.FromContext(ctx) + // Logging the creation of a KubeArmor policy. + log.Info("Creating KubeArmorPolicy", "Name", intent.Name) + + matchLabels := convertToMapString(extractLabels(intent)) + + // Convert extracted information into specific KubeArmor policy types. + if policyType == "host" { + return &kubearmorhostpolicyv1.KubeArmorHostPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: intent.Name, + Namespace: intent.Namespace, + }, + Spec: kubearmorhostpolicyv1.KubeArmorHostPolicySpec{ + NodeSelector: kubearmorhostpolicyv1.NodeSelectorType{ + MatchLabels: matchLabels, + }, + Process: convertToKubeArmorHostPolicyProcessType(extractProcessPolicy(intent)), + File: convertToKubeArmorHostPolicyFileType(extractFilePolicy(intent)), + Capabilities: convertToKubeArmorHostPolicyCapabilitiesType(extractCapabilitiesPolicy(intent)), + Network: convertToKubeArmorHostPolicyNetworkType(extractNetworkPolicy(intent)), + Action: kubearmorhostpolicyv1.ActionType(formatAction(intent.Spec.Intent.Action)), + }, + } + } else { + return &kubearmorpolicyv1.KubeArmorPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: intent.Name, + Namespace: intent.Namespace, + }, + Spec: kubearmorpolicyv1.KubeArmorPolicySpec{ + Selector: kubearmorpolicyv1.SelectorType{ + MatchLabels: matchLabels, + }, + Process: convertToKubeArmorPolicyProcessType(extractProcessPolicy(intent)), + File: convertToKubeArmorPolicyFileType(extractFilePolicy(intent)), + Capabilities: convertToKubeArmorPolicyCapabilitiesType(extractCapabilitiesPolicy(intent)), + Network: convertToKubeArmorPolicyNetworkType(extractNetworkPolicy(intent)), + Action: kubearmorpolicyv1.ActionType(formatAction(intent.Spec.Intent.Action)), + }, + } + } +} + +// BuildCiliumNetworkPolicySpec creates a Cilium network policy specification based on the provided SecurityIntent. +func BuildCiliumNetworkPolicySpec(ctx context.Context, intent *intentv1.SecurityIntent) interface{} { + // Logging the creation of a Cilium Network Policy. + log := log.FromContext(ctx) + log.Info("Creating CiliumNetworkPolicy", "Name", intent.Name) + + // Utilize utility functions to construct a Cilium network policy from the intent. + endpointSelector := getEndpointSelector(intent) + ingressDenyRules := getIngressDenyRules(intent) + + // Build and return a Cilium Network Policy based on the intent. + return &ciliumv2.CiliumNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: intent.Name, + Namespace: intent.Namespace, + }, + Spec: &api.Rule{ + EndpointSelector: endpointSelector, + IngressDeny: ingressDenyRules, + }, + } +} + +// ---------------------------------------- +// -------- Conversion Functions ---------- +// ---------------------------------------- + +// Various conversion functions to transform data from the SecurityIntent into specific policy types. +// These functions handle different aspects like processing types, file types, network types, etc., +// and convert them into the format required by KubeArmor and Cilium policies. + +// convertToMapString converts a slice of interfaces to a map of string key-value pairs. +func convertToMapString(slice []interface{}) map[string]string { + // Iterate through the slice, converting each item into a map and merging them into a single map. + result := make(map[string]string) + for _, item := range slice { + for key, value := range item.(map[string]string) { + result[key] = value + } + } + return result +} + +func convertToKubeArmorHostPolicyProcessType(slice []interface{}) kubearmorhostpolicyv1.ProcessType { + var result kubearmorhostpolicyv1.ProcessType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + // The 'Pattern' field is of type string, so it can be assigned directly + result.MatchPatterns = append(result.MatchPatterns, kubearmorhostpolicyv1.ProcessPatternType{ + Pattern: str, + }) + } + return result +} + +func convertToKubeArmorHostPolicyFileType(slice []interface{}) kubearmorhostpolicyv1.FileType { + var result kubearmorhostpolicyv1.FileType + for _, item := range slice { + result.MatchPaths = append(result.MatchPaths, kubearmorhostpolicyv1.FilePathType{ + Path: kubearmorhostpolicyv1.MatchPathType(item.(string)), + }) + } + return result +} + +func convertToKubeArmorHostPolicyNetworkType(slice []interface{}) kubearmorhostpolicyv1.NetworkType { + var result kubearmorhostpolicyv1.NetworkType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + // Requires explicit type conversion to MatchNetworkProtocolStringType + protocol := kubearmorhostpolicyv1.MatchNetworkProtocolStringType(str) + result.MatchProtocols = append(result.MatchProtocols, kubearmorhostpolicyv1.MatchNetworkProtocolType{ + Protocol: protocol, + }) + } + return result +} + +func convertToKubeArmorHostPolicyCapabilitiesType(slice []interface{}) kubearmorhostpolicyv1.CapabilitiesType { + var result kubearmorhostpolicyv1.CapabilitiesType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + // Convert to MatchCapabilitiesStringType + capability := kubearmorhostpolicyv1.MatchCapabilitiesStringType(str) + result.MatchCapabilities = append(result.MatchCapabilities, kubearmorhostpolicyv1.MatchCapabilitiesType{ + Capability: capability, + }) + } + return result +} + +func convertToKubeArmorPolicyProcessType(slice []interface{}) kubearmorpolicyv1.ProcessType { + var result kubearmorpolicyv1.ProcessType + for _, item := range slice { + if str, ok := item.(string); ok { + result.MatchPatterns = append(result.MatchPatterns, kubearmorpolicyv1.ProcessPatternType{ + Pattern: str, + }) + } + } + return result +} + +func convertToKubeArmorPolicyFileType(slice []interface{}) kubearmorpolicyv1.FileType { + var result kubearmorpolicyv1.FileType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + result.MatchPaths = append(result.MatchPaths, kubearmorpolicyv1.FilePathType{ + Path: kubearmorpolicyv1.MatchPathType(str), + }) + } + return result +} + +func convertToKubeArmorPolicyCapabilitiesType(slice []interface{}) kubearmorpolicyv1.CapabilitiesType { + var result kubearmorpolicyv1.CapabilitiesType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + result.MatchCapabilities = append(result.MatchCapabilities, kubearmorpolicyv1.MatchCapabilitiesType{ + Capability: kubearmorpolicyv1.MatchCapabilitiesStringType(str), + }) + } + return result +} + +func convertToKubeArmorPolicyNetworkType(slice []interface{}) kubearmorpolicyv1.NetworkType { + var result kubearmorpolicyv1.NetworkType + for _, item := range slice { + str, ok := item.(string) + if !ok { + continue // or appropriate error handling + } + result.MatchProtocols = append(result.MatchProtocols, kubearmorpolicyv1.MatchNetworkProtocolType{ + Protocol: kubearmorpolicyv1.MatchNetworkProtocolStringType(str), + }) + } + return result +} + +// -------------------------------------- +// -------- Utility Functions ---------- +// -------------------------------------- + +// GetPolicyType returns the type of policy as a string based on whether it's a host policy. +func GetPolicyType(isHost bool) string { + if isHost { + return "KubeArmorHostPolicy" + } + return "KubeArmorPolicy" +} + +// IsHostPolicy determines if the given SecurityIntent is a Host Policy. +func IsHostPolicy(intent *intentv1.SecurityIntent) bool { + // Check for specific labels in the CEL field of the intent to determine if it's a host policy. + for _, cel := range intent.Spec.Selector.CEL { + if strings.Contains(cel, "kubernetes.io") { + return true + } + } + return false +} + +// cleanLabelKey cleans up the label key to remove unnecessary prefixes. +func cleanLabelKey(key string) string { + // Remove specific prefixes from the label key, if present. + if strings.HasPrefix(key, "object.metadata.labels.") { + return strings.TrimPrefix(key, "object.metadata.labels.") + } + return key +} + +// parseCELExpression parses a CEL expression and extracts labels as key-value pairs. +func parseCELExpression(expression string) map[string]string { + // Process the CEL expression and convert it into a map of labels. + parsedLabels := make(map[string]string) + + expression = strings.TrimSpace(expression) + expressions := strings.Split(expression, " && ") + + for _, expr := range expressions { + parts := strings.Split(expr, " == ") + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + key = strings.TrimPrefix(key, "object.metadata.labels['") + key = strings.TrimSuffix(key, "']") + value = strings.Trim(value, "'") + + parsedLabels[key] = value + } + } + + return parsedLabels +} + +// extractLabels(): Extracts and returns labels from a CEL expression and Match fields +func extractLabels(intent *intentv1.SecurityIntent) []interface{} { + labels := make([]interface{}, 0) + + isHost := IsHostPolicy(intent) + + // Extract labels from Match.Any + for _, filter := range intent.Spec.Selector.Match.Any { + for key, val := range filter.Resources.MatchLabels { + if strings.HasPrefix(key, "object.metadata.labels.") { + cleanKey := cleanLabelKey(key) + labels = append(labels, map[string]string{cleanKey: val}) + } + } + } + + // Extract labels from Match.All + for _, filter := range intent.Spec.Selector.Match.All { + for key, val := range filter.Resources.MatchLabels { + if strings.HasPrefix(key, "object.metadata.labels.") { + cleanKey := cleanLabelKey(key) + labels = append(labels, map[string]string{cleanKey: val}) + } + } + } + + // Extract labels from CEL expressions + if isHost { + // Special handling for KubeArmorHostPolicy + for _, cel := range intent.Spec.Selector.CEL { + if strings.Contains(cel, "kubernetes.io/") { + parts := strings.Split(cel, " == ") + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + key = strings.TrimPrefix(key, "object.metadata.labels['") + key = strings.TrimSuffix(key, "']") + value := strings.Trim(parts[1], "'") + cleanKey := cleanLabelKey(key) + labels = append(labels, map[string]string{cleanKey: value}) + } + } + } + } else { + // General handling for other policies + for _, cel := range intent.Spec.Selector.CEL { + if strings.HasPrefix(cel, "object.metadata.labels.") { + parsedLabels := parseCELExpression(cel) + for key, val := range parsedLabels { + cleanKey := cleanLabelKey(key) + labels = append(labels, map[string]string{cleanKey: val}) + } + } + } + } + + return labels +} + +func extractProcessPolicy(intent *intentv1.SecurityIntent) []interface{} { + var matchPatterns []interface{} + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "commands" { + for _, cmd := range resource.Val { + matchPatterns = append(matchPatterns, map[string]string{"Pattern": cmd}) + } + } + } + return matchPatterns +} + +func extractFilePolicy(intent *intentv1.SecurityIntent) []interface{} { + var matchPaths []interface{} + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "paths" { + for _, path := range resource.Val { + matchPaths = append(matchPaths, map[string]string{"Path": path}) + } + } + } + return matchPaths +} + +func extractCapabilitiesPolicy(intent *intentv1.SecurityIntent) []interface{} { + var matchCapabilities []interface{} + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "capabilities" { + for _, capability := range resource.Val { + matchCapabilities = append(matchCapabilities, map[string]string{"Capability": capability}) + } + } + } + return matchCapabilities +} + +// extractNetworkPolicy() - Extracts network policy from SecurityIntent and returns it as a slice of interface{} +func extractNetworkPolicy(intent *intentv1.SecurityIntent) []interface{} { + var matchNetworkProtocols []interface{} + + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "protocols" { + for _, protocol := range resource.Val { + protocolMap := map[string]string{"Protocol": protocol} + matchNetworkProtocols = append(matchNetworkProtocols, protocolMap) + } + } + } + + return matchNetworkProtocols +} + +// getEndpointSelector creates an endpoint selector from the SecurityIntent. +func getEndpointSelector(intent *intentv1.SecurityIntent) api.EndpointSelector { + // Create an Endpoint Selector based on matched labels extracted from the intent. + matchLabels := make(map[string]string) + + // Matching labels to a "Match Any" filter + for _, filter := range intent.Spec.Selector.Match.Any { + for key, val := range filter.Resources.MatchLabels { + matchLabels[key] = val + } + } + + // Matching labels that fit the "Match All" filter + for _, filter := range intent.Spec.Selector.Match.All { + for key, val := range filter.Resources.MatchLabels { + matchLabels[key] = val + } + } + + // Create an Endpoint Selector based on matched labels + return api.NewESFromMatchRequirements(matchLabels, nil) +} + +// getIngressDenyRules generates ingress deny rules from SecurityIntent. +func getIngressDenyRules(intent *intentv1.SecurityIntent) []api.IngressDenyRule { + // Process the intent to create ingress deny rules. + var ingressDenyRules []api.IngressDenyRule + + for _, resource := range intent.Spec.Intent.Resource { + if resource.Key == "ingress" { + for _, val := range resource.Val { + cidr, port, protocol := splitCIDRAndPort(val) + + ingressRule := api.IngressDenyRule{ + ToPorts: api.PortDenyRules{ + { + Ports: []api.PortProtocol{ + { + Port: port, + Protocol: parseProtocol(protocol), + }, + }, + }, + }, + } + + if cidr != "" { + ingressRule.FromCIDRSet = []api.CIDRRule{{Cidr: api.CIDR(cidr)}} + } + + ingressDenyRules = append(ingressDenyRules, ingressRule) + } + } + } + + return ingressDenyRules +} + +// splitCIDRAndPort separates CIDR, port, and protocol information from a combined string. +func splitCIDRAndPort(cidrAndPort string) (string, string, string) { + // Split the string into CIDR, port, and protocol components, with handling for different formats. + + // Default protocol is TCP + defaultProtocol := "TCP" + + // Separate strings based on '-' + split := strings.Split(cidrAndPort, "-") + + // If there are three separate elements, return the CIDR, port, and protocol separately + if len(split) == 3 { + return split[0], split[1], split[2] + } + + // If there are two separate elements, return the CIDR, port, and default protocol + if len(split) == 2 { + return split[0], split[1], defaultProtocol + } + + // If there is only one element, return the CIDR, empty port, and default protocol + return cidrAndPort, "", defaultProtocol +} + +func parseProtocol(protocol string) api.L4Proto { + // Convert protocol string to L4Proto type. + switch strings.ToUpper(protocol) { + case "TCP": + return api.ProtoTCP + case "UDP": + return api.ProtoUDP + case "ICMP": + return api.ProtoICMP + default: + return api.ProtoTCP + } +} + +func formatAction(action string) string { + // Convert action string to a specific format. + switch strings.ToLower(action) { + case "block": + return "Block" + case "audit": + return "Audit" + case "allow": + return "Allow" + default: + return action + } +} + +// ---------------------------------------- +// -------- Apply & Update Policy -------- +// ---------------------------------------- + +// ApplyOrUpdatePolicy applies or updates the given policy. +func ApplyOrUpdatePolicy(ctx context.Context, c client.Client, policy client.Object, policyName string) error { + // Update the policy if it already exists, otherwise create a new one. + log := log.FromContext(ctx) + + var existingPolicy client.Object + var policySpec interface{} + + switch p := policy.(type) { + case *kubearmorpolicyv1.KubeArmorPolicy: + existingPolicy = &kubearmorpolicyv1.KubeArmorPolicy{} + policySpec = p.Spec + case *kubearmorhostpolicyv1.KubeArmorHostPolicy: + existingPolicy = &kubearmorhostpolicyv1.KubeArmorHostPolicy{} + policySpec = p.Spec + case *ciliumv2.CiliumNetworkPolicy: + existingPolicy = &ciliumv2.CiliumNetworkPolicy{} + policySpec = p.Spec + default: + return fmt.Errorf("unsupported policy type") + } + + err := c.Get(ctx, types.NamespacedName{Name: policyName, Namespace: policy.GetNamespace()}, existingPolicy) + if err != nil && !errors.IsNotFound(err) { + // Other error handling + log.Error(err, "Failed to get existing policy", "policy", policyName) + return err + } + + if errors.IsNotFound(err) { + // Create a policy if it doesn't exist + if err := c.Create(ctx, policy); err != nil { + log.Error(err, "Failed to apply policy", "policy", policyName) + return err + } + log.Info("Policy created", "Name", policyName) + } else { + // Update if policy already exists (compares specs only) + existingSpec := reflect.ValueOf(existingPolicy).Elem().FieldByName("Spec").Interface() + if !reflect.DeepEqual(policySpec, existingSpec) { + reflect.ValueOf(existingPolicy).Elem().FieldByName("Spec").Set(reflect.ValueOf(policySpec)) + if err := c.Update(ctx, existingPolicy); err != nil { + log.Error(err, "Failed to update policy", "policy", policyName) + return err + } + log.Info("Policy updated", "Name", policyName) + } else { + log.Info("Policy unchanged", "Name", policyName) + } + } + return nil +} + +// ---------------------------------------- +// ----------- Delete Policy ------------- +// ---------------------------------------- + +// DeletePolicy deletes a policy based on type, name, and namespace. +func DeletePolicy(ctx context.Context, c client.Client, policyType, name, namespace string) error { + // Process the deletion request based on policy type. + + var policy client.Object + log := log.FromContext(ctx) + + switch policyType { + case "KubeArmorPolicy": + policy = &kubearmorpolicyv1.KubeArmorPolicy{} + case "KubeArmorHostPolicy": + policy = &kubearmorhostpolicyv1.KubeArmorHostPolicy{} + case "CiliumNetworkPolicy": + policy = &ciliumv2.CiliumNetworkPolicy{} + default: + return fmt.Errorf("unknown policy type: %s", policyType) + } + + policy.SetName(name) + policy.SetNamespace(namespace) + + if err := c.Delete(ctx, policy); client.IgnoreNotFound(err) != nil { + log.Error(err, "Failed to delete policy", "Type", policyType, "Name", name, "Namespace", namespace) + return err + } + return nil +} + +// containsString checks if a string is included in a slice. +func ContainsString(slice []string, s string) bool { + // Iterate through the slice to check if the string exists. + + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +// removeString removes a specific string from a slice. +func RemoveString(slice []string, s string) []string { + // Create a new slice excluding the specified string. + result := []string{} + for _, item := range slice { + if item != s { + result = append(result, item) + } + } + return result +} diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 075e3f10..bd5c3a39 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -54,13 +54,14 @@ $ kustomize version Commands to run Nimbus operators: -### 1. Generate code -Generate the necessary code based on the API definition +### 1. Apply API group resources +Apply API group resources ``` $ make generate ``` + ### 2. Install CRD Install Custom Resource Definitions in a Kubernetes Cluster @@ -68,9 +69,12 @@ Install Custom Resource Definitions in a Kubernetes Cluster $ make install ``` +📌 Steps 1 and 2 are required if you have a completely clean environment, as they allow the server to find the requested resources. + ### 3. Run Operators Run the operator in your local environment ``` +$ make build $ make run ```

diff --git a/docs/Quick-tutorials.md b/docs/Quick-tutorials.md index c860797b..f9a2b919 100644 --- a/docs/Quick-tutorials.md +++ b/docs/Quick-tutorials.md @@ -5,7 +5,7 @@ busybox-pod ``` -$ kubectl apply -f ./test-yaml/busybox-pod.yaml +$ kubectl apply -f ./test-yaml/env/busybox-pod.yaml ``` ```yaml @@ -25,7 +25,7 @@ spec: redis-pod ``` -$ kubectl apply -f ./test-yaml/redis-pod.yaml +$ kubectl apply -f ./test-yaml/env/redis-pod.yaml ``` ```yaml apiVersion: v1 @@ -65,7 +65,7 @@ $ make run ### Create and apply intent file ``` -$ kubectl apply -f ./test-yaml/intent-redis.yaml +$ kubectl apply -f ./test-yaml/intents/network/intent-redis.yaml ``` ```yaml apiVersion: intent.security.nimbus.com/v1 @@ -167,5 +167,5 @@ You can see that the policy was applied, so access to port 6379 on the endpoint ``` -$ kubectl delete -f ./test-yaml/intent-redis.yaml +$ kubectl delete -f ./test-yaml/intents/network/intent-redis.yaml ``` \ No newline at end of file diff --git a/go.mod b/go.mod index 919465b1..3692a362 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/5GSEC/nimbus go 1.20 require ( + github.com/cilium/cilium v1.14.3 + github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy v0.0.0-20230616113436-0f9e047493a0 + github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy v0.0.0-20230622041458-52e38e236598 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 k8s.io/apimachinery v0.28.3 @@ -15,7 +18,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cilium/cilium v1.14.3 // indirect github.com/cilium/ebpf v0.10.1-0.20230626090016-654491c8a500 // indirect github.com/cilium/proxy v0.0.0-20230623092907-8fddead4e52c // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -54,8 +56,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy v0.0.0-20230616113436-0f9e047493a0 // indirect - github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy v0.0.0-20230622041458-52e38e236598 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/internal/controller/securityintent_controller.go b/internal/controller/securityintent_controller.go deleted file mode 100644 index 3e118274..00000000 --- a/internal/controller/securityintent_controller.go +++ /dev/null @@ -1,978 +0,0 @@ -/* -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" - "k8s.io/apimachinery/pkg/api/errors" - "reflect" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - intentv1 "github.com/5GSEC/nimbus/api/v1" - ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" - "github.com/cilium/cilium/pkg/policy/api" - kubearmorhostpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy/api/security.kubearmor.com/v1" - kubearmorpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorPolicy/api/security.kubearmor.com/v1" -) - -// SecurityIntentReconciler reconciles a SecurityIntent object -type SecurityIntentReconciler struct { - client.Client - Scheme *runtime.Scheme -} - -//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=intent.security.nimbus.com,resources=securityintents/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the SecurityIntent object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcil - -const securityIntentFinalizer = "finalizer.securityintent.intent.security.nimbus.com" - -// Reconcile attempts to bring the current state of the cluster closer to the desired state. -func (r *SecurityIntentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := log.FromContext(ctx) - - // Retrieve the SecurityIntent resource - intent := &intentv1.SecurityIntent{} - err := r.Get(ctx, req.NamespacedName, intent) - - // Handle cases where the resource is deleted or an error occurred during retrieval - if err != nil { - if errors.IsNotFound(err) { - // Log and exit when the resource is deleted - log.Info("SecurityIntent resource not found. Ignoring since object must be deleted") - return ctrl.Result{}, nil - } - // Other errors are returned after logging - log.Error(err, "Failed to get SecurityIntent") - return ctrl.Result{}, err - } - - // Gets a SecurityIntent object - intent, err = GeneralController(ctx, r.Client, req.NamespacedName.Name, req.NamespacedName.Namespace) - if err != nil { - log.Error(err, "Failed fetching SecurityIntent") - return ctrl.Result{}, err - } - - // Create and enforce security intent-based policies as actual policies - if err = PolicyController(ctx, intent, r.Client); err != nil { - log.Error(err, "Failed applying policy") - return ctrl.Result{}, err - } - - // Finalizer - if intent.ObjectMeta.DeletionTimestamp.IsZero() { - // If the SecurityIntent is not being deleted - if !containsString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) { - intent.ObjectMeta.Finalizers = append(intent.ObjectMeta.Finalizers, securityIntentFinalizer) - if err := r.Update(ctx, intent); err != nil { - return ctrl.Result{}, err - } - - } - } else { - // If the SecurityIntent is being deleted - if containsString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) { - // Related policy deletion logic - if err := deleteRelatedPolicies(ctx, r.Client, intent); err != nil { - return ctrl.Result{}, err - } - - // Remove Finalizer - intent.ObjectMeta.Finalizers = removeString(intent.ObjectMeta.Finalizers, securityIntentFinalizer) - if err := r.Update(ctx, intent); err != nil { - return ctrl.Result{}, err - } - - } - return ctrl.Result{}, nil - } - - log.Info("Successfully reconciled SecurityIntent", "intent", intent.Name) - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *SecurityIntentReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&intentv1.SecurityIntent{}). - Complete(r) -} - -// ------------------------------ -// ---- General Controller ------ -// ------------------------------ - -// GeneralController() : Detect and process SecurityIntent objects -func GeneralController(ctx context.Context, client client.Client, intentName string, namespace string) (*intentv1.SecurityIntent, error) { - log := log.FromContext(ctx) - log.Info("Searching for SecurityIntent", "Name", intentName, "Namespace", namespace) - - // Search for a SecurityIntent object - var intent intentv1.SecurityIntent - if err := client.Get(ctx, types.NamespacedName{Name: intentName, Namespace: namespace}, &intent); err != nil { - log.Error(err, "Failed searching SecurityIntent") - return nil, err - } - - log.Info("Found SecurityIntent", "Name", intent.Name, "Namespace", namespace) - return &intent, nil -} - -// extractLabels(): Extracts and returns labels from a CEL expression and Match fields -func extractLabels(intent *intentv1.SecurityIntent, isHostPolicy bool) map[string]string { - labels := make(map[string]string) - - // Extract labels from Match.Any - for _, filter := range intent.Spec.Selector.Match.Any { - for key, val := range filter.Resources.MatchLabels { - if strings.HasPrefix(key, "object.metadata.labels.") { - cleanKey := cleanLabelKey(key) - labels[cleanKey] = val - } - } - } - - // Extract labels from Match.All - for _, filter := range intent.Spec.Selector.Match.All { - for key, val := range filter.Resources.MatchLabels { - if strings.HasPrefix(key, "object.metadata.labels.") { - cleanKey := cleanLabelKey(key) - labels[cleanKey] = val - } - } - } - - // Extract labels from CEL expressions - if isHostPolicy { - // Special handling for KubeArmorHostPolicy - for _, cel := range intent.Spec.Selector.CEL { - if strings.Contains(cel, "kubernetes.io/") { - parts := strings.Split(cel, " == ") - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - key = strings.TrimPrefix(key, "object.metadata.labels['") - key = strings.TrimSuffix(key, "']") - value := strings.Trim(parts[1], "'") - cleanKey := cleanLabelKey(key) - labels[cleanKey] = value - } - } - } - } else { - // General handling for other policies - for _, cel := range intent.Spec.Selector.CEL { - if strings.HasPrefix(cel, "object.metadata.labels.") { - parsedLabels := parseCELExpression(cel) - for key, val := range parsedLabels { - cleanKey := cleanLabelKey(key) - labels[cleanKey] = val - } - } - } - } - - return labels -} - -// cleanLabelKey(): Cleans up the label key to remove unnecessary prefixes -func cleanLabelKey(key string) string { - if strings.HasPrefix(key, "object.metadata.labels.") { - return strings.TrimPrefix(key, "object.metadata.labels.") - } - return key -} - -// parseCELExpression: Parses a CEL expression and extracts labels -func parseCELExpression(expression string) map[string]string { - parsedLabels := make(map[string]string) - - expression = strings.TrimSpace(expression) - expressions := strings.Split(expression, " && ") - - for _, expr := range expressions { - parts := strings.Split(expr, " == ") - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - key = strings.TrimPrefix(key, "object.metadata.labels['") - key = strings.TrimSuffix(key, "']") - value = strings.Trim(value, "'") - - parsedLabels[key] = value - } - } - - return parsedLabels -} - -// ------------------------------ -// ---- Policy Controller ------- -// ------------------------------ - -// PolicyController() : Creates, applies, modifies, and deletes actual policies based on the policies defined in the SecurityIntent. -func PolicyController(ctx context.Context, intent *intentv1.SecurityIntent, c client.Client) error { - log := log.FromContext(ctx) - log.Info("Processing policy", "Name", intent.Name, "Type", intent.Spec.Intent.Type) - - switch intent.Spec.Intent.Type { - case "system": - if err := handleSystemPolicy(ctx, intent, c); err != nil { - log.Error(err, "Failed handling system policy") - return err - } - case "network": - if err := handleNetworkPolicy(ctx, intent, c); err != nil { - log.Error(err, "Failed handling network policy") - return err - } - if containsProtocolResource(intent) { - if err := handleKubeArmorNetworkPolicy(ctx, intent, c); err != nil { - log.Error(err, "Failed handling KubeArmor network policy") - return err - } - } - default: - log.Info("Encountered unknown policy type", "type", intent.Spec.Intent.Type) - } - log.Info("Policy processing completed", "Name", intent.Name) - return nil -} - -// --------------------------------------- -// ------ System Policy -------- -// --------------------------------------- - -// handleSystemPolicy(): Handle system policy -func handleSystemPolicy(ctx context.Context, intent *intentv1.SecurityIntent, c client.Client) error { - log := log.FromContext(ctx) - - // Create and apply the appropriate policy based on whether it's a HostPolicy or not - if isHostPolicy(intent) { - hostPolicy := createKubeArmorHostPolicy(ctx, intent, isHostPolicy(intent)) - if err := applyKubeArmorHostPolicy(ctx, c, hostPolicy); err != nil { - return err - } - log.Info("Applied System Policy") - } else { - armorPolicy := createKubeArmorPolicy(ctx, intent, isHostPolicy(intent)) - if err := applyKubeArmorPolicy(ctx, c, armorPolicy); err != nil { - return err - } - log.Info("Applied System Policy") - } - return nil -} - -// --------------------------------------- -// ------ Generate System Policy -------- -// --------------------------------------- - -// createKubeArmorHostPolicy(): Creates a KubeArmorHostPolicy object based on the given SecurityIntent -func createKubeArmorHostPolicy(ctx context.Context, intent *intentv1.SecurityIntent, isHost bool) *kubearmorhostpolicyv1.KubeArmorHostPolicy { - log := log.FromContext(ctx) - log.Info("Creating KubeArmorHostPolicy", "Name", intent.Name) - - process, file, capabilities, network := buildPolicySpec(intent) - if process == nil { - process = &kubearmorhostpolicyv1.ProcessType{} - } - if file == nil { - file = &kubearmorhostpolicyv1.FileType{} - } - if capabilities == nil { - capabilities = &kubearmorhostpolicyv1.CapabilitiesType{} - } - if network == nil { - network = &kubearmorhostpolicyv1.NetworkType{} - } - - matchLabels := extractLabels(intent, isHost) - - spec := kubearmorhostpolicyv1.KubeArmorHostPolicySpec{ - NodeSelector: kubearmorhostpolicyv1.NodeSelectorType{ - MatchLabels: matchLabels, - }, - Process: *process, - File: *file, - Capabilities: *capabilities, - Network: *network, - } - - log.Info("KubeArmorHostPolicy created", "Name", intent.Name) - return &kubearmorhostpolicyv1.KubeArmorHostPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: intent.Name, - Namespace: intent.Namespace, - }, - Spec: spec, - } -} - -// createKubeArmorPolicy() - Creates a KubeArmor policy based on SecurityIntent -func createKubeArmorPolicy(ctx context.Context, intent *intentv1.SecurityIntent, isHost bool) *kubearmorpolicyv1.KubeArmorPolicy { - log := log.FromContext(ctx) - log.Info("Creating KubeArmorPolicy", "Name", intent.Name) - - // Extract label match conditions from SecurityIntent - matchLabels := extractLabels(intent, isHost) - - // Constructing KubeArmorPolicySpec based on the intent - armorPolicySpec := kubearmorpolicyv1.KubeArmorPolicySpec{ - Selector: kubearmorpolicyv1.SelectorType{ - MatchLabels: matchLabels, - }, - Process: extractProcessPolicy(intent), - File: extractFilePolicy(intent), - Network: extractNetworkPolicy(intent), - Capabilities: extractCapabilitiesPolicy(intent), - Action: formatActionForArmorPolicy(intent.Spec.Intent.Action), - } - - // Creating the KubeArmorPolicy object - log.Info("KubeArmorPolicy created", "Name", intent.Name) - return &kubearmorpolicyv1.KubeArmorPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: intent.Name, - Namespace: intent.Namespace, - }, - Spec: armorPolicySpec, - } -} - -// buildPolicySpec(): Configures the Process, File, Capabilities, Network fields in the given SecurityIntent -func buildPolicySpec(intent *intentv1.SecurityIntent) (*kubearmorhostpolicyv1.ProcessType, *kubearmorhostpolicyv1.FileType, *kubearmorhostpolicyv1.CapabilitiesType, *kubearmorhostpolicyv1.NetworkType) { - var process *kubearmorhostpolicyv1.ProcessType - var file *kubearmorhostpolicyv1.FileType - var capabilities *kubearmorhostpolicyv1.CapabilitiesType - var network *kubearmorhostpolicyv1.NetworkType - - // Configure the Process and File fields - if len(intent.Spec.Intent.Resource) > 0 { - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "paths" { - for _, val := range resource.Val { - if isExecutablePath(val) { - // Executable path, add to process match patterns - if process == nil { - process = &kubearmorhostpolicyv1.ProcessType{ - MatchPatterns: []kubearmorhostpolicyv1.ProcessPatternType{}, - Action: kubearmorhostpolicyv1.ActionType(intent.Spec.Intent.Action), - } - } - process.MatchPatterns = append(process.MatchPatterns, kubearmorhostpolicyv1.ProcessPatternType{ - Pattern: val, - }) - } else { - // Non-executable path, add to file match paths - if file == nil { - file = &kubearmorhostpolicyv1.FileType{ - MatchPaths: []kubearmorhostpolicyv1.FilePathType{}, - Action: kubearmorhostpolicyv1.ActionType(intent.Spec.Intent.Action), - } - } - file.MatchPaths = append(file.MatchPaths, kubearmorhostpolicyv1.FilePathType{ - Path: kubearmorhostpolicyv1.MatchPathType(val), - }) - } - } - } - } - } - - // Set default values for capabilities and network if they are nil - if capabilities == nil { - capabilities = &kubearmorhostpolicyv1.CapabilitiesType{ - MatchCapabilities: []kubearmorhostpolicyv1.MatchCapabilitiesType{}, - } - } - if network == nil { - network = &kubearmorhostpolicyv1.NetworkType{ - MatchProtocols: []kubearmorhostpolicyv1.MatchNetworkProtocolType{}, - } - } - - return process, file, capabilities, network -} - -// isHostPolicy(): Determines if the given SecurityIntent is a Host Policy -func isHostPolicy(intent *intentv1.SecurityIntent) bool { - // Check for the kubernetes.io label in the CEL field - for _, cel := range intent.Spec.Selector.CEL { - if strings.Contains(cel, "kubernetes.io") { - return true - } - } - return false -} - -// isExecutablePath: Determines if a given path is likely an executable -func isExecutablePath(path string) bool { - // Define common executable paths - executablePaths := []string{"/bin", "/usr/bin", "/sbin", "/usr/sbin", "/usr/local/bin"} - - // Check if the path starts with any of the common executable paths - for _, prefix := range executablePaths { - if strings.HasPrefix(path, prefix) { - return true - } - } - return false -} - -// formatPatternWithValcel() : Form the entire pattern based on the 'valcel' pattern -func formatPatternWithValcel(valcel, command string) string { - if strings.HasPrefix(valcel, "pattern: ") { - pattern := strings.TrimPrefix(valcel, "pattern: ") - // e.g. 'pattern: /**/{command}' - return strings.Replace(pattern, "{command}", command, -1) - } - return command -} - -// extractMatchPaths(): Extract file paths from the resource -func extractMatchPaths(resources []intentv1.Resource) []kubearmorhostpolicyv1.FilePathType { - var matchPaths []kubearmorhostpolicyv1.FilePathType - for _, resource := range resources { - if resource.Key == "paths" { - for _, path := range resource.Val { - matchPaths = append(matchPaths, kubearmorhostpolicyv1.FilePathType{ - Path: kubearmorhostpolicyv1.MatchPathType(path), - }) - } - } - } - return matchPaths -} - -// extractProcessPolicy() - Extracts process policy from SecurityIntent -func extractProcessPolicy(intent *intentv1.SecurityIntent) kubearmorpolicyv1.ProcessType { - var processPolicy kubearmorpolicyv1.ProcessType - - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "commands" { - for _, cmd := range resource.Val { - processPolicy.MatchPatterns = append(processPolicy.MatchPatterns, kubearmorpolicyv1.ProcessPatternType{ - Pattern: cmd, - }) - } - } - } - - return processPolicy -} - -// extractFilePolicy() - Extracts file policy from SecurityIntent -func extractFilePolicy(intent *intentv1.SecurityIntent) kubearmorpolicyv1.FileType { - var filePolicy kubearmorpolicyv1.FileType - - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "paths" { - for _, path := range resource.Val { - filePolicy.MatchPaths = append(filePolicy.MatchPaths, kubearmorpolicyv1.FilePathType{ - Path: kubearmorpolicyv1.MatchPathType(path), // 올바른 타입으로 수정 - }) - } - } - } - - return filePolicy -} - -// extractCapabilitiesPolicy() - Extracts capabilities policy from SecurityIntent -func extractCapabilitiesPolicy(intent *intentv1.SecurityIntent) kubearmorpolicyv1.CapabilitiesType { - var capabilitiesPolicy kubearmorpolicyv1.CapabilitiesType - - // Check for capabilities in the SecurityIntent's resources - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "capabilities" { - for _, capability := range resource.Val { - // Add each capability to the MatchCapabilities slice - capabilitiesPolicy.MatchCapabilities = append(capabilitiesPolicy.MatchCapabilities, kubearmorpolicyv1.MatchCapabilitiesType{ - Capability: kubearmorpolicyv1.MatchCapabilitiesStringType(capability), - }) - } - } - } - - // Ensure that MatchCapabilities is always initialized, even if it's empty - if len(capabilitiesPolicy.MatchCapabilities) == 0 { - capabilitiesPolicy.MatchCapabilities = []kubearmorpolicyv1.MatchCapabilitiesType{} - } - - return capabilitiesPolicy -} - -// --------------------------------------- -// ------- Apply System Policy ---------- -// --------------------------------------- - -func applyKubeArmorPolicy(ctx context.Context, c client.Client, policy *kubearmorpolicyv1.KubeArmorPolicy) error { - return applyOrUpdatePolicy(ctx, c, policy, policy.Name) -} - -func applyKubeArmorHostPolicy(ctx context.Context, c client.Client, policy *kubearmorhostpolicyv1.KubeArmorHostPolicy) error { - return applyOrUpdatePolicy(ctx, c, policy, policy.Name) -} - -// ------------------------------- -// ------ Network Policy -------- -// ------------------------------- - -// handleNetworkPolicy(): Handle Cilium network policy -func handleNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent, c client.Client) error { - log := log.FromContext(ctx) - - ciliumPolicy := createCiliumNetworkPolicy(ctx, intent) - if err := applyCiliumNetworkPolicy(ctx, c, ciliumPolicy); err != nil { - log.Error(err, "Failed applying CiliumNetworkPolicy", "policy", ciliumPolicy) - return err - } - log.Info("Applied CiliumNetworkPolicy", "policy", ciliumPolicy) - - return nil // Returns nil on success -} - -// handleKubeArmorNetworkPolicy(): Handle KubeArmor network policy -func handleKubeArmorNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent, c client.Client) error { - log := log.FromContext(ctx) - - // Only create KubeArmor Network Policy if intent type is 'network' - if intent.Spec.Intent.Type == "network" { - armorPolicy := createKubeArmorNetworkPolicy(ctx, intent, isHostPolicy(intent)) - if err := applyKubeArmorNetworkPolicy(ctx, c, armorPolicy); err != nil { - log.Error(err, "Failed applying KubeArmorPolicy", "policy", armorPolicy.Name) - return err - } - log.Info("Applied KubeArmorPolicy", "policy", armorPolicy) - } - - return nil -} - -// ---------------------------------------- -// ------ Generate Network Policy -------- -// ---------------------------------------- - -// createKubeArmorNetworkPolicy(): Creates a KubeArmor policy for network-related intents -func createKubeArmorNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent, isHost bool) *kubearmorpolicyv1.KubeArmorPolicy { - log := log.FromContext(ctx) - log.Info("Creating KubearmorPolicy", "Name", intent.Name) - - matchLabels := extractLabels(intent, isHost) - - armorPolicySpec := kubearmorpolicyv1.KubeArmorPolicySpec{ - Selector: kubearmorpolicyv1.SelectorType{ - MatchLabels: matchLabels, - }, - Network: extractNetworkPolicy(intent), - Action: formatActionForArmorPolicy(intent.Spec.Intent.Action), - Capabilities: kubearmorpolicyv1.CapabilitiesType{ // 추가된 부분 - MatchCapabilities: []kubearmorpolicyv1.MatchCapabilitiesType{}, - }, - } - - return &kubearmorpolicyv1.KubeArmorPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: intent.Name, - Namespace: intent.Namespace, - }, - Spec: armorPolicySpec, - } -} - -// createCiliumNetworkPolicy() : Create a CiliumNetworkPolicy -func createCiliumNetworkPolicy(ctx context.Context, intent *intentv1.SecurityIntent) *ciliumv2.CiliumNetworkPolicy { - log := log.FromContext(ctx) - log.Info("Creating CiliumNetworkPolicy", "Name", intent.Name) - - // Create a Endpoint Selector field - endpointSelector := getEndpointSelector(intent) - // Create a Ingress Deny Rule - ingressDenyRules := getIngressDenyRules(intent) - - //egressRules := getEgressRules(intent) - - // Initializing a CiliumNetworkPolicy Object - log.Info("CiliumNetworkPolicy created", "Name", intent.Name) - return &ciliumv2.CiliumNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: intent.Name, // Policy name - Namespace: intent.Namespace, // Policy namespace - }, - Spec: &api.Rule{ - EndpointSelector: endpointSelector, // endpoint selector - IngressDeny: ingressDenyRules, // Ingress Deny Rule - //Egress: egressRules, - }, - } -} - -// extractNetworkPolicy() - Extracts network policy from SecurityIntent -func extractNetworkPolicy(intent *intentv1.SecurityIntent) kubearmorpolicyv1.NetworkType { - var networkPolicy kubearmorpolicyv1.NetworkType - - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "protocols" { - for _, protocol := range resource.Val { - networkPolicy.MatchProtocols = append(networkPolicy.MatchProtocols, kubearmorpolicyv1.MatchNetworkProtocolType{ - Protocol: kubearmorpolicyv1.MatchNetworkProtocolStringType(protocol), - }) - } - } - } - - // Ensure that MatchProtocols is always initialized, even if it's empty - if networkPolicy.MatchProtocols == nil { - networkPolicy.MatchProtocols = []kubearmorpolicyv1.MatchNetworkProtocolType{} - } - - return networkPolicy -} - -// getEndpointSelector(): Creates an endpoint selector from the SecurityIntent -func getEndpointSelector(intent *intentv1.SecurityIntent) api.EndpointSelector { - matchLabels := make(map[string]string) - - // Matching labels to a "Match Any" filter - for _, filter := range intent.Spec.Selector.Match.Any { - for key, val := range filter.Resources.MatchLabels { - matchLabels[key] = val - } - } - - // Matching labels that fit the "Match All" filter - for _, filter := range intent.Spec.Selector.Match.All { - for key, val := range filter.Resources.MatchLabels { - matchLabels[key] = val - } - } - - // Create an Endpoint Selector based on matched labels - return api.NewESFromMatchRequirements(matchLabels, nil) -} - -// getIngressDenyRules(): Generate ingress deny rules from SecurityIntent -func getIngressDenyRules(intent *intentv1.SecurityIntent) []api.IngressDenyRule { - var ingressDenyRules []api.IngressDenyRule - - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "ingress" { - for _, val := range resource.Val { - cidr, port, protocol := splitCIDRAndPort(val) - - ingressRule := api.IngressDenyRule{ - ToPorts: api.PortDenyRules{ - { - Ports: []api.PortProtocol{ - { - Port: port, - Protocol: parseProtocol(protocol), - }, - }, - }, - }, - } - - if cidr != "" { - ingressRule.FromCIDRSet = []api.CIDRRule{{Cidr: api.CIDR(cidr)}} - } - - ingressDenyRules = append(ingressDenyRules, ingressRule) - } - } - } - - return ingressDenyRules -} - -// splitCIDRAndPort(): Separates CIDR, port, and protocol information -func splitCIDRAndPort(cidrAndPort string) (string, string, string) { - // Default protocol is TCP - defaultProtocol := "TCP" - - // Separate strings based on '-' - split := strings.Split(cidrAndPort, "-") - - // If there are three separate elements, return the CIDR, port, and protocol separately - if len(split) == 3 { - return split[0], split[1], split[2] - } - - // If there are two separate elements, return the CIDR, port, and default protocol - if len(split) == 2 { - return split[0], split[1], defaultProtocol - } - - // If there is only one element, return the CIDR, empty port, and default protocol - return cidrAndPort, "", defaultProtocol -} - -// --------------------------------------- -// ------- Apply Network Policy --------- -// --------------------------------------- - -func applyCiliumNetworkPolicy(ctx context.Context, c client.Client, policy *ciliumv2.CiliumNetworkPolicy) error { - return applyOrUpdatePolicy(ctx, c, policy, policy.Name) -} - -func applyKubeArmorNetworkPolicy(ctx context.Context, c client.Client, policy *kubearmorpolicyv1.KubeArmorPolicy) error { - return applyOrUpdatePolicy(ctx, c, policy, policy.Name) -} - -// ------------------------------ -// ------ Apply Policy -------- -// ------------------------------ - -// applyOrUpdatePolicy: Applies or updates the given policy -// Update the policy if it already exists, otherwise create a new one. -func applyOrUpdatePolicy(ctx context.Context, c client.Client, policy client.Object, policyName string) error { - log := log.FromContext(ctx) - - var existingPolicy client.Object - var policySpec interface{} - - switch p := policy.(type) { - case *kubearmorpolicyv1.KubeArmorPolicy: - existingPolicy = &kubearmorpolicyv1.KubeArmorPolicy{} - policySpec = p.Spec - case *kubearmorhostpolicyv1.KubeArmorHostPolicy: - existingPolicy = &kubearmorhostpolicyv1.KubeArmorHostPolicy{} - policySpec = p.Spec - case *ciliumv2.CiliumNetworkPolicy: - existingPolicy = &ciliumv2.CiliumNetworkPolicy{} - policySpec = p.Spec - default: - return fmt.Errorf("unsupported policy type") - } - - err := c.Get(ctx, types.NamespacedName{Name: policyName, Namespace: policy.GetNamespace()}, existingPolicy) - if err != nil && !errors.IsNotFound(err) { - // Other error handling - log.Error(err, "Failed to get existing policy", "policy", policyName) - return err - } - - if errors.IsNotFound(err) { - // Create a policy if it doesn't exist - if err := c.Create(ctx, policy); err != nil { - log.Error(err, "Failed to apply policy", "policy", policyName) - return err - } - log.Info("Policy created", "Name", policyName) - } else { - // Update if policy already exists (compares specs only) - existingSpec := reflect.ValueOf(existingPolicy).Elem().FieldByName("Spec").Interface() - if !reflect.DeepEqual(policySpec, existingSpec) { - reflect.ValueOf(existingPolicy).Elem().FieldByName("Spec").Set(reflect.ValueOf(policySpec)) - if err := c.Update(ctx, existingPolicy); err != nil { - log.Error(err, "Failed to update policy", "policy", policyName) - return err - } - log.Info("Policy updated", "Name", policyName) - } else { - log.Info("Policy unchanged", "Name", policyName) - } - } - return nil -} - -// ------------------------------ -// ------ Delete Policy -------- -// ------------------------------ - -// deleteRelatedPolicies: Delete the associated policies based on the SecurityIntent -func deleteRelatedPolicies(ctx context.Context, c client.Client, intent *intentv1.SecurityIntent) error { - log := log.FromContext(ctx) - log.Info("Deleting related policies", "Name", intent.Name) - - switch intent.Spec.Intent.Type { - case "system": - if isHostPolicy(intent) { - // Delete KubeArmorHostPolicy - if err := deleteKubeArmorHostPolicy(ctx, c, intent.Name, intent.Namespace); err != nil { - return err - } - } else { - // Delete KubeArmorPolicy - if err := deleteKubeArmorPolicy(ctx, c, intent.Name, intent.Namespace); err != nil { - return err - } - } - - case "network": - // Delete CiliumNetworkPolicy - if err := deleteCiliumNetworkPolicy(ctx, c, intent.Name, intent.Namespace); err != nil { - return err - } - // Delete the protocol-specific KubeArmorPolicy if it exists - if containsProtocolResource(intent) { - if err := deleteKubeArmorPolicy(ctx, c, intent.Name, intent.Namespace); err != nil { - return err - } - } - } - - log.Info("Related policies deleted successfully", "Name", intent.Name) - return nil -} - -// deleteKubeArmorPolicy: Delete KubeArmorPolicy -func deleteKubeArmorPolicy(ctx context.Context, c client.Client, name, namespace string) error { - log := log.FromContext(ctx) - - armorPolicy := &kubearmorpolicyv1.KubeArmorPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - if err := c.Delete(ctx, armorPolicy); client.IgnoreNotFound(err) != nil { - log.Error(err, "Failed to delete KubeArmorPolicy", "Name", name, "Namespace", namespace) - return err - } - return nil -} - -// deleteKubeArmorHostPolicy: Delete KubeArmorHostPolicy -func deleteKubeArmorHostPolicy(ctx context.Context, c client.Client, name, namespace string) error { - log := log.FromContext(ctx) - - hostPolicy := &kubearmorhostpolicyv1.KubeArmorHostPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - if err := c.Delete(ctx, hostPolicy); client.IgnoreNotFound(err) != nil { - log.Error(err, "Failed to delete KubeArmorHostPolicy", "Name", name, "Namespace", namespace) - return err - } - return nil -} - -// deleteCiliumNetworkPolicy: Delete CiliumNetworkPolicy -func deleteCiliumNetworkPolicy(ctx context.Context, c client.Client, name, namespace string) error { - log := log.FromContext(ctx) - - ciliumPolicy := &ciliumv2.CiliumNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - if err := c.Delete(ctx, ciliumPolicy); client.IgnoreNotFound(err) != nil { - log.Error(err, "Failed to delete CiliumNetworkPolicy", "Name", name, "Namespace", namespace) - return err - } - return nil -} - -// ----------------------- -// ------- etc ---------- -// ----------------------- - -// formatAction(): Convert action value to a valid format -func formatActionForHostPolicy(action string) kubearmorhostpolicyv1.ActionType { - switch strings.ToLower(action) { - case "block": - return kubearmorhostpolicyv1.ActionType("Block") - case "audit": - return kubearmorhostpolicyv1.ActionType("Audit") - case "allow": - return kubearmorhostpolicyv1.ActionType("Allow") - default: - return kubearmorhostpolicyv1.ActionType(action) - } -} - -// formatActionForArmorPolicy(): Convert action value to kubearmorpolicyv1.ActionType -func formatActionForArmorPolicy(action string) kubearmorpolicyv1.ActionType { - switch strings.ToLower(action) { - case "block": - return kubearmorpolicyv1.ActionType("Block") - case "audit": - return kubearmorpolicyv1.ActionType("Audit") - case "allow": - return kubearmorpolicyv1.ActionType("Allow") - default: - return kubearmorpolicyv1.ActionType(action) - } -} - -func parseProtocol(protocol string) api.L4Proto { - switch strings.ToUpper(protocol) { - case "TCP": - return api.ProtoTCP - case "UDP": - return api.ProtoUDP - case "ICMP": - return api.ProtoICMP - default: - return api.ProtoTCP - } -} - -// containsProtocolResource: Checking for the presence of protocol resources in SecurityIntent -func containsProtocolResource(intent *intentv1.SecurityIntent) bool { - for _, resource := range intent.Spec.Intent.Resource { - if resource.Key == "protocols" { - return true - } - } - return false -} - -// containsString: Checking if a string is included in a slice -func containsString(slice []string, s string) bool { - for _, item := range slice { - if item == s { - return true - } - } - return false -} - -// removeString: Remove a specific string from a slice -func removeString(slice []string, s string) []string { - result := []string{} - for _, item := range slice { - if item != s { - result = append(result, item) - } - } - return result -} diff --git a/main.go b/main.go index 9592e580..08f3e2a4 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,9 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" intentv1 "github.com/5GSEC/nimbus/api/v1" - "github.com/5GSEC/nimbus/internal/controller" + "github.com/5GSEC/nimbus/controllers" + general "github.com/5GSEC/nimbus/controllers/general" + policy "github.com/5GSEC/nimbus/controllers/policy" ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" kubearmorhostpolicyv1 "github.com/kubearmor/KubeArmor/pkg/KubeArmorHostPolicy/api/security.kubearmor.com/v1" @@ -41,12 +43,14 @@ import ( //+kubebuilder:scaffold:imports ) +// Global variable for registering schemes. var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") + scheme = runtime.NewScheme() // Scheme registers the API types that the client and server should know. + setupLog = ctrl.Log.WithName("setup") // Logger specifically for setup. ) func init() { + // In init, various Kubernetes and custom resources are added to the scheme. utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(intentv1.AddToScheme(scheme)) @@ -58,9 +62,11 @@ func init() { } func main() { + // Flags for the command line parameters like metrics address, leader election, etc. var metricsAddr string var enableLeaderElection bool var probeAddr string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -72,8 +78,10 @@ func main() { opts.BindFlags(flag.CommandLine) flag.Parse() + // Setting the logger with the provided options. ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // Creating a new manager which will manage all the controllers. mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddr}, @@ -97,13 +105,26 @@ func main() { os.Exit(1) } - if err = (&controller.SecurityIntentReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + // Setting up the GeneralController and PolicyController. + generalController, err := general.NewGeneralController(mgr.GetClient()) + if err != nil { + setupLog.Error(err, "unable to create GeneralController") + os.Exit(1) + } + + policyController := policy.NewPolicyController(mgr.GetClient(), mgr.GetScheme()) + + // Setting up the SecurityIntentReconciler controller with the manager. + if err = (&controllers.SecurityIntentReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + GeneralController: generalController, + PolicyController: policyController, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "SecurityIntent") os.Exit(1) } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -115,6 +136,7 @@ func main() { os.Exit(1) } + // Starting the manager. setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") diff --git a/test-yaml/busybox-pod.yaml b/test-yaml/env/busybox-pod.yaml similarity index 100% rename from test-yaml/busybox-pod.yaml rename to test-yaml/env/busybox-pod.yaml diff --git a/test-yaml/multiubuntu.yaml b/test-yaml/env/multiubuntu.yaml similarity index 100% rename from test-yaml/multiubuntu.yaml rename to test-yaml/env/multiubuntu.yaml diff --git a/test-yaml/redis-pod.yaml b/test-yaml/env/redis-pod.yaml similarity index 100% rename from test-yaml/redis-pod.yaml rename to test-yaml/env/redis-pod.yaml diff --git a/test-yaml/intent-net-icmp-audit.yaml b/test-yaml/intents/network/intent-net-icmp-audit.yaml similarity index 100% rename from test-yaml/intent-net-icmp-audit.yaml rename to test-yaml/intents/network/intent-net-icmp-audit.yaml diff --git a/test-yaml/intent-network-sample.yaml b/test-yaml/intents/network/intent-network-sample.yaml similarity index 100% rename from test-yaml/intent-network-sample.yaml rename to test-yaml/intents/network/intent-network-sample.yaml diff --git a/test-yaml/intent-redis.yaml b/test-yaml/intents/network/intent-redis.yaml similarity index 100% rename from test-yaml/intent-redis.yaml rename to test-yaml/intents/network/intent-redis.yaml diff --git a/test-yaml/intent-risky-network-access.yaml b/test-yaml/intents/network/intent-risky-network-access.yaml similarity index 100% rename from test-yaml/intent-risky-network-access.yaml rename to test-yaml/intents/network/intent-risky-network-access.yaml diff --git a/test-yaml/intent-accessd-shadow-file.yaml b/test-yaml/intents/system/intent-accessd-shadow-file.yaml similarity index 100% rename from test-yaml/intent-accessd-shadow-file.yaml rename to test-yaml/intents/system/intent-accessd-shadow-file.yaml diff --git a/test-yaml/intent-allow-access-to-credentials-dir.yaml b/test-yaml/intents/system/intent-allow-access-to-credentials-dir.yaml similarity index 100% rename from test-yaml/intent-allow-access-to-credentials-dir.yaml rename to test-yaml/intents/system/intent-allow-access-to-credentials-dir.yaml diff --git a/test-yaml/intent-bug-block.yaml b/test-yaml/intents/system/intent-bug-block.yaml similarity index 100% rename from test-yaml/intent-bug-block.yaml rename to test-yaml/intents/system/intent-bug-block.yaml diff --git a/test-yaml/intent-cap-net-raw-block.yaml b/test-yaml/intents/system/intent-cap-net-raw-block.yaml similarity index 100% rename from test-yaml/intent-cap-net-raw-block.yaml rename to test-yaml/intents/system/intent-cap-net-raw-block.yaml diff --git a/test-yaml/intent-do-not-allow-priv-escalation.yaml b/test-yaml/intents/system/intent-do-not-allow-priv-escalation.yaml similarity index 100% rename from test-yaml/intent-do-not-allow-priv-escalation.yaml rename to test-yaml/intents/system/intent-do-not-allow-priv-escalation.yaml diff --git a/test-yaml/intent-path-block.yaml b/test-yaml/intents/system/intent-path-block.yaml similarity index 100% rename from test-yaml/intent-path-block.yaml rename to test-yaml/intents/system/intent-path-block.yaml diff --git a/test-yaml/intent-restrict-write-access-to-sys-folders.yaml b/test-yaml/intents/system/intent-restrict-write-access-to-sys-folders.yaml similarity index 100% rename from test-yaml/intent-restrict-write-access-to-sys-folders.yaml rename to test-yaml/intents/system/intent-restrict-write-access-to-sys-folders.yaml diff --git a/test-yaml/template-intent.yaml b/test-yaml/intents/template-intent.yaml similarity index 100% rename from test-yaml/template-intent.yaml rename to test-yaml/intents/template-intent.yaml diff --git a/test-yaml/policy-redis.yaml b/test-yaml/policy/cnp/policy-redis.yaml similarity index 100% rename from test-yaml/policy-redis.yaml rename to test-yaml/policy/cnp/policy-redis.yaml diff --git a/test-yaml/policy-risky.yaml b/test-yaml/policy/cnp/policy-risky.yaml similarity index 100% rename from test-yaml/policy-risky.yaml rename to test-yaml/policy/cnp/policy-risky.yaml diff --git a/test-yaml/policy-accessed-shadow-file.yaml b/test-yaml/policy/hsp/policy-accessed-shadow-file.yaml similarity index 100% rename from test-yaml/policy-accessed-shadow-file.yaml rename to test-yaml/policy/hsp/policy-accessed-shadow-file.yaml diff --git a/test-yaml/policy-bug-block.yaml b/test-yaml/policy/hsp/policy-bug-block.yaml similarity index 100% rename from test-yaml/policy-bug-block.yaml rename to test-yaml/policy/hsp/policy-bug-block.yaml diff --git a/test-yaml/policy-audit-all-unlink.yaml b/test-yaml/policy/ksp/policy-audit-all-unlink.yaml similarity index 100% rename from test-yaml/policy-audit-all-unlink.yaml rename to test-yaml/policy/ksp/policy-audit-all-unlink.yaml diff --git a/test-yaml/policy-cap-net-raw-block.yaml b/test-yaml/policy/ksp/policy-cap-net-raw-block.yaml similarity index 100% rename from test-yaml/policy-cap-net-raw-block.yaml rename to test-yaml/policy/ksp/policy-cap-net-raw-block.yaml diff --git a/test-yaml/policy-file-dir-allow-from-source-path.yaml b/test-yaml/policy/ksp/policy-file-dir-allow-from-source-path.yaml similarity index 100% rename from test-yaml/policy-file-dir-allow-from-source-path.yaml rename to test-yaml/policy/ksp/policy-file-dir-allow-from-source-path.yaml diff --git a/test-yaml/policy-net-icmp-audit.yaml b/test-yaml/policy/ksp/policy-net-icmp-audit.yaml similarity index 100% rename from test-yaml/policy-net-icmp-audit.yaml rename to test-yaml/policy/ksp/policy-net-icmp-audit.yaml diff --git a/test-yaml/policy-path-block.yaml b/test-yaml/policy/ksp/policy-path-block.yaml similarity index 100% rename from test-yaml/policy-path-block.yaml rename to test-yaml/policy/ksp/policy-path-block.yaml diff --git a/test-yaml/policy-proc-dir-block.yaml b/test-yaml/policy/ksp/policy-proc-dir-block.yaml similarity index 100% rename from test-yaml/policy-proc-dir-block.yaml rename to test-yaml/policy/ksp/policy-proc-dir-block.yaml