Skip to content

Commit aa46e55

Browse files
authored
Merge pull request #407 from Kuadrant/mtls-require-ext-client-auth
Check 'client auth' key usage on mtls identity
2 parents 1f45425 + a0b013d commit aa46e55

File tree

4 files changed

+90
-27
lines changed

4 files changed

+90
-27
lines changed

docs/features.md

+2
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ Trusted root Certificate Authorities (CA) are stored in Kubernetes Secrets label
203203

204204
Trusted root CA secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.authentication.x509.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)).
205205

206+
Client certificates must include x509 v3 extension specifying 'Client Authentication' extended key usage.
207+
206208
The identity object resolved out of a client x509 certificate is equal to the subject field of the certificate, and it serializes as JSON within the Authorization JSON usually as follows:
207209

208210
```jsonc

docs/user-guides/mtls-authentication.md

+34-8
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ kubectl apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/m
108108
Create a CA (Certificate Authority) certificate to issue the client certificates that will be used to authenticate clients that send requests to the Talker API:
109109

110110
```sh
111-
openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=talker-api-ca" -keyout /tmp/ca.key -out /tmp/ca.crt
111+
openssl req -x509 -sha256 -nodes \
112+
-days 365 \
113+
-newkey rsa:2048 \
114+
-subj "/CN=talker-api-ca" \
115+
-addext basicConstraints=CA:TRUE \
116+
-addext keyUsage=digitalSignature,keyCertSign \
117+
-keyout /tmp/ca.key \
118+
-out /tmp/ca.crt
112119
```
113120

114121
Store the CA cert in a Kubernetes `Secret`, labeled to be discovered by Authorino and to be mounted in the file system of the Envoy container:
@@ -118,6 +125,17 @@ kubectl create secret tls talker-api-ca --cert=/tmp/ca.crt --key=/tmp/ca.key
118125
kubectl label secret talker-api-ca authorino.kuadrant.io/managed-by=authorino app=talker-api
119126
```
120127

128+
Prepare an extension file for the client certificate signing requests:
129+
130+
```sh
131+
cat > /tmp/x509v3.ext << EOF
132+
authorityKeyIdentifier=keyid,issuer
133+
basicConstraints=CA:FALSE
134+
keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
135+
extendedKeyUsage=clientAuth
136+
EOF
137+
```
138+
121139
## ❺ Setup Envoy
122140

123141
The following command deploys the [Envoy](https://envoyproxy.io/) proxy and configuration to wire up the Talker API behind the reverse-proxy, with external authorization enabled with the Authorino instance.[^4]
@@ -361,8 +379,8 @@ With a TLS certificate signed by the trusted CA:
361379

362380
```sh
363381
openssl genrsa -out /tmp/aisha.key 2048
364-
openssl req -new -key /tmp/aisha.key -out /tmp/aisha.csr -subj "/CN=aisha/C=PK/L=Islamabad/O=ACME Inc./OU=Engineering"
365-
openssl x509 -req -in /tmp/aisha.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/aisha.crt -days 1 -sha256
382+
openssl req -new -subj "/CN=aisha/C=PK/L=Islamabad/O=ACME Inc./OU=Engineering" -key /tmp/aisha.key -out /tmp/aisha.csr
383+
openssl x509 -req -sha256 -days 1 -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -extfile /tmp/x509v3.ext -in /tmp/aisha.csr -out /tmp/aisha.crt
366384

367385
curl -k --cert /tmp/aisha.crt --key /tmp/aisha.key https://talker-api.127.0.0.1.nip.io:8000 -i
368386
# HTTP/1.1 200 OK
@@ -372,8 +390,8 @@ With a TLS certificate signed by the trusted CA, though missing an authorized Or
372390

373391
```sh
374392
openssl genrsa -out /tmp/john.key 2048
375-
openssl req -new -key /tmp/john.key -out /tmp/john.csr -subj "/CN=john/C=UK/L=London"
376-
openssl x509 -req -in /tmp/john.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/john.crt -days 1 -sha256
393+
openssl req -new -subj "/CN=john/C=UK/L=London" -key /tmp/john.key -out /tmp/john.csr
394+
openssl x509 -req -sha256 -days 1 -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -extfile /tmp/x509v3.ext -in /tmp/john.csr -out /tmp/john.crt
377395

378396
curl -k --cert /tmp/john.crt --key /tmp/john.key https://talker-api.127.0.0.1.nip.io:8000 -i
379397
# HTTP/1.1 403 Forbidden
@@ -398,10 +416,18 @@ curl -k --cert /tmp/aisha.crt --key /tmp/aisha.key -H 'Content-Type: application
398416
With a TLS certificate signed by an unknown authority:
399417

400418
```sh
401-
openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=untrusted" -keyout /tmp/untrusted-ca.key -out /tmp/untrusted-ca.crt
419+
openssl req -x509 -sha256 -nodes \
420+
-days 365 \
421+
-newkey rsa:2048 \
422+
-subj "/CN=untrusted" \
423+
-addext basicConstraints=CA:TRUE \
424+
-addext keyUsage=digitalSignature,keyCertSign \
425+
-keyout /tmp/untrusted-ca.key \
426+
-out /tmp/untrusted-ca.crt
427+
402428
openssl genrsa -out /tmp/niko.key 2048
403-
openssl req -new -key /tmp/niko.key -out /tmp/niko.csr -subj "/CN=niko/C=JP/L=Osaka"
404-
openssl x509 -req -in /tmp/niko.csr -CA /tmp/untrusted-ca.crt -CAkey /tmp/untrusted-ca.key -CAcreateserial -out /tmp/niko.crt -days 1 -sha256
429+
openssl req -new -subj "/CN=niko/C=JP/L=Osaka" -key /tmp/niko.key -out /tmp/niko.csr
430+
openssl x509 -req -sha256 -days 1 -CA /tmp/untrusted-ca.crt -CAkey /tmp/untrusted-ca.key -CAcreateserial -extfile /tmp/x509v3.ext -in /tmp/niko.csr -out /tmp/niko.crt
405431

406432
curl -k --cert /tmp/niko.crt --key /tmp/niko.key -H 'Content-Type: application/json' -d '{}' https://talker-api.127.0.0.1.nip.io:5001/check -i
407433
# HTTP/2 401

pkg/evaluators/identity/mtls.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{
9191
certs.AddCert(cert)
9292
}
9393

94-
if _, err := cert.Verify(x509.VerifyOptions{Roots: certs}); err != nil {
94+
if _, err := cert.Verify(x509.VerifyOptions{Roots: certs, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}); err != nil {
9595
return nil, err
9696
}
9797

pkg/evaluators/identity/mtls_test.go

+53-18
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func init() {
3838
// generate ca certs
3939
for _, name := range []string{"pets", "cars", "books"} {
4040
testCerts[name] = make(map[string][]byte)
41-
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(pkix.Name{CommonName: name}, nil, 1)
41+
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(pkix.Name{CommonName: name}, nil, 1, []x509.ExtKeyUsage{})
4242
}
4343

4444
// store the ca certs in k8s secrets
@@ -49,33 +49,44 @@ func init() {
4949

5050
// generate client certs
5151
for name, data := range map[string]struct {
52-
subject pkix.Name
53-
caName string
54-
days int
52+
subject pkix.Name
53+
caName string
54+
days int
55+
extKeyUsage []x509.ExtKeyUsage
5556
}{
5657
"john": {
57-
subject: pkix.Name{CommonName: "john", Country: []string{"UK"}, Locality: []string{"London"}},
58-
caName: "pets",
59-
days: 1,
58+
subject: pkix.Name{CommonName: "john", Country: []string{"UK"}, Locality: []string{"London"}},
59+
caName: "pets",
60+
days: 1,
61+
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
6062
},
6163
"bob": {
62-
subject: pkix.Name{CommonName: "bob", Country: []string{"US"}, Locality: []string{"Boston"}},
63-
caName: "pets",
64-
days: -1,
64+
subject: pkix.Name{CommonName: "bob", Country: []string{"US"}, Locality: []string{"Boston"}},
65+
caName: "pets",
66+
days: -1,
67+
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
6568
},
6669
"aisha": {
67-
subject: pkix.Name{CommonName: "aisha", Country: []string{"PK"}, Locality: []string{"Islamabad"}, Organization: []string{"ACME Inc."}, OrganizationalUnit: []string{"Engineering"}},
68-
caName: "cars",
69-
days: 1,
70+
subject: pkix.Name{CommonName: "aisha", Country: []string{"PK"}, Locality: []string{"Islamabad"}, Organization: []string{"ACME Inc."}, OrganizationalUnit: []string{"Engineering"}},
71+
caName: "cars",
72+
days: 1,
73+
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
7074
},
7175
"niko": {
72-
subject: pkix.Name{CommonName: "niko", Country: []string{"JP"}, Locality: []string{"Osaka"}},
73-
caName: "books",
74-
days: 1,
76+
subject: pkix.Name{CommonName: "niko", Country: []string{"JP"}, Locality: []string{"Osaka"}},
77+
caName: "books",
78+
days: 1,
79+
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
80+
},
81+
"tony": {
82+
subject: pkix.Name{CommonName: "tony", Country: []string{"IT"}, Locality: []string{"Rome"}},
83+
caName: "pets",
84+
days: 1,
85+
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
7586
},
7687
} {
7788
testCerts[name] = make(map[string][]byte)
78-
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(data.subject, testCerts[data.caName], data.days)
89+
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(data.subject, testCerts[data.caName], data.days, data.extKeyUsage)
7990
}
8091
}
8192

@@ -291,7 +302,28 @@ func TestCallExpiredClientCert(t *testing.T) {
291302
assert.ErrorContains(t, err, "certificate has expired or is not yet valid")
292303
}
293304

294-
func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte, []byte) {
305+
func TestExtendedKeyUsageMismatch(t *testing.T) {
306+
ctrl := gomock.NewController(t)
307+
defer ctrl.Finish()
308+
309+
selector, _ := k8s_labels.Parse("app=all")
310+
mtls := NewMTLSIdentity("mtls", selector, "ns1", testMTLSK8sClient, context.TODO())
311+
pipeline := mock_auth.NewMockAuthPipeline(ctrl)
312+
313+
// tony (ca: pets / extKeyUsage: server auth)
314+
pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{
315+
Attributes: &envoy_auth.AttributeContext{
316+
Source: &envoy_auth.AttributeContext_Peer{
317+
Certificate: url.QueryEscape(string(testCerts["tony"]["tls.crt"])),
318+
},
319+
},
320+
})
321+
obj, err := mtls.Call(pipeline, context.TODO())
322+
assert.Check(t, obj == nil)
323+
assert.ErrorContains(t, err, "certificate specifies an incompatible key usage")
324+
}
325+
326+
func issueCertificate(subject pkix.Name, ca map[string][]byte, days int, extKeyUsage []x509.ExtKeyUsage) ([]byte, []byte) {
295327
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
296328
isCA := ca == nil
297329
cert := &x509.Certificate{
@@ -300,6 +332,8 @@ func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte
300332
NotBefore: time.Now(),
301333
NotAfter: time.Now().AddDate(0, 0, days),
302334
IsCA: isCA,
335+
ExtKeyUsage: extKeyUsage,
336+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
303337
BasicConstraintsValid: isCA,
304338
}
305339
key, _ := rsa.GenerateKey(rand.Reader, 2048)
@@ -308,6 +342,7 @@ func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte
308342
if !isCA {
309343
parent = decodeCertificate(ca["tls.crt"])
310344
privKey = decodePrivateKey(ca["tls.key"])
345+
cert.KeyUsage = x509.KeyUsageDigitalSignature
311346
}
312347
certBytes, _ := x509.CreateCertificate(rand.Reader, cert, parent, &key.PublicKey, privKey)
313348
return encodeCertificate(certBytes), encodePrivateKey(key)

0 commit comments

Comments
 (0)