From 335901312360489871fdf41f3cf2c3745e7ed831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mah=C3=A9?= Date: Fri, 8 Jul 2022 13:52:59 +0200 Subject: [PATCH] Add a pod template generation command (#5) * Add a template generator command to create bad pods * Add tests for the kgen package * Add documentation about the new gen feature in README --- README.md | 46 +++++++++++++++ commands/gen.go | 77 +++++++++++++++++++++++++ pkg/kgen/kgen.go | 97 ++++++++++++++++++++++++++++++++ pkg/kgen/kgen_test.go | 128 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 348 insertions(+) create mode 100644 commands/gen.go create mode 100644 pkg/kgen/kgen.go create mode 100644 pkg/kgen/kgen_test.go diff --git a/README.md b/README.md index a8f4799..628db40 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Usage: Available Commands: completion Generate the autocompletion script for the specified shell dig Use all buckets or specific ones + gen Generate template for pod with security features disabled help Help about any command ls List available buckets or describe specific ones version Print the version information @@ -130,6 +131,7 @@ Flags: -w, --width int Width for the human output (default 140) Use "kdigger [command] --help" for more information about a command. + ``` Make sure to check out the help on the ``dig`` command to see all the available @@ -161,6 +163,50 @@ Global Flags: -w, --width int Width for the human output (default 140) ``` +You can also generate useful templates for pods with security features disabled +to escalate privileges when you can create such a pod. See the help for this +specific command for more information. + +```console +$ kdigger help gen +This command generates templates for pod with security features disabled. +You can customize the pods with some of the string flags and activate +boolean flags to disabled security features. Examples: + + # Generate a very simple template in json + kdigger gen -o json + + # Create a very simple pod + kdigger gen | kubectl apply -f - + + # Create a pod named mypod with most security features disabled + kdigger gen -all mypod | kubectl apply -f - + + # Create a custom privileged pod + kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f - + +Usage: + kdigger gen [name] [flags] + +Aliases: + gen, generate + +Flags: + --all Enable everything + --command stringArray Container command used (default [sleep,infinitely]) + -h, --help help for gen + --hostnetwork Add the hostNetwork flag on the whole pod + --hostpath Add a hostPath volume to the container + --hostpid Add the hostPid flag on the whole pod + --image string Container image used (default "busybox") + --privileged Add the security flag to the security context of the pod + --tolerations Add tolerations to be schedulable on most nodes + +Global Flags: + -o, --output string Output format. One of: human|json. (default "human") + -w, --width int Width for the human output (default 140) +``` + ## Details ### Updates diff --git a/commands/gen.go b/commands/gen.go new file mode 100644 index 0000000..21842ec --- /dev/null +++ b/commands/gen.go @@ -0,0 +1,77 @@ +package commands + +import ( + "os" + + "github.com/quarkslab/kdigger/pkg/kgen" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/printers" +) + +var opts kgen.GenerateOpts + +var genAll bool + +var genCmd = &cobra.Command{ + Use: "gen [name] [flags]", + Aliases: []string{"generate"}, + Short: "Generate template for pod with security features disabled", + Long: `This command generates templates for pod with security features disabled. +You can customize the pods with some of the string flags and activate +boolean flags to disabled security features. Examples: + + # Generate a very simple template in json + kdigger gen -o json + + # Create a very simple pod + kdigger gen | kubectl apply -f - + + # Create a pod named mypod with most security features disabled + kdigger gen -all mypod | kubectl apply -f - + + # Create a custom privileged pod + kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -`, + RunE: func(cmd *cobra.Command, args []string) error { + // all puts all the boolean flags to true + if genAll { + opts.HostNetwork = true + opts.Privileged = true + opts.HostPath = true + opts.HostPid = true + opts.Tolerations = true + } + if len(args) > 0 { + opts.Name = args[0] + } + + pod := kgen.Generate(opts) + + var p printers.ResourcePrinter + if output == "json" { + p = &printers.JSONPrinter{} + } else { + p = &printers.YAMLPrinter{} + } + err := p.PrintObj(pod, os.Stdout) + if err != nil { + return err + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(genCmd) + + genCmd.Flags().StringVar(&opts.Image, "image", "busybox", "Container image used") + genCmd.Flags().StringArrayVar(&opts.Command, "command", []string{"sleep", "infinitely"}, "Container command used") + + genCmd.Flags().BoolVar(&opts.Privileged, "privileged", false, "Add the security flag to the security context of the pod") + genCmd.Flags().BoolVar(&opts.HostPath, "hostpath", false, "Add a hostPath volume to the container") + genCmd.Flags().BoolVar(&opts.Tolerations, "tolerations", false, "Add tolerations to be schedulable on most nodes") + genCmd.Flags().BoolVar(&opts.HostPid, "hostpid", false, "Add the hostPid flag on the whole pod") + genCmd.Flags().BoolVar(&opts.HostNetwork, "hostnetwork", false, "Add the hostNetwork flag on the whole pod") + + genCmd.Flags().BoolVar(&genAll, "all", false, "Enable everything") +} diff --git a/pkg/kgen/kgen.go b/pkg/kgen/kgen.go new file mode 100644 index 0000000..a671194 --- /dev/null +++ b/pkg/kgen/kgen.go @@ -0,0 +1,97 @@ +package kgen + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/apimachinery/pkg/util/rand" +) + +type GenerateOpts struct { + Name string + Image string + Command []string + Privileged bool + HostPath bool + HostPid bool + HostNetwork bool + Tolerations bool +} + +func Generate(opts GenerateOpts) *v1.Pod { + pod := &v1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Command: []string{"sleep", "infinitely"}, + Name: "digger", + Image: "busybox", + }, + }, + }, + } + + if opts.Name == "" { + pod.ObjectMeta.Name = "digger-" + rand.String(5) + } else { + pod.ObjectMeta.Name = opts.Name + pod.Spec.Containers[0].Name = opts.Name + } + + if opts.Image != "" { + pod.Spec.Containers[0].Image = opts.Image + } + + if len(opts.Command) != 0 { + pod.Spec.Containers[0].Command = opts.Command + } + + pod.Spec.HostPID = opts.HostPid + + pod.Spec.HostNetwork = opts.HostNetwork + + if opts.Privileged { + pod.Spec.Containers[0].SecurityContext = &v1.SecurityContext{ + Privileged: &opts.Privileged, + } + } + + if opts.HostPath { + pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{ + Name: "host", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/", + }, + }, + }) + pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ + Name: "host", + MountPath: "/hostfs", + }) + } + + if opts.Tolerations { + pod.Spec.Tolerations = append(pod.Spec.Tolerations, []v1.Toleration{ + { + Key: "NoExecute", + Operator: "Exists", + }, + { + Key: "NoSchedule", + Operator: "Exists", + }, + { + Key: "CriticalAddonsOnly", + Operator: "Exists", + }, + }...) + } + + return pod +} diff --git a/pkg/kgen/kgen_test.go b/pkg/kgen/kgen_test.go new file mode 100644 index 0000000..e024d12 --- /dev/null +++ b/pkg/kgen/kgen_test.go @@ -0,0 +1,128 @@ +package kgen + +import ( + "reflect" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/rand" +) + +func TestGenerateManualName(t *testing.T) { + args := GenerateOpts{ + Name: "testname", + } + if got := Generate(args); got.ObjectMeta.Name != args.Name { + t.Errorf("Generate() = %v, want %v", got.ObjectMeta.Name, args.Name) + } +} + +func TestGenerateAutoName(t *testing.T) { + args := GenerateOpts{} + // generate pseudo random + rand.Seed(0) + name := "digger-cbhtc" + if got := Generate(args); got.ObjectMeta.Name != name { + t.Errorf("Generate() = %v, want %v", got.ObjectMeta.Name, name) + } +} + +func TestGenerateManualImage(t *testing.T) { + args := GenerateOpts{ + Image: "testimage", + } + if got := Generate(args); got.Spec.Containers[0].Image != args.Image { + t.Errorf("Generate() = %v, want %v", got.Spec.Containers[0].Image, args.Image) + } +} + +func TestGenerateManualCommand(t *testing.T) { + args := GenerateOpts{ + Command: []string{"test", "command"}, + } + if got := Generate(args); len(got.Spec.Containers[0].Command) != 2 && got.Spec.Containers[0].Command[0] != "test" && got.Spec.Containers[0].Command[1] != "command" { + t.Errorf("Generate() = %v, want %v", got.Spec.Containers[0].Command, args.Command) + } +} + +func TestGenerateHostPID(t *testing.T) { + args := GenerateOpts{ + HostPid: true, + } + if got := Generate(args); !got.Spec.HostPID { + t.Errorf("Generate() = %v, want %v", got.Spec.HostPID, true) + } + args = GenerateOpts{ + HostPid: false, + } + if got := Generate(args); got.Spec.HostPID { + t.Errorf("Generate() = %v, want %v", got.Spec.HostPID, false) + } +} + +func TestGenerateHostNetwork(t *testing.T) { + args := GenerateOpts{ + HostNetwork: true, + } + if got := Generate(args); !got.Spec.HostNetwork { + t.Errorf("Generate() = %v, want %v", got.Spec.HostNetwork, true) + } + args = GenerateOpts{ + HostNetwork: false, + } + if got := Generate(args); got.Spec.HostNetwork { + t.Errorf("Generate() = %v, want %v", got.Spec.HostNetwork, false) + } +} + +func TestGeneratePrivileged(t *testing.T) { + args := []GenerateOpts{ + { + Privileged: true, + }, + { + Privileged: false, + }, + } + for _, arg := range args { + if got := Generate(arg); got.Spec.Containers[0].SecurityContext != nil && got.Spec.Containers[0].SecurityContext.Privileged == &arg.Privileged { + t.Errorf("Generate() = %v, want %v", got.Spec.Containers[0].SecurityContext.Privileged, &arg.Privileged) + } + } +} + +func TestGenerateHostPath(t *testing.T) { + args := GenerateOpts{ + HostPath: true, + } + if got := Generate(args); len(got.Spec.Containers[0].VolumeMounts) != 1 && + got.Spec.Containers[0].VolumeMounts[0].Name == "host" && + got.Spec.Containers[0].VolumeMounts[0].MountPath == "hostfs" && + got.Spec.Volumes[0].Name == "host" && + got.Spec.Volumes[0].HostPath.Path == "/" { + t.Errorf("Generate() = %v and %v", got.Spec.Containers[0].VolumeMounts[0], got.Spec.Volumes[0]) + } +} + +func TestGenerateTolerations(t *testing.T) { + args := GenerateOpts{ + Tolerations: true, + } + want := []v1.Toleration{ + { + Key: "NoExecute", + Operator: "Exists", + }, + { + Key: "NoSchedule", + Operator: "Exists", + }, + { + Key: "CriticalAddonsOnly", + Operator: "Exists", + }, + } + if got := Generate(args); reflect.DeepEqual(got, want) { + t.Errorf("Generate() = %v, want %v", got, want) + } +}