From 63d1d5a500b8b405fcd598a1c758a60b5469d416 Mon Sep 17 00:00:00 2001 From: "Tim St. Clair" Date: Fri, 26 May 2017 18:05:45 -0700 Subject: [PATCH] Add AdvancedAuditing E2E test --- test/e2e/BUILD | 1 + test/e2e/audit.go | 169 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 test/e2e/audit.go diff --git a/test/e2e/BUILD b/test/e2e/BUILD index 2c96bf43e57..5155a8bd6f4 100644 --- a/test/e2e/BUILD +++ b/test/e2e/BUILD @@ -44,6 +44,7 @@ go_library( srcs = [ "addon_update.go", "apparmor.go", + "audit.go", "cadvisor.go", "certificates.go", "cluster_upgrade.go", diff --git a/test/e2e/audit.go b/test/e2e/audit.go new file mode 100644 index 00000000000..3536bcf39c1 --- /dev/null +++ b/test/e2e/audit.go @@ -0,0 +1,169 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 + + http://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 e2e + +import ( + "bufio" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiv1 "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/test/e2e/framework" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = framework.KubeDescribe("Advanced Audit [Feature:Audit]", func() { + f := framework.NewDefaultFramework("audit") + + It("should audit API calls", func() { + namespace := f.Namespace.Name + + // Create & Delete pod + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-pod", + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{{ + Name: "pause", + Image: framework.GetPauseImageName(f.ClientSet), + }}, + }, + } + f.PodClient().CreateSync(pod) + f.PodClient().DeleteSync(pod.Name, &metav1.DeleteOptions{}, framework.DefaultPodDeletionTimeout) + + // Create, Read, Delete secret + secret := &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-secret", + }, + Data: map[string][]byte{ + "top-secret": []byte("foo-bar"), + }, + } + _, err := f.ClientSet.Core().Secrets(f.Namespace.Name).Create(secret) + framework.ExpectNoError(err, "failed to create audit-secret") + _, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Get(secret.Name, metav1.GetOptions{}) + framework.ExpectNoError(err, "failed to get audit-secret") + err = f.ClientSet.Core().Secrets(f.Namespace.Name).Delete(secret.Name, &metav1.DeleteOptions{}) + framework.ExpectNoError(err, "failed to delete audit-secret") + + // /version should not be audited + _, err = f.ClientSet.Core().RESTClient().Get().AbsPath("/version").DoRaw() + framework.ExpectNoError(err, "failed to query version") + + expectedEvents := []auditEvent{{ + method: "create", + namespace: namespace, + uri: fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace), + response: "201", + }, { + method: "delete", + namespace: namespace, + uri: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, pod.Name), + response: "200", + }, { + method: "create", + namespace: namespace, + uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace), + response: "201", + }, { + method: "get", + namespace: namespace, + uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", namespace, secret.Name), + response: "200", + }, { + method: "delete", + namespace: namespace, + uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", namespace, secret.Name), + response: "200", + }} + expectAuditLines(f, expectedEvents) + }) +}) + +type auditEvent struct { + method, namespace, uri, response string +} + +// Search the audit log for the expected audit lines. +func expectAuditLines(f *framework.Framework, expected []auditEvent) { + expectations := map[auditEvent]bool{} + for _, event := range expected { + expectations[event] = false + } + + // Fetch the log stream. + stream, err := f.ClientSet.Core().RESTClient().Get().AbsPath("/logs/kube-apiserver-audit.log").Stream() + framework.ExpectNoError(err, "could not read audit log") + defer stream.Close() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + line := scanner.Text() + event, err := parseAuditLine(line) + framework.ExpectNoError(err) + + // If the event was expected, mark it as found. + if _, found := expectations[event]; found { + expectations[event] = true + } + + // /version should not be audited (filtered in the policy). + Expect(event.uri).NotTo(HavePrefix("/version")) + } + framework.ExpectNoError(scanner.Err(), "error reading audit log") + + for event, found := range expectations { + Expect(found).To(BeTrue(), "Event %#v not found!", event) + } +} + +func parseAuditLine(line string) (auditEvent, error) { + fields := strings.Fields(line) + if len(fields) < 3 { + return auditEvent{}, fmt.Errorf("could not parse audit line: %s", line) + } + // Ignore first field (timestamp) + if fields[1] != "AUDIT:" { + return auditEvent{}, fmt.Errorf("unexpected audit line format: %s", line) + } + fields = fields[2:] + event := auditEvent{} + for _, f := range fields { + parts := strings.SplitN(f, "=", 2) + if len(parts) != 2 { + return auditEvent{}, fmt.Errorf("could not parse audit line (part: %q): %s", f, line) + } + value := strings.Trim(parts[1], "\"") + switch parts[0] { + case "method": + event.method = value + case "namespace": + event.namespace = value + case "uri": + event.uri = value + case "response": + event.response = value + } + } + return event, nil +}