Skip to content

Commit b4713f3

Browse files
committed
authentication: when cert verification fails store details in audit log
In some cases finding the invalid cert by its serial number is tedious. However we can't expose more PII in kube-apiserver logs. This commit will add an audit log annotations with additional details: common name, issuer common name and serial number
1 parent a3d0b2c commit b4713f3

File tree

2 files changed

+172
-0
lines changed
  • staging/src/k8s.io/apiserver/pkg/authentication/request/x509
  • test/integration/auth

2 files changed

+172
-0
lines changed

staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
asn1util "k8s.io/apimachinery/pkg/apis/asn1"
3131
utilerrors "k8s.io/apimachinery/pkg/util/errors"
3232
"k8s.io/apimachinery/pkg/util/sets"
33+
"k8s.io/apiserver/pkg/audit"
3334
"k8s.io/apiserver/pkg/authentication/authenticator"
3435
"k8s.io/apiserver/pkg/authentication/user"
3536
"k8s.io/apiserver/pkg/features"
@@ -38,6 +39,10 @@ import (
3839
"k8s.io/component-base/metrics/legacyregistry"
3940
)
4041

42+
const (
43+
CertificateErrorAuditAnnotation = "authentication.k8s.io/certificate-error"
44+
)
45+
4146
/*
4247
* By default, the following metric is defined as falling under
4348
* ALPHA stability level https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/1209-metrics-stability/kubernetes-control-plane-metrics-stability.md#stability-classes)
@@ -134,6 +139,15 @@ func NewDynamic(verifyOptionsFn VerifyOptionFunc, user UserConversion) *Authenti
134139
return &Authenticator{verifyOptionsFn, user}
135140
}
136141

142+
func certificateErrorIdentifier(c *x509.Certificate) string {
143+
return fmt.Sprintf(
144+
"CN=%s, Issuer=%s, SN=%d",
145+
c.Subject.CommonName,
146+
c.Issuer.CommonName,
147+
c.SerialNumber,
148+
)[:512]
149+
}
150+
137151
// AuthenticateRequest authenticates the request using presented client certificates
138152
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
139153
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
@@ -184,6 +198,14 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.R
184198
clientCertificateExpirationHistogram.WithContext(req.Context()).Observe(remaining.Seconds())
185199
chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy)
186200
if err != nil {
201+
ctx := req.Context()
202+
audit.AddAuditAnnotation(ctx,
203+
CertificateErrorAuditAnnotation,
204+
fmt.Sprintf("certificate %s failed: %v", certificateErrorIdentifier(req.TLS.PeerCertificates[0]),
205+
err),
206+
)
207+
req = req.WithContext(ctx)
208+
187209
return nil, false, fmt.Errorf(
188210
"verifying certificate %s failed: %w",
189211
certificateIdentifier(req.TLS.PeerCertificates[0]),

test/integration/auth/certs_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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

Comments
 (0)