Skip to content

Commit

Permalink
Add a fuzzing feature for various SecurityContexts in a pod's manifest (
Browse files Browse the repository at this point in the history
#11)

It uses google/gofuzz to generate a random but somehow valid
SecurityContext that can be injected into a pod's manifest. It can
generate a pod securityContext, a container securityContext and an
initContainer securityContext.
  • Loading branch information
mtardy authored Oct 25, 2022
1 parent 39ed562 commit 211c47d
Show file tree
Hide file tree
Showing 7 changed files with 1,386 additions and 5 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,3 @@ jobs:
version: v1.49.0
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true
args: --timeout=5m

1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
run:
go: '1.19'
timeout: 5m

linters:
disable-all: true
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pentesting process.
* [With Nix](#with-nix)
* [Via Go](#via-go)
* [Usage](#usage)
* [Digging](#digging)
* [Generating](#generating)
* [Fuzzing](#fuzzing)
* [Details](#details)
* [Updates](#updates)
* [Usage warning](#usage-warning)
Expand Down Expand Up @@ -115,6 +118,8 @@ go install github.com/quarkslab/kdigger@main

## Usage

### Digging

What you generally want to do is running all the buckets with `dig all` or just
`d a`:
```bash
Expand Down Expand Up @@ -180,6 +185,8 @@ Global Flags:
-w, --width int Width for the human output (default 140)
```

### Generating

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.
Expand All @@ -202,6 +209,9 @@ boolean flags to disabled security features. Examples:
# Create a custom privileged pod
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -

# Fuzz the API server admission
kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -

Usage:
kdigger gen [name] [flags]

Expand All @@ -211,11 +221,15 @@ Aliases:
Flags:
--all Enable everything
--command stringArray Container command used (default [sleep,infinitely])
--fuzz-container Generate a random container security context. (will override other options)
--fuzz-init Generate a random init container security context.
--fuzz-pod Generate a random pod security context.
-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")
-n, --namespace string Kubernetes namespace to use
--privileged Add the security flag to the security context of the pod
--tolerations Add tolerations to be schedulable on most nodes

Expand All @@ -224,6 +238,28 @@ Global Flags:
-w, --width int Width for the human output (default 140)
```

### Fuzzing

You can try to fuzz your API admission with `kdigger`, find
[some information in this PR](https://github.com/quarkslab/kdigger/pull/11).
It can be interesting to see if your sets of custom policies are resistant
against randomly generated pod manifest.

See how `kdigger` can generate random container securityContext:
```console
./kdigger gen --fuzz-container -o json | jq '.spec.containers[].securityContext'
```

Or generate a dozen:
```bash
for _ in {1..12}; do ./kdigger gen --fuzz-container -o json | jq '.spec.containers[].securityContext'; done
```

Fuzz your admission API with simple commands similar to:
```bash
while true; do ./kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -; done
```

## Details

### Updates
Expand Down
26 changes: 25 additions & 1 deletion commands/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ var opts kgen.GenerateOpts

var genAll bool

var (
fuzzPod bool
fuzzContainer bool
fuzzInit bool
)

var genCmd = &cobra.Command{
Use: "gen [name] [flags]",
Aliases: []string{"generate"},
Expand All @@ -30,7 +36,10 @@ boolean flags to disabled security features. Examples:
kdigger gen -all mypod | kubectl apply -f -
# Create a custom privileged pod
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -`,
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -
# Fuzz the API server admission
kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -`,
RunE: func(cmd *cobra.Command, args []string) error {
// all puts all the boolean flags to true
if genAll {
Expand All @@ -46,6 +55,17 @@ boolean flags to disabled security features. Examples:

pod := kgen.Generate(opts)

// optional fuzzing steps
if fuzzPod {
kgen.FuzzPodSecurityContext(&pod.Spec.SecurityContext)
}
if fuzzContainer {
kgen.FuzzContainerSecurityContext(&pod.Spec.Containers[0].SecurityContext)
}
if fuzzInit {
kgen.CopyToInitAndFuzz(&pod.Spec)
}

var p printers.ResourcePrinter
if output == outputJSON {
p = &printers.JSONPrinter{}
Expand Down Expand Up @@ -74,4 +94,8 @@ func init() {
genCmd.Flags().BoolVar(&opts.HostNetwork, "hostnetwork", false, "Add the hostNetwork flag on the whole pod")

genCmd.Flags().BoolVar(&genAll, "all", false, "Enable everything")

genCmd.Flags().BoolVar(&fuzzPod, "fuzz-pod", false, "Generate a random pod security context.")
genCmd.Flags().BoolVar(&fuzzContainer, "fuzz-container", false, "Generate a random container security context. (will override other options)")
genCmd.Flags().BoolVar(&fuzzInit, "fuzz-init", false, "Generate a random init container security context.")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.19

require (
github.com/genuinetools/bpfd v0.0.1
github.com/google/gofuzz v1.2.0
github.com/jedib0t/go-pretty/v6 v6.3.9
github.com/mitchellh/go-ps v1.0.0
github.com/spf13/cobra v1.5.0
Expand Down Expand Up @@ -39,7 +40,6 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
Expand Down
134 changes: 133 additions & 1 deletion pkg/kgen/kgen.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package kgen

import (
"strings"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/apimachinery/pkg/util/rand"

fuzz "github.com/google/gofuzz"
"github.com/syndtr/gocapability/capability"
)

const (
// The probability of capabilities to be "ALL" will be 1/fuzzCapabilityAllOrEmptyChances.
fuzzCapabilityAllOrEmptyChances = 6
fuzzNilChances = .5
// Chances of putting zero in integers used for runAsUser, runAsGroup, etc.
fuzzIntZeroChances = .2
)

// Fuzzing will pick a uniform integer between 0 and fuzzCapabilityRandomMaxLen - 1 for capabilities list
var fuzzCapabilityRandomMaxLen = len(capability.List())

type GenerateOpts struct {
Name string
Image string
Expand All @@ -19,6 +34,123 @@ type GenerateOpts struct {
Tolerations bool
}

var intMutator = func(e *int64, c fuzz.Continue) {
if c.Float64() >= fuzzIntZeroChances {
*e = int64(c.Int31())
} else {
*e = 0
}
}

var seccompMutator = func(e *v1.SeccompProfile, c fuzz.Continue) {
supportedProfileValues := [...]string{"Localhost", "RuntimeDefault", "Unconfined"}
n := c.Intn(len(supportedProfileValues))

e.Type = v1.SeccompProfileType(supportedProfileValues[n])
if e.Type == v1.SeccompProfileType(supportedProfileValues[0]) {
// spec.securityContext.seccompProfile.localhostProfile: Required value: must be set when seccomp type is Localhost
s := c.RandString()
e.LocalhostProfile = &s
}
}

func FuzzPodSecurityContext(sc **v1.PodSecurityContext) {
f := fuzz.New().NilChance(fuzzNilChances).Funcs(
func(e *v1.Sysctl, c fuzz.Continue) {
// must have at most 253 characters and match regex ^([a-z0-9]([-_a-z0-9]*[a-z0-9])?[\./])*[a-z0-9]([-_a-z0-9]*[a-z0-9])?$
e.Name = sysctls[c.Intn(len(sysctls))]
c.Fuzz(&e.Value)
},
func(e *v1.PodFSGroupChangePolicy, c fuzz.Continue) {
if c.Intn(2) == 0 {
*e = v1.FSGroupChangeAlways
} else {
*e = v1.FSGroupChangeOnRootMismatch
}
},
func(e *[]v1.Sysctl, c fuzz.Continue) {
if c.Float64() >= fuzzNilChances {
uniqSysctls := map[string]v1.Sysctl{}
for n := c.Intn(10); len(uniqSysctls) < n; {
var sysctl v1.Sysctl
c.Fuzz(&sysctl)
uniqSysctls[sysctl.Name] = sysctl
}
for _, v := range uniqSysctls {
*e = append(*e, v)
}
} else {
*e = []v1.Sysctl{}
}
},
// let's ignore Windows for now
func(e *v1.WindowsSecurityContextOptions, c fuzz.Continue) {
*e = v1.WindowsSecurityContextOptions{}
},
// for supplementalGroups, runAsUser and runAsGroup value that must be between 0 and 2147483647, inclusive
intMutator,
seccompMutator,
)

securityContext := &v1.PodSecurityContext{}

f.Fuzz(securityContext)

*sc = securityContext
}

// FuzzContainerSecurityContext will override the SecurityContext with random (valid) values
func FuzzContainerSecurityContext(sc **v1.SecurityContext) {
// add .NilChange(0) to disable nil pointers generation, by default is 0.2
f := fuzz.New().NilChance(fuzzNilChances).Funcs(
func(e *v1.WindowsSecurityContextOptions, c fuzz.Continue) {
*e = v1.WindowsSecurityContextOptions{}
},
func(e *v1.Capability, c fuzz.Continue) {
caps := capability.List()
n := c.Intn(len(caps))
*e = v1.Capability(strings.ToUpper(caps[n].String()))
},
func(e *[]v1.Capability, c fuzz.Continue) {
if r := c.Intn(fuzzCapabilityAllOrEmptyChances); r == 0 {
*e = []v1.Capability{"ALL"}
} else if r == 1 {
*e = []v1.Capability{}
} else {
length := c.Intn(fuzzCapabilityRandomMaxLen)
for i := 0; i < length; i++ {
var cap v1.Capability
c.Fuzz(&cap)
*e = append(*e, cap)
}
}
},
// for runAsUser and runAsGroup value that must be between 0 and 2147483647, inclusive
intMutator,
seccompMutator,
)
securityContext := &v1.SecurityContext{}
f.Fuzz(securityContext)

// cannot set `allowPrivilegeEscalation` to false and `privileged` to true
// there are more interdependences, like CAP_SYS_ADMIN imply privileged but
// maybe it's too rare to be interesting
if securityContext.AllowPrivilegeEscalation != nil && !*securityContext.AllowPrivilegeEscalation {
b := false
securityContext.Privileged = &b
}

*sc = securityContext
}

func CopyToInitAndFuzz(spec *v1.PodSpec) {
if len(spec.Containers) > 0 {
spec.InitContainers = append(spec.InitContainers, spec.Containers[0])
spec.InitContainers[0].Name += "-init"
FuzzContainerSecurityContext(&spec.InitContainers[0].SecurityContext)
}
}

func Generate(opts GenerateOpts) *v1.Pod {
pod := &v1.Pod{
TypeMeta: metav1.TypeMeta{
Expand Down
Loading

0 comments on commit 211c47d

Please sign in to comment.