Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit 8ca9060

Browse files
committed
Adds e2e tests for auditing (both file and webhook)
Signed-off-by: JoshVanL <[email protected]>
1 parent ca1f70b commit 8ca9060

File tree

10 files changed

+559
-1
lines changed

10 files changed

+559
-1
lines changed

test/e2e/framework/helper/deploy.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
ProxyName = "kube-oidc-proxy-e2e"
2323
IssuerName = "oidc-issuer-e2e"
2424
FakeAPIServerName = "fake-apiserver-e2e"
25+
AuditWebhookName = "audit-webhook-e2e"
2526
)
2627

2728
func (h *Helper) DeployProxy(ns *corev1.Namespace, issuerURL *url.URL, clientID string,
@@ -266,6 +267,62 @@ func (h *Helper) DeployFakeAPIServer(ns string) ([]corev1.Volume, *url.URL, erro
266267
return extraVolumes, appURL, nil
267268
}
268269

270+
func (h *Helper) DeployAuditWebhook(ns, logPath string) (corev1.Volume, *url.URL, error) {
271+
cnt := corev1.Container{
272+
Name: AuditWebhookName,
273+
Image: AuditWebhookName,
274+
ImagePullPolicy: corev1.PullNever,
275+
Args: []string{
276+
"audit-webhook",
277+
"--secure-port=6443",
278+
"--tls-cert-file=/tls/cert.pem",
279+
"--tls-private-key-file=/tls/key.pem",
280+
"--audit-file-path=" + logPath,
281+
},
282+
VolumeMounts: []corev1.VolumeMount{
283+
corev1.VolumeMount{
284+
MountPath: "/tls",
285+
Name: "tls",
286+
ReadOnly: true,
287+
},
288+
},
289+
Ports: []corev1.ContainerPort{
290+
corev1.ContainerPort{
291+
ContainerPort: 6443,
292+
},
293+
},
294+
}
295+
296+
bundle, appURL, err := h.deployApp(ns, AuditWebhookName, corev1.ServiceTypeClusterIP, cnt)
297+
if err != nil {
298+
return corev1.Volume{}, nil, err
299+
}
300+
301+
sec, err := h.KubeClient.CoreV1().Secrets(ns).Create(&corev1.Secret{
302+
ObjectMeta: metav1.ObjectMeta{
303+
GenerateName: "audit-webhook-ca-",
304+
Namespace: ns,
305+
},
306+
Data: map[string][]byte{
307+
"ca.pem": bundle.CertBytes,
308+
},
309+
})
310+
if err != nil {
311+
return corev1.Volume{}, nil, err
312+
}
313+
314+
auditWebhookCAVol := corev1.Volume{
315+
Name: "audit-webhook-ca",
316+
VolumeSource: corev1.VolumeSource{
317+
Secret: &corev1.SecretVolumeSource{
318+
SecretName: sec.Name,
319+
},
320+
},
321+
}
322+
323+
return auditWebhookCAVol, appURL, nil
324+
}
325+
269326
func (h *Helper) deployApp(ns, name string, serviceType corev1.ServiceType, container corev1.Container, volumes ...corev1.Volume) (*util.KeyBundle, *url.URL, error) {
270327
host, appURL := h.appURL(ns, name, "6443")
271328

test/e2e/suite/cases/audit/audit.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package passthrough
3+
4+
import (
5+
"bufio"
6+
"bytes"
7+
"encoding/json"
8+
"fmt"
9+
"io/ioutil"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"reflect"
14+
"time"
15+
16+
. "github.com/onsi/ginkgo"
17+
. "github.com/onsi/gomega"
18+
19+
authnv1 "k8s.io/api/authentication/v1"
20+
corev1 "k8s.io/api/core/v1"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
23+
24+
"github.com/jetstack/kube-oidc-proxy/test/e2e/framework"
25+
)
26+
27+
var _ = framework.CasesDescribe("Audit", func() {
28+
f := framework.NewDefaultFramework("audit")
29+
30+
It("should be able to write audit logs to file", func() {
31+
By("Creating policy file ConfigMap")
32+
cm, err := f.Helper().KubeClient.CoreV1().ConfigMaps(f.Namespace.Name).Create(&corev1.ConfigMap{
33+
ObjectMeta: metav1.ObjectMeta{
34+
GenerateName: "kube-oidc-proxy-policy-",
35+
},
36+
Data: map[string]string{
37+
"audit.yaml": `apiVersion: audit.k8s.io/v1
38+
kind: Policy
39+
rules:
40+
- level: RequestResponse`,
41+
},
42+
})
43+
Expect(err).NotTo(HaveOccurred())
44+
45+
vols := []corev1.Volume{
46+
corev1.Volume{
47+
Name: "audit",
48+
VolumeSource: corev1.VolumeSource{
49+
ConfigMap: &corev1.ConfigMapVolumeSource{
50+
LocalObjectReference: corev1.LocalObjectReference{
51+
Name: cm.Name,
52+
},
53+
},
54+
},
55+
},
56+
}
57+
58+
By("Deploying proxy with audit policy enabled")
59+
f.DeployProxyWith(vols, "--audit-log-path=/audit-log", "--audit-policy-file=/audit/audit.yaml")
60+
61+
testAuditLogs(f, "app=kube-oidc-proxy-e2e")
62+
})
63+
64+
It("should be able to write audit logs to webhook", func() {
65+
By("Creating policy file ConfigMap")
66+
cmPolicy, err := f.Helper().KubeClient.CoreV1().ConfigMaps(f.Namespace.Name).Create(&corev1.ConfigMap{
67+
ObjectMeta: metav1.ObjectMeta{
68+
GenerateName: "kube-oidc-proxy-policy-",
69+
},
70+
Data: map[string]string{
71+
"audit.yaml": `apiVersion: audit.k8s.io/v1
72+
kind: Policy
73+
rules:
74+
- level: RequestResponse`,
75+
},
76+
})
77+
Expect(err).NotTo(HaveOccurred())
78+
79+
extraWebhookVol, webhookURL, err := f.Helper().DeployAuditWebhook(f.Namespace.Name, "/audit-log")
80+
Expect(err).NotTo(HaveOccurred())
81+
82+
cmWebhook, err := f.Helper().KubeClient.CoreV1().ConfigMaps(f.Namespace.Name).Create(&corev1.ConfigMap{
83+
ObjectMeta: metav1.ObjectMeta{
84+
GenerateName: "kube-oidc-proxy-webhook-config-",
85+
},
86+
Data: map[string]string{
87+
"kubeconfig.yaml": `apiVersion: v1
88+
kind: Config
89+
clusters:
90+
- cluster:
91+
server: ` + webhookURL.String() + `
92+
certificate-authority: /audit-webhook-ca/ca.pem
93+
name: logstash
94+
contexts:
95+
- context:
96+
cluster: logstash
97+
user: ""
98+
name: default-context
99+
current-context: default-context
100+
preferences: {}
101+
users: []`,
102+
},
103+
})
104+
Expect(err).NotTo(HaveOccurred())
105+
106+
vols := []corev1.Volume{
107+
corev1.Volume{
108+
Name: "audit",
109+
VolumeSource: corev1.VolumeSource{
110+
ConfigMap: &corev1.ConfigMapVolumeSource{
111+
LocalObjectReference: corev1.LocalObjectReference{
112+
Name: cmPolicy.Name,
113+
},
114+
},
115+
},
116+
},
117+
corev1.Volume{
118+
Name: "audit-webhook",
119+
VolumeSource: corev1.VolumeSource{
120+
ConfigMap: &corev1.ConfigMapVolumeSource{
121+
LocalObjectReference: corev1.LocalObjectReference{
122+
Name: cmWebhook.Name,
123+
},
124+
},
125+
},
126+
},
127+
extraWebhookVol,
128+
}
129+
130+
By("Deploying proxy with audit policy enabled")
131+
f.DeployProxyWith(vols, "--audit-webhook-config-file=/audit-webhook/kubeconfig.yaml",
132+
"--audit-policy-file=/audit/audit.yaml", "--audit-webhook-initial-backoff=1s", "--audit-webhook-batch-max-wait=1s")
133+
134+
testAuditLogs(f, "app=audit-webhook-e2e")
135+
})
136+
})
137+
138+
func testAuditLogs(f *framework.Framework, podLabelSelector string) {
139+
By("Making calls to proxy to ensure audit get created")
140+
token := f.Helper().NewTokenPayload(f.IssuerURL(), f.ClientID(), time.Now().Add(time.Second*5))
141+
signedToken, err := f.Helper().SignToken(f.IssuerKeyBundle(), token)
142+
Expect(err).NotTo(HaveOccurred())
143+
144+
proxyConfig := f.NewProxyRestConfig()
145+
requester := f.Helper().NewRequester(proxyConfig.Transport, signedToken)
146+
147+
target := fmt.Sprintf("%s/api/v1/namespaces/kube-system/pods", proxyConfig.Host)
148+
149+
// Make request that should succeed
150+
_, _, err = requester.Get(target)
151+
Expect(err).NotTo(HaveOccurred())
152+
153+
// Make request that should be unauthenticated
154+
requester = f.Helper().NewRequester(proxyConfig.Transport, "foo")
155+
_, resp, err := requester.Get(target)
156+
Expect(err).NotTo(HaveOccurred())
157+
158+
if resp.StatusCode != http.StatusUnauthorized {
159+
Expect(fmt.Errorf("expected to get unauthorized, got=%d", resp.StatusCode)).NotTo(HaveOccurred())
160+
}
161+
162+
By("Waiting for audit logs to be written")
163+
time.Sleep(time.Second * 5)
164+
165+
By("Copying audit log from proxy locally")
166+
// Get pod
167+
pods, err := f.Helper().KubeClient.CoreV1().Pods(f.Namespace.Name).List(metav1.ListOptions{
168+
LabelSelector: podLabelSelector,
169+
})
170+
Expect(err).NotTo(HaveOccurred())
171+
if len(pods.Items) != 1 {
172+
Expect(fmt.Errorf("expected single kube-oidc-proxy pod running, got=%d", len(pods.Items))).NotTo(HaveOccurred())
173+
}
174+
175+
tmpDir, err := ioutil.TempDir(os.TempDir(), "kube-oidc-proxy-e2e")
176+
Expect(err).NotTo(HaveOccurred())
177+
178+
defer func() {
179+
err := os.RemoveAll(tmpDir)
180+
Expect(err).NotTo(HaveOccurred())
181+
}()
182+
183+
logFile := filepath.Join(tmpDir, "log.txt")
184+
185+
err = f.Helper().Kubectl(f.Namespace.Name).Run(
186+
"cp", fmt.Sprintf("%s:audit-log", pods.Items[0].Name), logFile)
187+
Expect(err).NotTo(HaveOccurred())
188+
189+
logs, err := ioutil.ReadFile(logFile)
190+
Expect(err).NotTo(HaveOccurred())
191+
192+
scanner := bufio.NewScanner(bytes.NewReader(logs))
193+
194+
expAuditEvents := []auditv1.Event{
195+
auditv1.Event{
196+
Level: auditv1.LevelRequestResponse,
197+
Stage: auditv1.StageRequestReceived,
198+
RequestURI: "/api/v1/namespaces/kube-system/pods",
199+
Verb: "get",
200+
User: authnv1.UserInfo{
201+
Username: "[email protected]",
202+
Groups: []string{"group-1", "group-2"},
203+
},
204+
},
205+
auditv1.Event{
206+
Level: auditv1.LevelRequestResponse,
207+
Stage: auditv1.StageResponseComplete,
208+
RequestURI: "/api/v1/namespaces/kube-system/pods",
209+
Verb: "get",
210+
User: authnv1.UserInfo{
211+
Username: "[email protected]",
212+
Groups: []string{"group-1", "group-2"},
213+
},
214+
ResponseStatus: &metav1.Status{
215+
Code: 403,
216+
},
217+
},
218+
auditv1.Event{
219+
Level: auditv1.LevelRequestResponse,
220+
Stage: auditv1.StageResponseStarted,
221+
RequestURI: "/api/v1/namespaces/kube-system/pods",
222+
Verb: "get",
223+
ResponseStatus: &metav1.Status{
224+
Code: 401,
225+
Message: "Authentication failed, attempted: bearer",
226+
},
227+
},
228+
}
229+
230+
By("Testing for expected audit logs")
231+
var i int
232+
for scanner.Scan() {
233+
if i > len(expAuditEvents) {
234+
Expect(fmt.Errorf("more proxy audit logs than expected, exp=%d got=%s", len(expAuditEvents), logs)).NotTo(HaveOccurred())
235+
}
236+
237+
var auditEvent auditv1.Event
238+
err = json.Unmarshal(scanner.Bytes(), &auditEvent)
239+
Expect(err).NotTo(HaveOccurred())
240+
241+
gotAuditEvent := auditv1.Event{
242+
Level: auditEvent.Level,
243+
Stage: auditEvent.Stage,
244+
RequestURI: auditEvent.RequestURI,
245+
Verb: auditEvent.Verb,
246+
User: authnv1.UserInfo{
247+
Username: auditEvent.User.Username,
248+
Groups: auditEvent.User.Groups,
249+
},
250+
}
251+
252+
if auditEvent.ResponseStatus != nil {
253+
gotAuditEvent.ResponseStatus = &metav1.Status{
254+
Code: auditEvent.ResponseStatus.Code,
255+
Message: auditEvent.ResponseStatus.Message,
256+
}
257+
}
258+
259+
if !reflect.DeepEqual(expAuditEvents[i], gotAuditEvent) {
260+
Expect(fmt.Errorf("unexpected audit event\nexp=%v\ngot=%v", expAuditEvents[i], gotAuditEvent)).NotTo(HaveOccurred())
261+
}
262+
263+
i++
264+
}
265+
266+
if i != len(expAuditEvents) {
267+
Expect(fmt.Errorf("less proxy audit logs then expected, exp=%d, got=%s", len(expAuditEvents), logs)).NotTo(HaveOccurred())
268+
}
269+
}

test/e2e/suite/cases/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package cases
33

44
import (
5+
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/audit"
56
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/headers"
67
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/impersonation"
78
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/passthrough"

test/e2e/suite/cases/upgrade/upgrade.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ var _ = framework.CasesDescribe("Upgrade", func() {
140140
Expect(err).NotTo(HaveOccurred())
141141
}
142142

143-
By(fmt.Sprintf("exec outpu t%s/%s: %s", pod.Namespace, pod.Name, execOut.String()))
143+
By(fmt.Sprintf("exec output %s/%s: %s", pod.Namespace, pod.Name, execOut.String()))
144144

145145
// should have correct stdout output from echo server
146146
if !strings.HasSuffix(execOut.String(), "BODY:\nhello world") {

test/kind/image.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ func (k *Kind) LoadAllImages() error {
2828
return err
2929
}
3030

31+
if err := k.LoadAuditWebhook(); err != nil {
32+
return err
33+
}
34+
3135
return nil
3236
}
3337

@@ -57,6 +61,15 @@ func (k *Kind) LoadFakeAPIServer() error {
5761
return k.loadImage(binPath, mainPath, image, dockerfilePath)
5862
}
5963

64+
func (k *Kind) LoadAuditWebhook() error {
65+
binPath := filepath.Join(k.rootPath, "./test/tools/audit-webhook/bin/audit-webhook")
66+
dockerfilePath := filepath.Join(k.rootPath, "./test/tools/audit-webhook")
67+
mainPath := filepath.Join(dockerfilePath, "cmd")
68+
image := "audit-webhook-e2e"
69+
70+
return k.loadImage(binPath, mainPath, image, dockerfilePath)
71+
}
72+
6073
func (k *Kind) loadImage(binPath, mainPath, image, dockerfilePath string) error {
6174
log.Infof("kind: building %q", mainPath)
6275

test/tools/audit-webhook/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/bin

test/tools/audit-webhook/Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright Jetstack Ltd. See LICENSE for details.
2+
FROM alpine:3.10
3+
4+
LABEL description="A audit webhook sink to read audit events and write to file."
5+
6+
RUN apk --no-cache --update add ca-certificates
7+
8+
COPY ./bin/audit-webhook /usr/bin/audit-webhook
9+
10+
CMD ["/usr/bin/audit-webhook"]

0 commit comments

Comments
 (0)