Skip to content

Commit

Permalink
Add ability to convert K8s Obj to PSP
Browse files Browse the repository at this point in the history
Add the ability to convert a single K8s Object to a Pod Security Policy,
instead of reading all objects from a live cluster.

 - Add spf13/cobra to add subcommands "inspect", which covers the
   existing functionality, and "convert", which converts a single K8s
   Object as a yaml file.

 - Move the code that generates a PodSecurityPolicy from lists of
   ContainerSecuritySpec/PodSecuritySpec to a standalone package in
   generator/generator.go. It only has a few minor changes:

      - It has a struct so it's more like an object than a standalone
        function.

      - The provided service account is optional. When not provided
        e.g. nil, "secret" is always added as an allowed volume type.

      - When used by the converter, the namespace and serverGitVersion
        are set to default values "default" and "v1.11", which allows
        enforcement of ReadOnly filesystems.

 - Error handling at the top level is done by log.Fatalf instead of
   panic(), to make problems like incorrect arguments a bit more
   graceful.

 - Add logging at least for the conversion path, showing the files that
   are read and written at debug level.
  • Loading branch information
mstemm committed Oct 15, 2019
1 parent df1989f commit cc6425d
Show file tree
Hide file tree
Showing 7 changed files with 887 additions and 571 deletions.
17 changes: 11 additions & 6 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Kube PodSecurityPolicy Advisor

kube-psp-advisor is a tool that makes it easier to create K8s Pod Security Policies (PSPs) from either a live K8s environment or from a single .yaml file containing a pod specification (Deployment, DaemonSet, Pod, etc).

It has 2 subcommands, `kube-psp-advisor inspect` and `kube-psp-advisor convert`. `inspect` connects to a K8s API server, downloads all Pod-related objects in a given namespace, and generates a PSP based on the properties of those objects. `convert` works without connecting to an API Server, reading a single .yaml file containing a object with a pod spec and generating a PSP based on the file.

## Build and Run locally
1. ```make build```
2. ```./kube-psp-advisor``` to generate Pod Security Policy based on running cluster configuration
3. ```./kube-psp-advisor --report``` to print the details reports (why this PSP is recommended for the cluster)
2. ```./kube-psp-advisor inspect``` to generate Pod Security Policy based on running cluster configuration
3. ```./kube-psp-advisor inspect --report``` to print the details reports (why this PSP is recommended for the cluster)
4. ```./kube-psp-advisor convert --podFile <path> --pspFile <path>``` to generate a PSP from a single .yaml file.

## Build and Run as Container
1. ```docker build -t <Image Name> -f container/Dockerfile .```
Expand Down Expand Up @@ -37,9 +42,9 @@ Some attributes(capabilities, host ports etc.) required gathering runtime inform
- [x] Basic functionalities;
- [ ] Create PSP's for common charts
- [ ] Kubectl plugin

`
## Sample Pod Security Policy
Command: `./kube-psp-advisor --namespace=psp-test`
Command: `./kube-psp-advisor inspect --namespace=psp-test`
```
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
Expand Down Expand Up @@ -73,8 +78,8 @@ spec:
- secret
```

## Sample Report
Command: `./kube-psp-advisor --namespace=psp-test --report | jq .podSecuritySpecs`
## Sample Report
Command: `./kube-psp-advisor inspect --namespace=psp-test --report | jq .podSecuritySpecs`
```
{
"hostIPC": [
Expand Down
190 changes: 9 additions & 181 deletions advisor/processor/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package processor

import (
"fmt"
"time"

"github.com/sysdiglabs/kube-psp-advisor/generator"
"github.com/sysdiglabs/kube-psp-advisor/advisor/report"
"github.com/sysdiglabs/kube-psp-advisor/advisor/types"
"github.com/sysdiglabs/kube-psp-advisor/utils"

v1 "k8s.io/api/core/v1"
"k8s.io/api/policy/v1beta1"
Expand All @@ -20,10 +19,16 @@ type Processor struct {
namespace string
serviceAccountMap map[string]v1.ServiceAccount
serverGitVersion string
gen *generator.Generator
}

// NewProcessor returns a new processor
func NewProcessor(kubeconfig string) (*Processor, error) {

gen, err := generator.NewGenerator(); if err != nil {
return nil, fmt.Errorf("Could not create generator: %v", err)
}

config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
Expand All @@ -43,6 +48,7 @@ func NewProcessor(kubeconfig string) (*Processor, error) {
k8sClient: clientset,
resourceNamePrefix: map[string]bool{},
serverGitVersion: info.GitVersion,
gen: gen,
}, nil
}

Expand All @@ -52,186 +58,8 @@ func (p *Processor) SetNamespace(ns string) {

// GeneratePSP generate Pod Security Policy
func (p *Processor) GeneratePSP(cssList []types.ContainerSecuritySpec, pssList []types.PodSecuritySpec) *v1beta1.PodSecurityPolicy {
var ns string
// no PSP will be generated if no security spec is provided
if len(cssList) == 0 && len(pssList) == 0 {
return nil
}

psp := &v1beta1.PodSecurityPolicy{}

psp.APIVersion = "policy/v1beta1"
psp.Kind = "PodSecurityPolicy"

addedCap := map[string]int{}
droppedCap := map[string]int{}

effectiveCap := map[string]bool{}

runAsUser := map[int64]bool{}

volumeTypes := map[string]bool{}

hostPaths := map[string]bool{}

runAsUserCount := 0

runAsNonRootCount := 0

notAllowPrivilegeEscationCount := 0

ns = p.namespace

if ns == "" {
ns = "all"
}

psp.Name = fmt.Sprintf("%s-%s-%s", "pod-security-policy", ns, time.Now().Format("20060102150405"))

for _, sc := range pssList {
psp.Spec.HostPID = psp.Spec.HostPID || sc.HostPID
psp.Spec.HostIPC = psp.Spec.HostIPC || sc.HostIPC
psp.Spec.HostNetwork = psp.Spec.HostNetwork || sc.HostNetwork

for _, t := range sc.VolumeTypes {
volumeTypes[t] = true
}

for path, readOnly := range sc.MountHostPaths {
if _, exists := hostPaths[path]; !exists {
hostPaths[path] = readOnly
} else {
hostPaths[path] = readOnly && hostPaths[path]
}
}
}

for _, sc := range cssList {
for _, cap := range sc.Capabilities {
effectiveCap[cap] = true
}

for _, cap := range sc.AddedCap {
addedCap[cap]++
}

for _, cap := range sc.DroppedCap {
droppedCap[cap]++
}

psp.Spec.Privileged = psp.Spec.Privileged || sc.Privileged

psp.Spec.ReadOnlyRootFilesystem = psp.Spec.ReadOnlyRootFilesystem || sc.ReadOnlyRootFS

if sc.RunAsNonRoot != nil && *sc.RunAsNonRoot {
runAsNonRootCount++
}

if sc.RunAsUser != nil {
runAsUser[*sc.RunAsUser] = true
runAsUserCount++
}

if sc.AllowPrivilegeEscalation != nil && !*sc.AllowPrivilegeEscalation {
notAllowPrivilegeEscationCount++
}

// set host ports
// TODO: need to integrate with listening port during the runtime, might cause false positive.
//for _, port := range sc.HostPorts {
// psp.Spec.HostPorts = append(psp.Spec.HostPorts, v1beta1.HostPortRange{Min: port, Max: port})
//}
}

// set allowedPrivilegeEscalation
if notAllowPrivilegeEscationCount == len(cssList) {
notAllowed := false
psp.Spec.AllowPrivilegeEscalation = &notAllowed
}

// set runAsUser strategy
if runAsNonRootCount == len(cssList) {
psp.Spec.RunAsUser.Rule = v1beta1.RunAsUserStrategyMustRunAsNonRoot
}

if runAsUserCount == len(cssList) {
psp.Spec.RunAsUser.Rule = v1beta1.RunAsUserStrategyMustRunAs
for uid := range runAsUser {
if psp.Spec.RunAsUser.Rule == v1beta1.RunAsUserStrategyMustRunAsNonRoot && uid != 0 {
psp.Spec.RunAsUser.Ranges = append(psp.Spec.RunAsUser.Ranges, v1beta1.IDRange{
Min: uid,
Max: uid,
})
}
}
}

// set allowed host path
enforceReadOnly, _ := utils.CompareVersion(p.serverGitVersion, types.Version1_11)

for path, readOnly := range hostPaths {
psp.Spec.AllowedHostPaths = append(psp.Spec.AllowedHostPaths, v1beta1.AllowedHostPath{
PathPrefix: path,
ReadOnly: readOnly || enforceReadOnly,
})
}

// set limit volumes
volumeTypeList := utils.MapToArray(volumeTypes)

for _, v := range volumeTypeList {
psp.Spec.Volumes = append(psp.Spec.Volumes, v1beta1.FSType(v))
}

// set allowedCapabilities
defaultCap := utils.ArrayToMap(types.DefaultCaps)
for cap := range defaultCap {
if _, exists := effectiveCap[cap]; exists {
delete(effectiveCap, cap)
}
}

// set allowedAddCapabilities
for cap := range effectiveCap {
psp.Spec.AllowedCapabilities = append(psp.Spec.AllowedCapabilities, v1.Capability(cap))
}

// set defaultAddCapabilities
for k, v := range addedCap {
if v == len(cssList) {
psp.Spec.DefaultAddCapabilities = append(psp.Spec.DefaultAddCapabilities, v1.Capability(k))
}
}

// set requiredDroppedCapabilities
for k, v := range droppedCap {
if v == len(cssList) {
psp.Spec.RequiredDropCapabilities = append(psp.Spec.RequiredDropCapabilities, v1.Capability(k))
}
}

// set to default values
if string(psp.Spec.RunAsUser.Rule) == "" {
psp.Spec.RunAsUser.Rule = v1beta1.RunAsUserStrategyRunAsAny
}

if psp.Spec.RunAsGroup != nil && string(psp.Spec.RunAsGroup.Rule) == "" {
psp.Spec.RunAsGroup.Rule = v1beta1.RunAsGroupStrategyRunAsAny
}

if string(psp.Spec.FSGroup.Rule) == "" {
psp.Spec.FSGroup.Rule = v1beta1.FSGroupStrategyRunAsAny
}

if string(psp.Spec.SELinux.Rule) == "" {
psp.Spec.SELinux.Rule = v1beta1.SELinuxStrategyRunAsAny
}

if string(psp.Spec.SupplementalGroups.Rule) == "" {
psp.Spec.SupplementalGroups.Rule = v1beta1.SupplementalGroupsStrategyRunAsAny
}

return psp
return p.gen.GeneratePSP(cssList, pssList, p.namespace, p.serverGitVersion)
}

// GenerateReport generate a JSON report
Expand Down
Loading

0 comments on commit cc6425d

Please sign in to comment.