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 || len(config.SensitivePaths) == 0 {
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
120 changes: 120 additions & 0 deletions auditors/mounts/mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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-"

const (
MountNameMetadataKey = "MountName"
MountPathMetadataKey = "MountPath"
MountReadOnlyMetadataKey = "MountReadOnly"
)

// 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[MountNameMetadataKey]))
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 (hostPath: %s). It should be removed from the container's mounts list.", mount.Name, volume.HostPath.Path),
Metadata: kubeaudit.Metadata{
"Container": container.Name,
MountNameMetadataKey: mount.Name,
MountPathMetadataKey: mount.MountPath,
MountReadOnlyMetadataKey: fmt.Sprintf("%t", mount.ReadOnly),
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually can we add the volume name and host path as metadata as well? It might be useful for JSON output

},
})
}
}

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{}

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(sensitivePathsFlagName) {
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)
}
48 changes: 48 additions & 0 deletions cmd/commands/mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package commands

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

const sensitivePathsFlagName = "paths"

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, sensitivePathsFlagName, "s", mounts.DefaultSensitivePaths,
Copy link
Contributor

Choose a reason for hiding this comment

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

Another tiny thing, and more of a question. How about p rather than s for the shorthand?

Suggested change
cmd.Flags().StringSliceVarP(&mountsConfig.SensitivePaths, sensitivePathsFlagName, "s", mounts.DefaultSensitivePaths,
cmd.Flags().StringSliceVarP(&mountsConfig.SensitivePaths, sensitivePathsFlagName, "p", mounts.DefaultSensitivePaths,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that was my initial idea but -p is already used as a shorthand for the --format argument (I guess it comes from pretty?)

  -p, --format string        The output format to use (one of "pretty", "logrus", "json") (default "pretty")

So I fall back on s for "Sensitive paths" but it's indeed not really obvious

Copy link
Contributor

@dani-santos-code dani-santos-code Feb 17, 2021

Choose a reason for hiding this comment

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

oh, yeah! you're right! perhaps sp? not sure...it's optional. We can always revisit that later if someone gives us feedback saying it's confusing.

"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"`
}
Loading