Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Add EmptyDir size limit policy #40

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ k-rail is a workload policy enforcement tool for Kubernetes. It can help you sec
* [No Exec](#no-exec)
* [No Bind Mounts](#no-bind-mounts)
* [No Docker Sock Mount](#no-docker-sock-mount)
* [Mutate Default Seccomp Profile](#mutate-default-seccomp-profile)
* [EmptyDir size limit](#emptyDir-size-limit)
+ [Policy configuration](#policy-configuration)
* [Mutate Default Seccomp Profile](#mutate-default-seccomp-profile)
+ [Policy configuration](#policy-configuration-1)
* [Immutable Image Reference](#immutable-image-reference)
* [No Host Network](#no-host-network)
* [No Host PID](#no-host-pid)
* [No New Capabilities](#no-new-capabilities)
* [No Privileged Container](#no-privileged-container)
* [No Helm Tiller](#no-helm-tiller)
* [Trusted Image Repository](#trusted-image-repository)
+ [Policy configuration](#policy-configuration-1)
+ [Policy configuration](#policy-configuration-2)
* [Safe to Evict (DEPRECATED)](#safe-to-evict--deprecated)
* [Mutate Safe to Evict](#mutate-safe-to-evict)
* [Require Ingress Exemption](#require-ingress-exemption)
+ [Policy configuration](#policy-configuration-2)
+ [Policy configuration](#policy-configuration-3)
- [Configuration](#configuration)
* [Logging](#logging)
* [Modes of operation](#modes-of-operation)
Expand Down Expand Up @@ -230,6 +232,21 @@ The Docker socket bind mount provides API access to the host Docker daemon, whic

**Note:** It is recommended to use the `No Bind Mounts` policy to disable all `hostPath` mounts rather than only this policy.

## EmptyDir size limit
By [default](https://kubernetes.io/docs/concepts/storage/volumes/#example-pod), an `emptyDir` lacks a `sizeLimit` parameter, and is disk-based;
a Pod with access to said `emptyDir` can consume the Node's entire disk (i.e. the limit is unbounded) until the offending Pod is deleted or evicted, which can constitute a denial-of-service condition at the affected Node (i.e. DiskPressure).
This policy
* sets the configured default size when none is set for an `emptyDir` volume
* reports a violation when the size is greater then the configured max size

### Policy configuration
```yaml
policy_config:
mutate_empty_dir_size_limit:
maximum_size_limit: "1Gi"
default_size_limit: "512Mi"
```

## Mutate Default Seccomp Profile

Sets a default seccomp profile (`runtime/default` or a configured one) for Pods if they have no existing seccomp configuration. The default seccomp policy for Docker and Containerd both block over 40 syscalls, [many of which](https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile) are potentially dangerous. The default policies are [usually very compatible](https://blog.jessfraz.com/post/containers-security-and-echo-chambers/#breaking-changes) with applications, too.
Expand Down
6 changes: 6 additions & 0 deletions deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ config:
- '^k8s.gcr.io/.*' # official k8s GCR repo
- '^[A-Za-z0-9\-:@]+$' # official docker hub images
policy_default_seccomp_policy: "runtime/default"
mutate_empty_dir_size_limit:
maximum_size_limit: "1Gi"
default_size_limit: "512Mi"
policies:
- name: "pod_no_exec"
enabled: True
Expand Down Expand Up @@ -73,6 +76,9 @@ config:
- name: "pod_mutate_safe_to_evict"
enabled: True
report_only: False
- name: "pod_empty_dir_size_limit"
enabled: True
report_only: False
- name: "pod_default_seccomp_policy"
enabled: True
report_only: False
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ require (
golang.org/x/text v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/inf.v0 v0.9.0 // indirect
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.2.2 // indirect
k8s.io/api v0.0.0-20190301173355-16f65c82b8fa
k8s.io/apimachinery v0.0.0-20190301173222-2f7e9cae4418
k8s.io/klog v0.0.0-20181108234604-8139d8cb77af // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
sigs.k8s.io/yaml v1.1.0
)

replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0
49 changes: 46 additions & 3 deletions policies/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,57 @@

package policies

import (
"encoding/json"
"errors"
"fmt"

apiresource "k8s.io/apimachinery/pkg/api/resource"
)

// Config contains configuration for Policies
type Config struct {
// PolicyRequireIngressExemptionClasses contains the Ingress classes that an exemption is required for
// to use. Typically this would include your public ingress classes.
PolicyRequireIngressExemptionClasses []string `yaml:"policy_require_ingress_exemption_classes"`
PolicyRequireIngressExemptionClasses []string `json:"policy_require_ingress_exemption_classes"`
// PolicyTrustedRepositoryRegexes contains regexes that match image repositories that you want to allow.
PolicyTrustedRepositoryRegexes []string `yaml:"policy_trusted_repository_regexes"`
PolicyTrustedRepositoryRegexes []string `json:"policy_trusted_repository_regexes"`
// PolicyDefaultSeccompPolicy contains the seccomp policy that you want to be applied on Pods by default.
// Defaults to 'runtime/default'
PolicyDefaultSeccompPolicy string `yaml:"policy_default_seccomp_policy"`
PolicyDefaultSeccompPolicy string `json:"policy_default_seccomp_policy"`

MutateEmptyDirSizeLimit MutateEmptyDirSizeLimit `json:"mutate_empty_dir_size_limit"`
}

type MutateEmptyDirSizeLimit struct {
MaximumSizeLimit apiresource.Quantity `json:"maximum_size_limit"`
DefaultSizeLimit apiresource.Quantity `json:"default_size_limit"`
}

func (m *MutateEmptyDirSizeLimit) UnmarshalJSON(value []byte) error {
var v map[string]json.RawMessage
if err := json.Unmarshal(value, &v); err != nil {
return err
}

if max, ok := v["maximum_size_limit"]; ok {
if err := m.MaximumSizeLimit.UnmarshalJSON(max); err != nil {
return fmt.Errorf("maximum_size_limit failed: %s", err)
}
}
if def, ok := v["default_size_limit"]; ok {
if err := m.DefaultSizeLimit.UnmarshalJSON(def); err != nil {
return fmt.Errorf("default_size_limit failed: %s", err)
}
}
if m.DefaultSizeLimit.IsZero() {
return errors.New("default size must not be empty")
}
if m.MaximumSizeLimit.IsZero() {
return errors.New("max size must not be empty")
}
if m.DefaultSizeLimit.Cmp(m.MaximumSizeLimit) > 0 {
return errors.New("default size must not be greater than max size")
}
return nil
}
77 changes: 77 additions & 0 deletions policies/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package policies

import (
"reflect"
"testing"

apiresource "k8s.io/apimachinery/pkg/api/resource"
"sigs.k8s.io/yaml"
)

func TestMutateEmptyDirSizeLimit(t *testing.T) {
specs := map[string]struct {
src string
exp *MutateEmptyDirSizeLimit
expErr bool
}{

"all good": {
src: `
mutate_empty_dir_size_limit:
maximum_size_limit: "1Gi"
default_size_limit: "512Mi"
`,
exp: &MutateEmptyDirSizeLimit{
MaximumSizeLimit: apiresource.MustParse("1Gi"),
DefaultSizeLimit: apiresource.MustParse("512Mi"),
},
},
"default > max": {
src: `
mutate_empty_dir_size_limit:
maximum_size_limit: "1Gi"
default_size_limit: "2Gi"
`,
expErr: true,
},
"default not set": {
src: `
mutate_empty_dir_size_limit:
maximum_size_limit: "1Gi"
`,
expErr: true,
},
"max not set": {
src: `
mutate_empty_dir_size_limit:
default_size_limit: "2Gi"
`,
expErr: true,
},
"unsupported type": {
src: `
mutate_empty_dir_size_limit:
default_size_limit: "2ALX"
maximum_size_limit: "2ALX"
`,
expErr: true,
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
var cfg Config
switch err := yaml.Unmarshal([]byte(spec.src), &cfg); {
case spec.expErr && err != nil:
return
case spec.expErr:
t.Fatal("expected error")
case !spec.expErr && err != nil:
t.Fatalf("unexpected error: %+v", err)
}
if exp, got := *spec.exp, cfg.MutateEmptyDirSizeLimit; !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v but got %v", exp, got)
}
})
}

}
12 changes: 6 additions & 6 deletions policies/exemption.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import (

"github.com/gobwas/glob"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
authenticationv1 "k8s.io/api/authentication/v1"
"sigs.k8s.io/yaml"
)

// RawExemption is the configuration for a policy exemption
type RawExemption struct {
ResourceName string `yaml:"resource_name"`
Namespace string `yaml:"namespace"`
Username string `yaml:"username"`
Group string `yaml:"group"`
ExemptPolicies []string `yaml:"exempt_policies"`
ResourceName string `json:"resource_name"`
Namespace string `json:"namespace"`
Username string `json:"username"`
Group string `json:"group"`
ExemptPolicies []string `json:"exempt_policies"`
}

// CompiledExemption is the compiled configuration for a policy exemption
Expand Down
81 changes: 81 additions & 0 deletions policies/pod/empty_dir_size_limit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2019 Cruise LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// https://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.package ingress

package pod

import (
"context"
"fmt"

"github.com/cruise-automation/k-rail/policies"
"github.com/cruise-automation/k-rail/resource"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
)

type PolicyEmptyDirSizeLimit struct {
}

func (p PolicyEmptyDirSizeLimit) Name() string {
return "pod_empty_dir_size_limit"
}

const violationText = "Empty dir size limit: size limit exceeds the max value"

func (p PolicyEmptyDirSizeLimit) Validate(ctx context.Context, config policies.Config, ar *admissionv1beta1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) {
var resourceViolations []policies.ResourceViolation

podResource := resource.GetPodResource(ar, ctx)
if podResource == nil {
return resourceViolations, nil
}

cfg := config.MutateEmptyDirSizeLimit
var patches []policies.PatchOperation

for i, volume := range podResource.PodSpec.Volumes {
if volume.EmptyDir == nil {
continue
}
if volume.EmptyDir.SizeLimit == nil || volume.EmptyDir.SizeLimit.IsZero() {
patches = append(patches, policies.PatchOperation{
Op: "replace",
Path: fmt.Sprintf(volumePatchPath(podResource.ResourceKind)+"/%d/emptyDir/sizeLimit", i),
Value: cfg.DefaultSizeLimit.String(),
})
continue
}

if volume.EmptyDir.SizeLimit.Cmp(cfg.MaximumSizeLimit) > 0 {
resourceViolations = append(resourceViolations, policies.ResourceViolation{
Namespace: ar.Namespace,
ResourceName: podResource.ResourceName,
ResourceKind: podResource.ResourceKind,
Violation: violationText,
Policy: p.Name(),
})
}
}
return resourceViolations, patches
}

const templateVolumePath = "/spec/template/spec/volumes"

func volumePatchPath(podKind string) string {
alpe marked this conversation as resolved.
Show resolved Hide resolved
nonTemplateKinds := map[string]string{
"Pod": "/spec/volumes",
"CronJob": "/spec/jobTemplate/spec/template/spec/volumes",
}
if pathPath, ok := nonTemplateKinds[podKind]; ok {
return pathPath
}
return templateVolumePath
}