|
| 1 | +package auth |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "crypto/ecdsa" |
| 6 | + "crypto/elliptic" |
| 7 | + "crypto/rand" |
| 8 | + "crypto/x509" |
| 9 | + "crypto/x509/pkix" |
| 10 | + "encoding/pem" |
| 11 | + "fmt" |
| 12 | + "math/big" |
| 13 | + "os" |
| 14 | + "testing" |
| 15 | + "time" |
| 16 | + |
| 17 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 18 | + auditinternal "k8s.io/apiserver/pkg/apis/audit" |
| 19 | + auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" |
| 20 | + kubex509 "k8s.io/apiserver/pkg/authentication/request/x509" |
| 21 | + |
| 22 | + clientset "k8s.io/client-go/kubernetes" |
| 23 | + "k8s.io/client-go/rest" |
| 24 | + utiltesting "k8s.io/client-go/util/testing" |
| 25 | + kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" |
| 26 | + "k8s.io/kubernetes/test/integration/framework" |
| 27 | + "k8s.io/kubernetes/test/utils" |
| 28 | +) |
| 29 | + |
| 30 | +var ( |
| 31 | + auditPolicy = ` |
| 32 | +apiVersion: audit.k8s.io/v1 |
| 33 | +kind: Policy |
| 34 | +rules: |
| 35 | + - level: Request |
| 36 | + resources: |
| 37 | + - group: "" |
| 38 | + resources: ["pods"] |
| 39 | + verbs: ["get"] |
| 40 | +` |
| 41 | +) |
| 42 | + |
| 43 | +func TestCerts(t *testing.T) { |
| 44 | + logFile, err := os.CreateTemp("", "audit.log") |
| 45 | + if err != nil { |
| 46 | + t.Fatalf("Failed to create audit log file: %v", err) |
| 47 | + } |
| 48 | + defer utiltesting.CloseAndRemove(t, logFile) |
| 49 | + |
| 50 | + policyFile, err := os.CreateTemp("", "audit-policy.yaml") |
| 51 | + if err != nil { |
| 52 | + t.Fatalf("Failed to create audit policy file: %v", err) |
| 53 | + } |
| 54 | + if _, err := policyFile.Write([]byte(auditPolicy)); err != nil { |
| 55 | + t.Fatalf("Failed to write audit policy file: %v", err) |
| 56 | + } |
| 57 | + |
| 58 | + s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{ |
| 59 | + "--audit-policy-file", policyFile.Name(), |
| 60 | + "--audit-log-version", "audit.k8s.io/v1", |
| 61 | + "--audit-log-mode", "blocking", |
| 62 | + "--audit-log-path", logFile.Name(), |
| 63 | + }, framework.SharedEtcd()) |
| 64 | + defer s.TearDownFn() |
| 65 | + |
| 66 | + // Generate self-signed certificate |
| 67 | + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| 68 | + if err != nil { |
| 69 | + t.Fatal(err) |
| 70 | + } |
| 71 | + keyRaw, err := x509.MarshalECPrivateKey(key) |
| 72 | + if err != nil { |
| 73 | + t.Fatal(err) |
| 74 | + } |
| 75 | + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) |
| 76 | + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
| 77 | + if err != nil { |
| 78 | + t.Fatal(err) |
| 79 | + } |
| 80 | + commonName := "test-cn" |
| 81 | + notBefore := time.Now().Truncate(time.Second) |
| 82 | + notAfter := notBefore.Add(time.Hour) |
| 83 | + cert := &x509.Certificate{ |
| 84 | + SerialNumber: serialNumber, |
| 85 | + Subject: pkix.Name{CommonName: commonName}, |
| 86 | + NotBefore: notBefore, |
| 87 | + NotAfter: notAfter, |
| 88 | + |
| 89 | + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, |
| 90 | + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, |
| 91 | + BasicConstraintsValid: true, |
| 92 | + } |
| 93 | + certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) |
| 94 | + if err != nil { |
| 95 | + t.Fatal(err) |
| 96 | + } |
| 97 | + |
| 98 | + // Use self-signed certificate in client config |
| 99 | + clientConfig := rest.CopyConfig(s.ClientConfig) |
| 100 | + clientConfig.BearerToken = "" |
| 101 | + clientConfig.CertData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}) |
| 102 | + clientConfig.KeyData = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) |
| 103 | + client, err := clientset.NewForConfig(clientConfig) |
| 104 | + if err != nil { |
| 105 | + t.Fatalf("error occurred: %v", err) |
| 106 | + } |
| 107 | + |
| 108 | + // Make a request using the client |
| 109 | + podName := "foobar" |
| 110 | + currentTime := time.Now().Truncate(time.Second) |
| 111 | + _, err = client.CoreV1().Pods("default").Get(context.TODO(), podName, metav1.GetOptions{}) |
| 112 | + if err == nil { |
| 113 | + t.Fatal("expected error, but it was nil") |
| 114 | + } |
| 115 | + |
| 116 | + // Verify that audit log has the expected entry |
| 117 | + stream, err := os.OpenFile(logFile.Name(), os.O_RDWR, 0600) |
| 118 | + if err != nil { |
| 119 | + t.Errorf("unexpected error: %v", err) |
| 120 | + } |
| 121 | + defer stream.Close() |
| 122 | + |
| 123 | + annotationDetails := fmt.Sprintf(`certificate "%s" [client] issuer="<self>" (%s to %s (now=%s)) failed: x509: certificate signed by unknown authority`, commonName, notBefore.UTC(), notAfter.UTC(), currentTime.UTC()) |
| 124 | + expectedEvents := []utils.AuditEvent{ |
| 125 | + { |
| 126 | + Level: auditinternal.LevelRequest, |
| 127 | + Stage: auditinternal.StageResponseStarted, |
| 128 | + RequestURI: fmt.Sprintf("/api/v1/namespaces/default/pods/%s", podName), |
| 129 | + Verb: "get", |
| 130 | + Resource: "pods", |
| 131 | + Namespace: "default", |
| 132 | + Code: 401, |
| 133 | + CustomAuditAnnotations: map[string]string{ |
| 134 | + kubex509.CertificateErrorAuditAnnotation: annotationDetails, |
| 135 | + }, |
| 136 | + }, |
| 137 | + } |
| 138 | + |
| 139 | + auditAnnotationFilter := func(key, val string) bool { |
| 140 | + return key == kubex509.CertificateErrorAuditAnnotation |
| 141 | + } |
| 142 | + |
| 143 | + missing, err := utils.CheckAuditLinesFiltered(stream, expectedEvents, auditv1.SchemeGroupVersion, auditAnnotationFilter) |
| 144 | + if err != nil { |
| 145 | + t.Errorf("unexpected error checking audit lines: %v", err) |
| 146 | + } |
| 147 | + if len(missing.MissingEvents) > 0 { |
| 148 | + t.Errorf("failed to get expected events -- missing: %s", missing) |
| 149 | + } |
| 150 | +} |
0 commit comments