-
Notifications
You must be signed in to change notification settings - Fork 186
Adds a new 'mounts' command to audit sensitive host paths mounts #322
Changes from 8 commits
6a42bf1
230e1a6
fa88f5e
ebad8dd
26cc11a
cf59458
57c7453
f2782d0
7eb28a2
0452090
d0fb510
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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 |
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
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" | ||
) | ||
|
||
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" | ||
MountVolumeNameKey = "MountVolume" | ||
MountVolumeHostPathKey = "MountVolumeHostPath" | ||
) | ||
|
||
// 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), | ||
MountVolumeNameKey: volume.Name, | ||
MountVolumeHostPathKey: volume.HostPath.Path, | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
return auditResults | ||
} | ||
|
||
func getOverrideLabel(mountName string) string { | ||
return overrideLabelPrefix + mountName | ||
} |
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) | ||
}) | ||
} | ||
} |
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 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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another tiny thing, and more of a question. How about
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that was my initial idea but
So I fall back on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, yeah! you're right! perhaps |
||||||
"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() | ||||||
} |
There was a problem hiding this comment.
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