From a3f73cd9f05dc103cb0fa0b4d9419a54ab0cf56c Mon Sep 17 00:00:00 2001 From: Dustin Decker Date: Mon, 4 Jan 2021 11:45:25 -0800 Subject: [PATCH] Add Deny Unconfined AppArmor policy (#95) Signed-off-by: Dustin Decker --- README.md | 5 ++ charts/k-rail/Chart.yaml | 2 +- charts/k-rail/values.yaml | 3 + policies/pod/deny_unconfined_apparmor.go | 56 +++++++++++++ policies/pod/deny_unconfined_apparmor_test.go | 79 +++++++++++++++++++ server/policies.go | 1 + 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 policies/pod/deny_unconfined_apparmor.go create mode 100644 policies/pod/deny_unconfined_apparmor_test.go diff --git a/README.md b/README.md index 1bff8fd..582fedc 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ k-rail is a workload policy enforcement tool for Kubernetes. It can help you sec - [No Anonymous Role Binding](#no-anonymous-role-binding) - [Invalid Pod Disruption Budget](#invalid-pod-disruption-budget) - [No External IP on Service](#no-external-ip-on-service) + - [Deny Unconfined AppArmor Policies](#deny-unconfined-apparmor-policies) - [Configuration](#configuration) - [Webhook Configuration](#webhook-configuration) - [Logging](#logging) @@ -461,6 +462,10 @@ Prevent misconfigured pod disruption budgets from disrupting normal system maint Prevents providing External IPs on a Service to mitigate [CVE-2020-8554](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8554). +## Deny Unconfined AppArmor Policies + +Prevents users from specifing an unconfined apparmor policy which can be used with other conditions to lead to [container escape](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/). + # Configuration For the Helm deployment, all configuration is contained in [`charts/k-rail/values.yaml`](charts/k-rail/values.yaml). diff --git a/charts/k-rail/Chart.yaml b/charts/k-rail/Chart.yaml index 8133afc..65db9e3 100644 --- a/charts/k-rail/Chart.yaml +++ b/charts/k-rail/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v1 name: k-rail description: Kubernetes security tool for policy enforcement home: https://github.com/cruise-automation/k-rail -version: v2.5.0 +version: v2.6.0 diff --git a/charts/k-rail/values.yaml b/charts/k-rail/values.yaml index 3e3bf2a..44d6733 100644 --- a/charts/k-rail/values.yaml +++ b/charts/k-rail/values.yaml @@ -123,6 +123,9 @@ config: - name: "service_no_external_ip" enabled: True report_only: False + - name: "pod_deny_unconfined_apparmor_policy" + enabled: True + report_only: False exemptions: - resource_name: "*" diff --git a/policies/pod/deny_unconfined_apparmor.go b/policies/pod/deny_unconfined_apparmor.go new file mode 100644 index 0000000..caf5f60 --- /dev/null +++ b/policies/pod/deny_unconfined_apparmor.go @@ -0,0 +1,56 @@ +// 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 pod + +import ( + "context" + "strings" + + "github.com/cruise-automation/k-rail/policies" + "github.com/cruise-automation/k-rail/resource" + admissionv1beta1 "k8s.io/api/admission/v1beta1" +) + +type PolicyDenyUnconfinedApparmorPolicy struct{} + +func (p PolicyDenyUnconfinedApparmorPolicy) Name() string { + return "pod_deny_unconfined_apparmor_policy" +} + +func (p PolicyDenyUnconfinedApparmorPolicy) Validate(ctx context.Context, config policies.Config, ar *admissionv1beta1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { + resourceViolations := []policies.ResourceViolation{} + + podResource := resource.GetPodResource(ctx, ar) + if podResource == nil { + return nil, nil + } + + if podResource.ResourceKind == "Pod" { + for name, value := range podResource.PodAnnotations { + if strings.HasPrefix(name, "container.apparmor.security.beta.kubernetes.io") { + if value == "unconfined" { + resourceViolations = append(resourceViolations, policies.ResourceViolation{ + Namespace: ar.Namespace, + ResourceName: podResource.ResourceName, + ResourceKind: podResource.ResourceKind, + Violation: violationText, + Policy: p.Name(), + Error: nil, + }) + } + } + } + } + + return resourceViolations, nil +} diff --git a/policies/pod/deny_unconfined_apparmor_test.go b/policies/pod/deny_unconfined_apparmor_test.go new file mode 100644 index 0000000..823ed71 --- /dev/null +++ b/policies/pod/deny_unconfined_apparmor_test.go @@ -0,0 +1,79 @@ +// 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 pod + +import ( + "context" + "encoding/json" + "testing" + + "github.com/cruise-automation/k-rail/policies" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestPolicyDenyUnconfinedApparmorPolicy(t *testing.T) { + type args struct { + ctx context.Context + config policies.Config + ar *admissionv1beta1.AdmissionRequest + } + tests := []struct { + name string + podSpec v1.PodSpec + annotations map[string]string + violations int + }{ + { + name: "violation", + podSpec: v1.PodSpec{}, + annotations: map[string]string{ + "container.apparmor.security.beta.kubernetes.io/app": "unconfined", + }, + violations: 1, + }, + { + name: "no violation", + podSpec: v1.PodSpec{}, + annotations: map[string]string{}, + violations: 0, + }, + { + name: "no violation, using other than unconfined", + podSpec: v1.PodSpec{}, + annotations: map[string]string{ + "container.apparmor.security.beta.kubernetes.io/app": "runtime/default", + }, + violations: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := PolicyDenyUnconfinedApparmorPolicy{} + raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec, ObjectMeta: metav1.ObjectMeta{Annotations: tt.annotations}}) + ar := &admissionv1beta1.AdmissionRequest{ + Namespace: "namespace", + Name: "name", + Object: runtime.RawExtension{Raw: raw}, + Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + } + got, _ := p.Validate(context.Background(), policies.Config{}, ar) + if len(got) != tt.violations { + t.Errorf("PolicyDenyUnconfinedApparmorPolicy.Validate() got = %v, want %v", len(got), tt.violations) + } + }) + } +} diff --git a/server/policies.go b/server/policies.go index 8ce5656..36f51e6 100644 --- a/server/policies.go +++ b/server/policies.go @@ -57,6 +57,7 @@ func (s *Server) registerPolicies() { s.registerPolicy(pod.PolicyDefaultSeccompPolicy{}) s.registerPolicy(pod.PolicyNoShareProcessNamespace{}) s.registerPolicy(pod.PolicyImagePullPolicy{}) + s.registerPolicy(pod.PolicyDenyUnconfinedApparmorPolicy{}) s.registerPolicy(ingress.PolicyRequireIngressExemption{}) s.registerPolicy(service.PolicyRequireServiceLoadbalancerExemption{}) s.registerPolicy(service.PolicyServiceNoExternalIP{})