Skip to content
This repository has been archived by the owner on Oct 30, 2024. It is now read-only.

Adds a new 'mounts' command to audit sensitive host paths mounts #322

Merged
merged 11 commits into from
Feb 24, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Auditors can also be run individually.
| `image` | Finds containers which do not use the desired version of an image (via the tag) or use an image without a tag. | [docs](docs/auditors/image.md) |
| `limits` | Finds containers which exceed the specified CPU and memory limits or do not specify any. | [docs](docs/auditors/limits.md) |
| `mountds` | Finds containers that have docker socket mounted. | [docs](docs/auditors/mountds.md) |
| `mounts` | Finds containers that have sensitive host paths mounted. | [docs](docs/auditors/mountds.md) |
| `netpols` | Finds namespaces that do not have a default-deny network policy. | [docs](docs/auditors/netpols.md) |
| `nonroot` | Finds containers running as root. | [docs](docs/auditors/nonroot.md) |
| `privesc` | Finds containers that allow privilege escalation. | [docs](docs/auditors/privesc.md) |
Expand Down
4 changes: 4 additions & 0 deletions auditors/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/Shopify/kubeaudit/auditors/image"
"github.com/Shopify/kubeaudit/auditors/limits"
"github.com/Shopify/kubeaudit/auditors/mountds"
"github.com/Shopify/kubeaudit/auditors/mounts"
"github.com/Shopify/kubeaudit/auditors/netpols"
"github.com/Shopify/kubeaudit/auditors/nonroot"
"github.com/Shopify/kubeaudit/auditors/privesc"
Expand All @@ -30,6 +31,7 @@ var AuditorNames = []string{
hostns.Name,
image.Name,
limits.Name,
mounts.Name,
mountds.Name,
netpols.Name,
nonroot.Name,
Expand Down Expand Up @@ -71,6 +73,8 @@ func initAuditor(name string, conf config.KubeauditConfig) (kubeaudit.Auditable,
return image.New(conf.GetAuditorConfigs().Image), nil
case limits.Name:
return limits.New(conf.GetAuditorConfigs().Limits)
case mounts.Name:
return mounts.New(conf.GetAuditorConfigs().Mounts), nil
case mountds.Name:
return mountds.New(), nil
case netpols.Name:
Expand Down
12 changes: 12 additions & 0 deletions auditors/mounts/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mounts

type Config struct {
SensitivePaths []string `yaml:"paths"`
}

func (config *Config) GetSensitivePaths() []string {
if config == nil {
return DefaultSensitivePaths
}
return config.SensitivePaths
}
16 changes: 16 additions & 0 deletions auditors/mounts/fixtures/docker-sock-mounted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
namespace: docker-sock-mounted
spec:
containers:
- name: container
image: scratch
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock-volume
volumes:
- name: docker-sock-volume
hostPath:
path: /var/run/docker.sock
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
labels:
name: pod
container.audit.kubernetes.io/container1.allow-host-path-mount-proc-volume: "SomeReason"
container.audit.kubernetes.io/container2.allow-host-path-mount-proc-volume: "SomeReason"
namespace: proc-mounted-allowed-multi-containers-multi-labels
spec:
containers:
- name: container1
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
- name: container2
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
volumes:
- name: proc-volume
hostPath:
path: /proc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
labels:
name: pod
container.audit.kubernetes.io/container1.allow-host-path-mount-proc-volume: "SomeReason"
namespace: proc-mounted-allowed-multi-containers-single-label
spec:
containers:
- name: container1
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
- name: container2
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
volumes:
- name: proc-volume
hostPath:
path: /proc
19 changes: 19 additions & 0 deletions auditors/mounts/fixtures/proc-mounted-allowed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
labels:
name: pod
audit.kubernetes.io/pod.allow-host-path-mount-proc-volume: "SomeReason"
namespace: proc-mounted-allowed
spec:
containers:
- name: container
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
volumes:
- name: proc-volume
hostPath:
path: /proc
16 changes: 16 additions & 0 deletions auditors/mounts/fixtures/proc-mounted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
namespace: proc-mounted
spec:
containers:
- name: container
image: scratch
volumeMounts:
- mountPath: /host/proc
name: proc-volume
volumes:
- name: proc-volume
hostPath:
path: /proc
112 changes: 112 additions & 0 deletions auditors/mounts/mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package mounts

import (
"fmt"
"github.com/Shopify/kubeaudit"
"github.com/Shopify/kubeaudit/internal/k8s"
"github.com/Shopify/kubeaudit/internal/override"
"github.com/Shopify/kubeaudit/k8stypes"
v1 "k8s.io/api/core/v1"
"strings"
)

const Name = "mounts"

const (
// SensitivePathsMounted occurs when a container has sensitive host paths mounted
SensitivePathsMounted = "SensitivePathsMounted"
)

// DefaultSensitivePaths is the default list of sensitive mount paths (from Falco rule: https://github.com/falcosecurity/falco/blob/master/rules/k8s_audit_rules.yaml#L155)
var DefaultSensitivePaths = []string{"/proc", "/var/run/docker.sock", "/", "/etc", "/root", "/var/run/crio/crio.sock", "/home/admin", "/var/lib/kubelet", "/var/lib/kubelet/pki", "/etc/kubernetes", "/etc/kubernetes/manifests"}

const overrideLabelPrefix = "allow-host-path-mount-"

// SensitivePathMounts implements Auditable
type SensitivePathMounts struct {
sensitivePaths map[string]bool
}

func New(config Config) *SensitivePathMounts {
paths := make(map[string]bool)
for _, path := range config.GetSensitivePaths() {
paths[path] = true
}
return &SensitivePathMounts{
sensitivePaths: paths,
}
}

// Audit checks that the container does not have any sensitive host path
func (sensitive *SensitivePathMounts) Audit(resource k8stypes.Resource, _ []k8stypes.Resource) ([]*kubeaudit.AuditResult, error) {
var auditResults []*kubeaudit.AuditResult

spec := k8s.GetPodSpec(resource)
if spec == nil {
return auditResults, nil
}

sensitiveVolumes := auditPodVolumes(spec, sensitive.sensitivePaths)

if len(sensitiveVolumes) == 0 {
return auditResults, nil
}

for _, container := range k8s.GetContainers(resource) {
for _, auditResult := range auditContainer(container, sensitiveVolumes) {
auditResult = override.ApplyOverride(auditResult, container.Name, resource, getOverrideLabel(auditResult.Metadata["Mount"]))
if auditResult != nil {
auditResults = append(auditResults, auditResult)
}
}
}

return auditResults, nil
}

func auditPodVolumes(podSpec *k8stypes.PodSpecV1, sensitivePaths map[string]bool) map[string]v1.Volume {
if podSpec.Volumes == nil {
return nil
}

found := make(map[string]v1.Volume)
for _, volume := range podSpec.Volumes {
if volume.HostPath == nil {
continue
}

if _, ok := sensitivePaths[volume.HostPath.Path]; ok {
found[volume.Name] = volume
}
}

return found
}

func auditContainer(container *k8stypes.ContainerV1, sensitiveVolumes map[string]v1.Volume) []*kubeaudit.AuditResult {
if container.VolumeMounts == nil {
return nil
}

var auditResults []*kubeaudit.AuditResult

for _, mount := range container.VolumeMounts {
if volume, ok := sensitiveVolumes[mount.Name]; ok {
auditResults = append(auditResults, &kubeaudit.AuditResult{
Name: SensitivePathsMounted,
Severity: kubeaudit.Error,
Message: fmt.Sprintf("Sensitive path mounted as volume: %s (%s -> %s, readOnly: %t). It should be removed from the container's mounts list.", mount.Name, volume.HostPath.Path, mount.MountPath, mount.ReadOnly),
Metadata: kubeaudit.Metadata{
"Container": container.Name,
"Mount": mount.Name,
},
})
}
}

return auditResults
}

func getOverrideLabel(mountName string) string {
return overrideLabelPrefix + strings.Replace(strings.ToLower(mountName), "_", "-", -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return overrideLabelPrefix + strings.Replace(strings.ToLower(mountName), "_", "-", -1)
return overrideLabelPrefix + mountName

}
34 changes: 34 additions & 0 deletions auditors/mounts/mounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package mounts

import (
"github.com/Shopify/kubeaudit/internal/override"
"strings"
"testing"

"github.com/Shopify/kubeaudit/internal/test"
)

const fixtureDir = "fixtures"

func TestSensitivePathsMounted(t *testing.T) {
cases := []struct {
file string
fixtureDir string
expectedErrors []string
}{
{"docker-sock-mounted.yml", fixtureDir, []string{SensitivePathsMounted}},
{"proc-mounted.yml", fixtureDir, []string{SensitivePathsMounted}},
{"proc-mounted-allowed.yml", fixtureDir, []string{override.GetOverriddenResultName(SensitivePathsMounted)}},
{"proc-mounted-allowed-multi-containers-multi-labels.yml", fixtureDir, []string{override.GetOverriddenResultName(SensitivePathsMounted)}},
{"proc-mounted-allowed-multi-containers-single-label.yml", fixtureDir, []string{SensitivePathsMounted, override.GetOverriddenResultName(SensitivePathsMounted)}},
}

config := Config{SensitivePaths: DefaultSensitivePaths}
jcbbc marked this conversation as resolved.
Show resolved Hide resolved

for _, tc := range cases {
t.Run(tc.file, func(t *testing.T) {
test.AuditManifest(t, tc.fixtureDir, tc.file, New(config), tc.expectedErrors)
test.AuditLocal(t, tc.fixtureDir, tc.file, New(config), strings.Split(tc.file, ".")[0], tc.expectedErrors)
})
}
}
5 changes: 5 additions & 0 deletions cmd/commands/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func setConfigFromFlags(cmd *cobra.Command, conf config.KubeauditConfig) config.
conf.AuditorConfig.Capabilities.AllowAddList = capabilitiesConfig.AllowAddList
}

if flagset.Changed("paths") {
jcbbc marked this conversation as resolved.
Show resolved Hide resolved
conf.AuditorConfig.Mounts.SensitivePaths = mountsConfig.SensitivePaths
}

return conf
}

Expand Down Expand Up @@ -88,4 +92,5 @@ func init() {
setImageFlags(auditAllCmd)
setLimitsFlags(auditAllCmd)
setCapabilitiesFlags(auditAllCmd)
setPathsFlags(auditAllCmd)
}
46 changes: 46 additions & 0 deletions cmd/commands/mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package commands

import (
"bytes"
"fmt"
"github.com/Shopify/kubeaudit/auditors/mounts"
"github.com/spf13/cobra"
"strings"
)

var mountsConfig mounts.Config

var mountsCmd = &cobra.Command{
Use: "mounts",
Short: "Audit containers that mount sensitive paths",
Long: fmt.Sprintf(`This command determines which containers mount sensitive host paths. If no paths list is provided, the following
paths are used:
%s

A WARN result is generated when a container mounts one or more paths specified with the '--paths' argument.

Example usage:
kubeaudit mount --paths "%s"`, formatPathsList(), strings.Join(mounts.DefaultSensitivePaths[:3], ",")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
kubeaudit mount --paths "%s"`, formatPathsList(), strings.Join(mounts.DefaultSensitivePaths[:3], ",")),
kubeaudit mounts --paths "%s"`, formatPathsList(), strings.Join(mounts.DefaultSensitivePaths[:3], ",")),

Run: func(cmd *cobra.Command, args []string) {
runAudit(mounts.New(mountsConfig))(cmd, args)
},
}

func init() {
RootCmd.AddCommand(mountsCmd)
setPathsFlags(mountsCmd)
}

func setPathsFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&mountsConfig.SensitivePaths, "paths", "s", mounts.DefaultSensitivePaths,
"List of sensitive paths that shouldn't be mounted")
}

func formatPathsList() string {
var buffer bytes.Buffer
for _, path := range mounts.DefaultSensitivePaths {
buffer.WriteString("\n- ")
buffer.WriteString(path)
}
return buffer.String()
}
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"github.com/Shopify/kubeaudit/auditors/mounts"
"io"
"io/ioutil"

Expand Down Expand Up @@ -54,4 +55,5 @@ type AuditorConfig struct {
Capabilities capabilities.Config `yaml:"capabilities"`
Image image.Config `yaml:"image"`
Limits limits.Config `yaml:"limits"`
Mounts mounts.Config `yaml:"mounts"`
}
3 changes: 3 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enabledAuditors:
image: true
limits: true
mountds: true
mounts: true
netpols: true
nonroot: true
privesc: true
Expand All @@ -24,3 +25,5 @@ auditors:
limits:
cpu: "750m"
memory: "500m"
mounts:
paths: ["/proc", "/var/run/docker.sock", "/", "/etc", "/root", "/var/run/crio/crio.sock", "/home/admin", "/var/lib/kubelet", "/var/lib/kubelet/pki", "/etc/kubernetes", "/etc/kubernetes/manifests"]
Loading