Skip to content

Commit 211c47d

Browse files
authored
Add a fuzzing feature for various SecurityContexts in a pod's manifest (#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.
1 parent 39ed562 commit 211c47d

File tree

7 files changed

+1386
-5
lines changed

7 files changed

+1386
-5
lines changed

.github/workflows/golangci-lint.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,3 @@ jobs:
2424
version: v1.49.0
2525
# Optional: show only new issues if it's a pull request. The default value is `false`.
2626
only-new-issues: true
27-
args: --timeout=5m
28-

.golangci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
run:
22
go: '1.19'
3+
timeout: 5m
34

45
linters:
56
disable-all: true

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pentesting process.
2727
* [With Nix](#with-nix)
2828
* [Via Go](#via-go)
2929
* [Usage](#usage)
30+
* [Digging](#digging)
31+
* [Generating](#generating)
32+
* [Fuzzing](#fuzzing)
3033
* [Details](#details)
3134
* [Updates](#updates)
3235
* [Usage warning](#usage-warning)
@@ -115,6 +118,8 @@ go install github.com/quarkslab/kdigger@main
115118

116119
## Usage
117120

121+
### Digging
122+
118123
What you generally want to do is running all the buckets with `dig all` or just
119124
`d a`:
120125
```bash
@@ -180,6 +185,8 @@ Global Flags:
180185
-w, --width int Width for the human output (default 140)
181186
```
182187

188+
### Generating
189+
183190
You can also generate useful templates for pods with security features disabled
184191
to escalate privileges when you can create such a pod. See the help for this
185192
specific command for more information.
@@ -202,6 +209,9 @@ boolean flags to disabled security features. Examples:
202209
# Create a custom privileged pod
203210
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -
204211

212+
# Fuzz the API server admission
213+
kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -
214+
205215
Usage:
206216
kdigger gen [name] [flags]
207217

@@ -211,11 +221,15 @@ Aliases:
211221
Flags:
212222
--all Enable everything
213223
--command stringArray Container command used (default [sleep,infinitely])
224+
--fuzz-container Generate a random container security context. (will override other options)
225+
--fuzz-init Generate a random init container security context.
226+
--fuzz-pod Generate a random pod security context.
214227
-h, --help help for gen
215228
--hostnetwork Add the hostNetwork flag on the whole pod
216229
--hostpath Add a hostPath volume to the container
217230
--hostpid Add the hostPid flag on the whole pod
218231
--image string Container image used (default "busybox")
232+
-n, --namespace string Kubernetes namespace to use
219233
--privileged Add the security flag to the security context of the pod
220234
--tolerations Add tolerations to be schedulable on most nodes
221235

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

241+
### Fuzzing
242+
243+
You can try to fuzz your API admission with `kdigger`, find
244+
[some information in this PR](https://github.com/quarkslab/kdigger/pull/11).
245+
It can be interesting to see if your sets of custom policies are resistant
246+
against randomly generated pod manifest.
247+
248+
See how `kdigger` can generate random container securityContext:
249+
```console
250+
./kdigger gen --fuzz-container -o json | jq '.spec.containers[].securityContext'
251+
```
252+
253+
Or generate a dozen:
254+
```bash
255+
for _ in {1..12}; do ./kdigger gen --fuzz-container -o json | jq '.spec.containers[].securityContext'; done
256+
```
257+
258+
Fuzz your admission API with simple commands similar to:
259+
```bash
260+
while true; do ./kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -; done
261+
```
262+
227263
## Details
228264

229265
### Updates

commands/gen.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ var opts kgen.GenerateOpts
1212

1313
var genAll bool
1414

15+
var (
16+
fuzzPod bool
17+
fuzzContainer bool
18+
fuzzInit bool
19+
)
20+
1521
var genCmd = &cobra.Command{
1622
Use: "gen [name] [flags]",
1723
Aliases: []string{"generate"},
@@ -30,7 +36,10 @@ boolean flags to disabled security features. Examples:
3036
kdigger gen -all mypod | kubectl apply -f -
3137
3238
# Create a custom privileged pod
33-
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -`,
39+
kdigger gen --privileged --image bash --command watch --command date | kubectl apply -f -
40+
41+
# Fuzz the API server admission
42+
kdigger gen --fuzz-pod --fuzz-init --fuzz-container | kubectl apply --dry-run=server -f -`,
3443
RunE: func(cmd *cobra.Command, args []string) error {
3544
// all puts all the boolean flags to true
3645
if genAll {
@@ -46,6 +55,17 @@ boolean flags to disabled security features. Examples:
4655

4756
pod := kgen.Generate(opts)
4857

58+
// optional fuzzing steps
59+
if fuzzPod {
60+
kgen.FuzzPodSecurityContext(&pod.Spec.SecurityContext)
61+
}
62+
if fuzzContainer {
63+
kgen.FuzzContainerSecurityContext(&pod.Spec.Containers[0].SecurityContext)
64+
}
65+
if fuzzInit {
66+
kgen.CopyToInitAndFuzz(&pod.Spec)
67+
}
68+
4969
var p printers.ResourcePrinter
5070
if output == outputJSON {
5171
p = &printers.JSONPrinter{}
@@ -74,4 +94,8 @@ func init() {
7494
genCmd.Flags().BoolVar(&opts.HostNetwork, "hostnetwork", false, "Add the hostNetwork flag on the whole pod")
7595

7696
genCmd.Flags().BoolVar(&genAll, "all", false, "Enable everything")
97+
98+
genCmd.Flags().BoolVar(&fuzzPod, "fuzz-pod", false, "Generate a random pod security context.")
99+
genCmd.Flags().BoolVar(&fuzzContainer, "fuzz-container", false, "Generate a random container security context. (will override other options)")
100+
genCmd.Flags().BoolVar(&fuzzInit, "fuzz-init", false, "Generate a random init container security context.")
77101
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.19
44

55
require (
66
github.com/genuinetools/bpfd v0.0.1
7+
github.com/google/gofuzz v1.2.0
78
github.com/jedib0t/go-pretty/v6 v6.3.9
89
github.com/mitchellh/go-ps v1.0.0
910
github.com/spf13/cobra v1.5.0
@@ -39,7 +40,6 @@ require (
3940
github.com/google/btree v1.1.2 // indirect
4041
github.com/google/gnostic v0.6.9 // indirect
4142
github.com/google/go-cmp v0.5.9 // indirect
42-
github.com/google/gofuzz v1.2.0 // indirect
4343
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
4444
github.com/google/uuid v1.3.0 // indirect
4545
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect

pkg/kgen/kgen.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
package kgen
22

33
import (
4+
"strings"
5+
46
v1 "k8s.io/api/core/v1"
57
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6-
78
"k8s.io/apimachinery/pkg/util/rand"
9+
10+
fuzz "github.com/google/gofuzz"
11+
"github.com/syndtr/gocapability/capability"
12+
)
13+
14+
const (
15+
// The probability of capabilities to be "ALL" will be 1/fuzzCapabilityAllOrEmptyChances.
16+
fuzzCapabilityAllOrEmptyChances = 6
17+
fuzzNilChances = .5
18+
// Chances of putting zero in integers used for runAsUser, runAsGroup, etc.
19+
fuzzIntZeroChances = .2
820
)
921

22+
// Fuzzing will pick a uniform integer between 0 and fuzzCapabilityRandomMaxLen - 1 for capabilities list
23+
var fuzzCapabilityRandomMaxLen = len(capability.List())
24+
1025
type GenerateOpts struct {
1126
Name string
1227
Image string
@@ -19,6 +34,123 @@ type GenerateOpts struct {
1934
Tolerations bool
2035
}
2136

37+
var intMutator = func(e *int64, c fuzz.Continue) {
38+
if c.Float64() >= fuzzIntZeroChances {
39+
*e = int64(c.Int31())
40+
} else {
41+
*e = 0
42+
}
43+
}
44+
45+
var seccompMutator = func(e *v1.SeccompProfile, c fuzz.Continue) {
46+
supportedProfileValues := [...]string{"Localhost", "RuntimeDefault", "Unconfined"}
47+
n := c.Intn(len(supportedProfileValues))
48+
49+
e.Type = v1.SeccompProfileType(supportedProfileValues[n])
50+
if e.Type == v1.SeccompProfileType(supportedProfileValues[0]) {
51+
// spec.securityContext.seccompProfile.localhostProfile: Required value: must be set when seccomp type is Localhost
52+
s := c.RandString()
53+
e.LocalhostProfile = &s
54+
}
55+
}
56+
57+
func FuzzPodSecurityContext(sc **v1.PodSecurityContext) {
58+
f := fuzz.New().NilChance(fuzzNilChances).Funcs(
59+
func(e *v1.Sysctl, c fuzz.Continue) {
60+
// 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])?$
61+
e.Name = sysctls[c.Intn(len(sysctls))]
62+
c.Fuzz(&e.Value)
63+
},
64+
func(e *v1.PodFSGroupChangePolicy, c fuzz.Continue) {
65+
if c.Intn(2) == 0 {
66+
*e = v1.FSGroupChangeAlways
67+
} else {
68+
*e = v1.FSGroupChangeOnRootMismatch
69+
}
70+
},
71+
func(e *[]v1.Sysctl, c fuzz.Continue) {
72+
if c.Float64() >= fuzzNilChances {
73+
uniqSysctls := map[string]v1.Sysctl{}
74+
for n := c.Intn(10); len(uniqSysctls) < n; {
75+
var sysctl v1.Sysctl
76+
c.Fuzz(&sysctl)
77+
uniqSysctls[sysctl.Name] = sysctl
78+
}
79+
for _, v := range uniqSysctls {
80+
*e = append(*e, v)
81+
}
82+
} else {
83+
*e = []v1.Sysctl{}
84+
}
85+
},
86+
// let's ignore Windows for now
87+
func(e *v1.WindowsSecurityContextOptions, c fuzz.Continue) {
88+
*e = v1.WindowsSecurityContextOptions{}
89+
},
90+
// for supplementalGroups, runAsUser and runAsGroup value that must be between 0 and 2147483647, inclusive
91+
intMutator,
92+
seccompMutator,
93+
)
94+
95+
securityContext := &v1.PodSecurityContext{}
96+
97+
f.Fuzz(securityContext)
98+
99+
*sc = securityContext
100+
}
101+
102+
// FuzzContainerSecurityContext will override the SecurityContext with random (valid) values
103+
func FuzzContainerSecurityContext(sc **v1.SecurityContext) {
104+
// add .NilChange(0) to disable nil pointers generation, by default is 0.2
105+
f := fuzz.New().NilChance(fuzzNilChances).Funcs(
106+
func(e *v1.WindowsSecurityContextOptions, c fuzz.Continue) {
107+
*e = v1.WindowsSecurityContextOptions{}
108+
},
109+
func(e *v1.Capability, c fuzz.Continue) {
110+
caps := capability.List()
111+
n := c.Intn(len(caps))
112+
*e = v1.Capability(strings.ToUpper(caps[n].String()))
113+
},
114+
func(e *[]v1.Capability, c fuzz.Continue) {
115+
if r := c.Intn(fuzzCapabilityAllOrEmptyChances); r == 0 {
116+
*e = []v1.Capability{"ALL"}
117+
} else if r == 1 {
118+
*e = []v1.Capability{}
119+
} else {
120+
length := c.Intn(fuzzCapabilityRandomMaxLen)
121+
for i := 0; i < length; i++ {
122+
var cap v1.Capability
123+
c.Fuzz(&cap)
124+
*e = append(*e, cap)
125+
}
126+
}
127+
},
128+
// for runAsUser and runAsGroup value that must be between 0 and 2147483647, inclusive
129+
intMutator,
130+
seccompMutator,
131+
)
132+
securityContext := &v1.SecurityContext{}
133+
f.Fuzz(securityContext)
134+
135+
// cannot set `allowPrivilegeEscalation` to false and `privileged` to true
136+
// there are more interdependences, like CAP_SYS_ADMIN imply privileged but
137+
// maybe it's too rare to be interesting
138+
if securityContext.AllowPrivilegeEscalation != nil && !*securityContext.AllowPrivilegeEscalation {
139+
b := false
140+
securityContext.Privileged = &b
141+
}
142+
143+
*sc = securityContext
144+
}
145+
146+
func CopyToInitAndFuzz(spec *v1.PodSpec) {
147+
if len(spec.Containers) > 0 {
148+
spec.InitContainers = append(spec.InitContainers, spec.Containers[0])
149+
spec.InitContainers[0].Name += "-init"
150+
FuzzContainerSecurityContext(&spec.InitContainers[0].SecurityContext)
151+
}
152+
}
153+
22154
func Generate(opts GenerateOpts) *v1.Pod {
23155
pod := &v1.Pod{
24156
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)