From 136df30935c45a007e7b52abcfc35f5befe6998f Mon Sep 17 00:00:00 2001 From: Ankur Kothiwal Date: Tue, 9 May 2023 18:09:36 +0530 Subject: [PATCH] add support for crown jewel policies Add support for generating lenient policies protecting sensitive assets (mount points here) Signed-off-by: Ankur Kothiwal --- src/conf/local-file.yaml | 5 + src/conf/local.yaml | 5 + src/config/configManager.go | 26 ++ src/crownjewel/crownjewel.go | 432 +++++++++++++++++++++++++ src/recommendpolicy/helperFunctions.go | 3 - src/server/grpcServer.go | 3 + src/types/configData.go | 7 + src/types/policyData.go | 2 + 8 files changed, 480 insertions(+), 3 deletions(-) create mode 100644 src/crownjewel/crownjewel.go diff --git a/src/conf/local-file.yaml b/src/conf/local-file.yaml index 97a5d5d0..27713cf0 100644 --- a/src/conf/local-file.yaml +++ b/src/conf/local-file.yaml @@ -120,6 +120,11 @@ recommend: cron-job-time-interval: "1h0m00s" # format: XhYmZs recommend-host-policy: true +# Recommended policies configuration +crownjewel: + operation-mode: 1 # 1: cronjob | 2: one-time-job + cron-job-time-interval: "1h0m00s" # format: XhYmZs + # license license: enabled: false diff --git a/src/conf/local.yaml b/src/conf/local.yaml index 06aff848..c3541720 100644 --- a/src/conf/local.yaml +++ b/src/conf/local.yaml @@ -72,6 +72,11 @@ recommend: cron-job-time-interval: "1h0m00s" # format: XhYmZs recommend-host-policy: true +# Recommended policies configuration +crownjewel: + operation-mode: 1 # 1: cronjob | 2: one-time-job + cron-job-time-interval: "1h0m00s" # format: XhYmZs + # license license: enabled: false diff --git a/src/config/configManager.go b/src/config/configManager.go index 2dc11016..48ae9703 100644 --- a/src/config/configManager.go +++ b/src/config/configManager.go @@ -218,6 +218,13 @@ func LoadConfigFromFile() { RecommendAdmissionControllerPolicy: viper.GetBool("recommend.admission-controller-policy"), } + // crown jewel policy configurations + CurrentCfg.ConfigCrownjewelPolicy = types.ConfigCrownjewelPolicy{ + CronJobTimeInterval: "@every " + viper.GetString("crownjewel.cron-job-time-interval"), + OneTimeJobTimeSelection: "", // e.g., 2021-01-20 07:00:23|2021-01-20 07:00:25 + OperationMode: viper.GetInt("crownjewel.operation-mode"), + } + // load database CurrentCfg.ConfigDB = LoadConfigDB() @@ -524,3 +531,22 @@ func GetCfgRecommendHostPolicy() bool { func GetCfgRecommendAdmissionControllerPolicy() bool { return CurrentCfg.ConfigRecommendPolicy.RecommendAdmissionControllerPolicy } + +// ================================== // +// == Get Crown Jewel Config Info == // +// ================================ // + +// run the Crown jewel scan once +func GetCfgCrownjewelOneTime() string { + return CurrentCfg.ConfigCrownjewelPolicy.OneTimeJobTimeSelection +} + +// run the Crown jewel scan as a cron job +func GetCfgCrownjewelCronJobTime() string { + return CurrentCfg.ConfigCrownjewelPolicy.CronJobTimeInterval +} + +// dont' run the Crown jewel scan +func GetCfgCrownjewelOperationMode() int { + return CurrentCfg.ConfigCrownjewelPolicy.OperationMode +} diff --git a/src/crownjewel/crownjewel.go b/src/crownjewel/crownjewel.go new file mode 100644 index 00000000..472103ac --- /dev/null +++ b/src/crownjewel/crownjewel.go @@ -0,0 +1,432 @@ +package crownjewel + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/accuknox/auto-policy-discovery/src/cluster" + "github.com/accuknox/auto-policy-discovery/src/common" + "github.com/accuknox/auto-policy-discovery/src/config" + cfg "github.com/accuknox/auto-policy-discovery/src/config" + "github.com/accuknox/auto-policy-discovery/src/libs" + logger "github.com/accuknox/auto-policy-discovery/src/logging" + obs "github.com/accuknox/auto-policy-discovery/src/observability" + opb "github.com/accuknox/auto-policy-discovery/src/protobuf/v1/observability" + "github.com/accuknox/auto-policy-discovery/src/systempolicy" + "github.com/accuknox/auto-policy-discovery/src/types" + "github.com/robfig/cron" + "github.com/rs/zerolog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +var log *zerolog.Logger + +// CrownjewelCronJob for cron job +var CrownjewelCronJob *cron.Cron + +var CrownjewelStopChan chan struct{} + +// CrownjewelWorkerStatus global worker +var CrownjewelWorkerStatus string + +// const values +const ( + // operation mode + opModeNoop = 0 + opModeCronjob = 1 + + // status + statusRunning = "running" + statusIdle = "idle" +) + +// init Function +func init() { + log = logger.GetInstance() + CrownjewelWorkerStatus = statusIdle + CrownjewelStopChan = make(chan struct{}) +} + +// StartCrownjewelWorker starts the crown jewel worker (run once or as a cronjob) +func StartCrownjewelWorker() { + if CrownjewelWorkerStatus != statusIdle { + log.Info().Msg("There is no idle Crown jewel policy worker") + return + } + if cfg.GetCfgCrownjewelOperationMode() == opModeNoop { // Do not run the operation + log.Info().Msg("Crown jewel operation mode is NOOP... ") + } else if cfg.GetCfgCrownjewelOperationMode() == opModeCronjob { // every time intervals + log.Info().Msg("Crown jewel policy cron job started") + CrownjewelPolicyMain() + StartCrownjewelCronJob() + } else { // one-time generation + CrownjewelPolicyMain() + log.Info().Msgf("Crown jewel policy onetime job done") + } +} + +// StartCrownjewelCronJob starts the cronjob +func StartCrownjewelCronJob() { + // init cron job + CrownjewelCronJob = cron.New() + err := CrownjewelCronJob.AddFunc(cfg.GetCfgCrownjewelCronJobTime(), CrownjewelPolicyMain) + if err != nil { + log.Error().Msg(err.Error()) + return + } + CrownjewelCronJob.Start() +} + +// StopCrownjewelCronJob stops the cronjob +func StopCrownjewelCronJob() { + if CrownjewelCronJob != nil { + log.Info().Msg("Got a signal to terminate the auto system policy discovery") + + CrownjewelStopChan = make(chan struct{}) + + close(CrownjewelStopChan) + + CrownjewelCronJob.Stop() // Stop the scheduler (does not stop any jobs already running). + + CrownjewelCronJob = nil + } +} + +// Create Crown Jewel Policy based on K8s object type +func CrownjewelPolicyMain() { + client := cluster.ConnectK8sClient() + deployments, err := client.AppsV1().Deployments("").List(context.Background(), metav1.ListOptions{}) + if err != nil { + log.Error().Msg("Error getting Deployments err=" + err.Error()) + return + } + replicaSets, err := client.AppsV1().ReplicaSets("").List(context.Background(), metav1.ListOptions{}) + if err != nil { + log.Error().Msg("Error getting replicasets err=" + err.Error()) + return + } + statefulSets, err := client.AppsV1().StatefulSets("").List(context.Background(), metav1.ListOptions{}) + if err != nil { + log.Error().Msg("Error getting statefulsets err=" + err.Error()) + return + } + daemonsets, err := client.AppsV1().DaemonSets("").List(context.Background(), metav1.ListOptions{}) + if err != nil { + log.Error().Msg("Error getting daemonsets err=" + err.Error()) + return + } + for _, d := range deployments.Items { + err := getFilteredPolicy(client, d.Name, d.Namespace, d.Spec.Template.Labels) + if err != nil { + log.Error().Msg("Error getting mount paths, err=" + err.Error()) + } + } + for _, r := range replicaSets.Items { + err := getFilteredPolicy(client, r.Name, r.Namespace, r.Spec.Template.Labels) + if err != nil { + log.Error().Msg("Error getting mount paths, err=" + err.Error()) + } + } + for _, s := range statefulSets.Items { + err := getFilteredPolicy(client, s.Name, s.Namespace, s.Spec.Template.Labels) + if err != nil { + log.Error().Msg("Error getting mount paths, err=" + err.Error()) + } + } + for _, rs := range daemonsets.Items { + err := getFilteredPolicy(client, rs.Name, rs.Namespace, rs.Spec.Template.Labels) + if err != nil { + log.Error().Msg("Error getting mount paths, err=" + err.Error()) + } + } +} + +type LabelMap = map[string]string + +// Get list of running processes from observability data +func getProcessList(client kubernetes.Interface, namespace string, labels types.LabelMap) ([]string, error) { + var processList []string + duplicatePaths := make(map[string]bool) + + podList, err := client.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{ + LabelSelector: libs.LabelMapToString(labels), + }) + if err != nil { + log.Warn().Msg(err.Error()) + } + for _, pod := range podList.Items { + for _, container := range pod.Spec.Containers { + sumResp, err := obs.GetSummaryData(&opb.Request{ + PodName: pod.Name, + NameSpace: pod.Namespace, + ContainerName: container.Name, + Type: "process,file", + }) + if err != nil { + log.Warn().Msgf("Error getting summary data for pod %s, container %s, namespace %s: %s", pod.Name, container.Name, pod.Namespace, err.Error()) + break + } + + for _, fileData := range sumResp.FileData { + if !duplicatePaths[fileData.Source] { + // ignore serviceaccount related process + if strings.Contains(fileData.Destination, "serviceaccount") { + continue + } + processList = append(processList, fileData.Source) + duplicatePaths[fileData.Source] = true + } + } + } + } + return processList, nil +} + +// Get all mounted paths +func getVolumeMountPaths(client kubernetes.Interface, labels LabelMap) ([]string, error) { + podList, err := client.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{ + LabelSelector: libs.LabelMapToString(labels), + }) + if err != nil { + return nil, fmt.Errorf("failed to get pod list: %v", err) + } + + var mountPaths []string + + for _, pod := range podList.Items { + for _, container := range pod.Spec.Containers { + for _, volumeMount := range container.VolumeMounts { + mountPaths = append(mountPaths, volumeMount.MountPath) + } + } + } + return mountPaths, nil +} + +// Get used mount paths from observability data +func usedMountPath(client kubernetes.Interface, namespace string, labels types.LabelMap) ([]string, map[string]string, error) { + var sumResponses []string + fromSource := make(map[string]string) + + podList, err := client.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{ + LabelSelector: libs.LabelMapToString(labels), + }) + if err != nil { + log.Warn().Msg(err.Error()) + } + + for _, pod := range podList.Items { + for _, container := range pod.Spec.Containers { + sumResp, err := obs.GetSummaryData(&opb.Request{ + PodName: pod.Name, + NameSpace: pod.Namespace, + ContainerName: container.Name, + Type: "process,file", + }) + if err != nil { + log.Warn().Msgf("Error getting summary data for pod %s, container %s, namespace %s: %s", pod.Name, container.Name, pod.Namespace, err.Error()) + break + } + + for _, fileData := range sumResp.FileData { + sumResponses = append(sumResponses, fileData.Destination) + fromSource[fileData.Destination] = fileData.Source + } + } + } + return sumResponses, fromSource, nil +} + +// Match used mounts paths with actually accessed mount paths +func accessedMountPaths(sumResp, mnt []string) ([]string, error) { + var matchedMountPaths []string + duplicatePaths := make(map[string]bool) + + for _, sumRespPath := range sumResp { + for _, mntPath := range mnt { + if strings.Contains(sumRespPath, mntPath) && !duplicatePaths[mntPath] { + matchedMountPaths = append(matchedMountPaths, mntPath) + duplicatePaths[mntPath] = true + } + } + } + return matchedMountPaths, nil +} + +// Ignore namespaces based on config +func getFilteredPolicy(client kubernetes.Interface, cname, namespace string, labels LabelMap) error { + // filters to check the namespaces to be ignored + nsFilter := config.CurrentCfg.ConfigSysPolicy.NsFilter + nsNotFilter := config.CurrentCfg.ConfigSysPolicy.NsNotFilter + + if len(nsFilter) > 0 { + for _, ns := range nsFilter { + if strings.Contains(namespace, ns) { + err := getCrownjewelPolicy(client, cname, namespace, labels) + if err != nil { + log.Error().Msg("Error getting Crown jewel policy, err=" + err.Error()) + } + } + } + } else if len(nsNotFilter) > 0 { + for _, notns := range nsNotFilter { + if !strings.Contains(namespace, notns) { + err := getCrownjewelPolicy(client, cname, namespace, labels) + if err != nil { + log.Error().Msg("Error getting Crown jewel policy, err=" + err.Error()) + } + } + } + } + return nil +} + +// Generate crown jewel policy +func getCrownjewelPolicy(client kubernetes.Interface, cname, namespace string, labels LabelMap) error { + var policies []types.KnoxSystemPolicy + + var matchedMountPaths []string + var ms types.MatchSpec + action := "Allow" + + // mount paths being used (from observability) + sumResp, fromSrc, _ := usedMountPath(client, namespace, labels) + + // all mount paths being used (from k8s cluster) + mnt, _ := getVolumeMountPaths(client, labels) + + // mount paths being used and are present in observability data (accessed mount paths) + matchedMountPaths, _ = accessedMountPaths(sumResp, mnt) + + // process paths being used and are present in observability data + matchedProcessPaths, _ := getProcessList(client, namespace, labels) + + policy := createCrownjewelPolicy(ms, cname, namespace, action, labels, mnt, matchedMountPaths, matchedProcessPaths, fromSrc) + // Check for empty policy + if policy.Spec.File.MatchDirectories == nil && policy.Spec.File.MatchPaths == nil && + policy.Spec.Process.MatchDirectories == nil && policy.Spec.Process.MatchPaths == nil { + return nil + } + policies = append(policies, policy) + + systempolicy.UpdateSysPolicies(policies) + + return nil +} + +// Build Crown jewel System policy structure +func buildSystemPolicy(cname, ns, action string, labels LabelMap, matchDirs []types.KnoxMatchDirectories, matchPaths []types.KnoxMatchPaths) types.KnoxSystemPolicy { + clustername := config.GetCfgClusterName() + + // expand the labels to be in string format + var combinedLabels []string + for key, value := range labels { + label := fmt.Sprintf("%s=%s", key, value) + combinedLabels = append(combinedLabels, label) + } + labelsString := strings.Join(combinedLabels, ",") + + // create policy name + name := strconv.FormatUint(uint64(common.HashInt(ns+clustername+cname+labelsString)), 10) + return types.KnoxSystemPolicy{ + APIVersion: "v1", + Kind: "KubeArmorPolicy", + Metadata: map[string]string{ + "name": "autopol-sensitive-" + name, + "namespace": ns, + }, + Spec: types.KnoxSystemSpec{ + Severity: 7, + Selector: types.Selector{ + MatchLabels: labels}, + Action: "Allow", // global action - default Allow + Message: "Sensitive assets and process control policy", + File: types.KnoxSys{ + MatchDirectories: matchDirs, + }, + Process: types.KnoxSys{ + MatchPaths: matchPaths, + }, + }, + } +} + +func createCrownjewelPolicy(ms types.MatchSpec, cname, namespace, action string, labels LabelMap, matchedDirPts, matchedMountPts, matchedProcessPts []string, fromSrc map[string]string) types.KnoxSystemPolicy { + var matchDirs []types.KnoxMatchDirectories + i := 1 + for _, dirpath := range matchedDirPts { + action = "Block" + // TODO: handle serviceaccount token access + // ignore serviceaccount token related accesses + if strings.Contains(dirpath, "serviceaccount") { + continue + } + + for _, mountPt := range matchedMountPts { + if dirpath == mountPt { + action = "Allow" + break + } + } + + var fromSourceVal []types.KnoxFromSource + for key, value := range fromSrc { + if strings.Contains(key, dirpath) { + // Check if the value already exists in fromSourceVal + exists := false + for _, existing := range fromSourceVal { + if existing.Path == value { + exists = true + break + } + } + if !exists { + fromSourceVal = append(fromSourceVal, types.KnoxFromSource{Path: value}) + } + } + } + + matchDir := types.KnoxMatchDirectories{ + Dir: dirpath + "/", + Recursive: true, + FromSource: fromSourceVal, + } + + if action == "Allow" { + // Block that dir from global access + matchAllowedDir := types.KnoxMatchDirectories{ + Dir: dirpath + "/", + Recursive: true, + Action: "Block", + } + matchDirs = append(matchDirs, matchAllowedDir) + } + + matchDirs = append(matchDirs, matchDir) + + if i == 1 { + // default allow access to root directory "/" + matchDir := types.KnoxMatchDirectories{ + Dir: "/", + Recursive: true, + } + matchDirs = append(matchDirs, matchDir) + i++ + } + } + + var matchPaths []types.KnoxMatchPaths + for _, processpath := range matchedProcessPts { + matchPath := types.KnoxMatchPaths{ + Path: processpath, + } + matchPaths = append(matchPaths, matchPath) + } + + policy := buildSystemPolicy(cname, namespace, action, labels, matchDirs, matchPaths) + + return policy +} diff --git a/src/recommendpolicy/helperFunctions.go b/src/recommendpolicy/helperFunctions.go index 2ed7fd41..77aaf046 100644 --- a/src/recommendpolicy/helperFunctions.go +++ b/src/recommendpolicy/helperFunctions.go @@ -122,7 +122,6 @@ func getNextRule(idx *int) (types.MatchSpec, error) { } func genericPolicy(precondition []string) bool { - for _, preCondition := range precondition { if strings.Contains(preCondition, "OPTSCAN") { return true @@ -132,7 +131,6 @@ func genericPolicy(precondition []string) bool { } func generateKnoxSystemPolicy(name, namespace string, labels LabelMap) ([]types.KnoxSystemPolicy, error) { - var ms types.MatchSpec var err error var policies []types.KnoxSystemPolicy @@ -351,7 +349,6 @@ func createPolicy(ms types.MatchSpec, name, namespace string, labels LabelMap) ( } func addPolicyRule(policy *types.KnoxSystemPolicy, r *types.KnoxSystemSpec) { - if r.File.MatchDirectories != nil || r.File.MatchPaths != nil { policy.Spec.File = r.File } diff --git a/src/server/grpcServer.go b/src/server/grpcServer.go index 22441e48..cd9e8ee2 100644 --- a/src/server/grpcServer.go +++ b/src/server/grpcServer.go @@ -5,6 +5,7 @@ import ( "errors" "strings" + "github.com/accuknox/auto-policy-discovery/src/crownjewel" "github.com/accuknox/auto-policy-discovery/src/license" "github.com/rs/zerolog" @@ -360,5 +361,7 @@ func AddServers(s *grpc.Server) *grpc.Server { //start recommendation recommend.StartRecommendWorker() + // start crown jewel + crownjewel.StartCrownjewelWorker() return s } diff --git a/src/types/configData.go b/src/types/configData.go index f7fc45a1..c36dd34f 100644 --- a/src/types/configData.go +++ b/src/types/configData.go @@ -130,6 +130,12 @@ type ConfigRecommendPolicy struct { RecommendAdmissionControllerPolicy bool `json:"recommend_admission_controller_policy,omitempty" bson:"recommend_admission_controller_policy,omitempty"` } +type ConfigCrownjewelPolicy struct { + OperationMode int `json:"operation_mode,omitempty" bson:"operation_mode,omitempty"` + CronJobTimeInterval string `json:"cronjob_time_interval,omitempty" bson:"cronjob_time_interval,omitempty"` + OneTimeJobTimeSelection string `json:"one_time_job_time_selection,omitempty" bson:"one_time_job_time_selection,omitempty"` +} + type Configuration struct { ConfigName string `json:"config_name,omitempty" bson:"config_name,omitempty"` Status int `json:"status,omitempty" bson:"status,omitempty"` @@ -150,4 +156,5 @@ type Configuration struct { ConfigPublisher ConfigPublisher `json:"config_summarizer,omitempty" bson:"config_summarizer,omitempty"` ConfigPurgeOldDBEntries ConfigPurgeOldDBEntries `json:"config_purge_old_db_entries,omitempty" bson:"config_purge_old_db_entries,omitempty"` ConfigRecommendPolicy ConfigRecommendPolicy `json:"config_recommend_policy,omitempty" bson:"config_recommend_policy,omitempty"` + ConfigCrownjewelPolicy ConfigCrownjewelPolicy `json:"config_crownjewel_policy,omitempty" bson:"config_crownjewel_policy,omitempty"` } diff --git a/src/types/policyData.go b/src/types/policyData.go index 0b8dc9a1..425029af 100644 --- a/src/types/policyData.go +++ b/src/types/policyData.go @@ -244,6 +244,7 @@ type KnoxMatchPaths struct { ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` OwnerOnly bool `json:"ownerOnly,omitempty" yaml:"ownerOnly,omitempty"` FromSource []KnoxFromSource `json:"fromSource,omitempty" yaml:"fromSource,omitempty"` + Action string `json:"action,omitempty"` } // KnoxMatchDirectories Structure @@ -253,6 +254,7 @@ type KnoxMatchDirectories struct { ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` OwnerOnly bool `json:"ownerOnly,omitempty" yaml:"ownerOnly,omitempty"` FromSource []KnoxFromSource `json:"fromSource,omitempty" yaml:"fromSource,omitempty"` + Action string `json:"action,omitempty"` } // KnoxMatchProtocols Structure