Skip to content

Commit

Permalink
Check 'client auth' key usage on mtls identity
Browse files Browse the repository at this point in the history
Make the mTLS identity verification method to check for 'client auth' extended key usage, instead of default 'server auth'.
  • Loading branch information
guicassolato committed Feb 20, 2024
1 parent 744220a commit a0b013d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 27 deletions.
2 changes: 2 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Trusted root Certificate Authorities (CA) are stored in Kubernetes Secrets label

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)).

Client certificates must include x509 v3 extension specifying 'Client Authentication' extended key usage.

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:

```jsonc
Expand Down
42 changes: 34 additions & 8 deletions docs/user-guides/mtls-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,14 @@ kubectl apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/m
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:

```sh
openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=talker-api-ca" -keyout /tmp/ca.key -out /tmp/ca.crt
openssl req -x509 -sha256 -nodes \
-days 365 \
-newkey rsa:2048 \
-subj "/CN=talker-api-ca" \
-addext basicConstraints=CA:TRUE \
-addext keyUsage=digitalSignature,keyCertSign \
-keyout /tmp/ca.key \
-out /tmp/ca.crt
```

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:
Expand All @@ -118,6 +125,17 @@ kubectl create secret tls talker-api-ca --cert=/tmp/ca.crt --key=/tmp/ca.key
kubectl label secret talker-api-ca authorino.kuadrant.io/managed-by=authorino app=talker-api
```

Prepare an extension file for the client certificate signing requests:

```sh
cat > /tmp/x509v3.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
extendedKeyUsage=clientAuth
EOF
```

## ❺ Setup Envoy

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]
Expand Down Expand Up @@ -361,8 +379,8 @@ With a TLS certificate signed by the trusted CA:

```sh
openssl genrsa -out /tmp/aisha.key 2048
openssl req -new -key /tmp/aisha.key -out /tmp/aisha.csr -subj "/CN=aisha/C=PK/L=Islamabad/O=ACME Inc./OU=Engineering"
openssl x509 -req -in /tmp/aisha.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/aisha.crt -days 1 -sha256
openssl req -new -subj "/CN=aisha/C=PK/L=Islamabad/O=ACME Inc./OU=Engineering" -key /tmp/aisha.key -out /tmp/aisha.csr
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

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

```sh
openssl genrsa -out /tmp/john.key 2048
openssl req -new -key /tmp/john.key -out /tmp/john.csr -subj "/CN=john/C=UK/L=London"
openssl x509 -req -in /tmp/john.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/john.crt -days 1 -sha256
openssl req -new -subj "/CN=john/C=UK/L=London" -key /tmp/john.key -out /tmp/john.csr
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

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

```sh
openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=untrusted" -keyout /tmp/untrusted-ca.key -out /tmp/untrusted-ca.crt
openssl req -x509 -sha256 -nodes \
-days 365 \
-newkey rsa:2048 \
-subj "/CN=untrusted" \
-addext basicConstraints=CA:TRUE \
-addext keyUsage=digitalSignature,keyCertSign \
-keyout /tmp/untrusted-ca.key \
-out /tmp/untrusted-ca.crt

openssl genrsa -out /tmp/niko.key 2048
openssl req -new -key /tmp/niko.key -out /tmp/niko.csr -subj "/CN=niko/C=JP/L=Osaka"
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
openssl req -new -subj "/CN=niko/C=JP/L=Osaka" -key /tmp/niko.key -out /tmp/niko.csr
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

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
# HTTP/2 401
Expand Down
2 changes: 1 addition & 1 deletion pkg/evaluators/identity/mtls.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{
certs.AddCert(cert)
}

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

Expand Down
71 changes: 53 additions & 18 deletions pkg/evaluators/identity/mtls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func init() {
// generate ca certs
for _, name := range []string{"pets", "cars", "books"} {
testCerts[name] = make(map[string][]byte)
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(pkix.Name{CommonName: name}, nil, 1)
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(pkix.Name{CommonName: name}, nil, 1, []x509.ExtKeyUsage{})
}

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

// generate client certs
for name, data := range map[string]struct {
subject pkix.Name
caName string
days int
subject pkix.Name
caName string
days int
extKeyUsage []x509.ExtKeyUsage
}{
"john": {
subject: pkix.Name{CommonName: "john", Country: []string{"UK"}, Locality: []string{"London"}},
caName: "pets",
days: 1,
subject: pkix.Name{CommonName: "john", Country: []string{"UK"}, Locality: []string{"London"}},
caName: "pets",
days: 1,
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
},
"bob": {
subject: pkix.Name{CommonName: "bob", Country: []string{"US"}, Locality: []string{"Boston"}},
caName: "pets",
days: -1,
subject: pkix.Name{CommonName: "bob", Country: []string{"US"}, Locality: []string{"Boston"}},
caName: "pets",
days: -1,
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
},
"aisha": {
subject: pkix.Name{CommonName: "aisha", Country: []string{"PK"}, Locality: []string{"Islamabad"}, Organization: []string{"ACME Inc."}, OrganizationalUnit: []string{"Engineering"}},
caName: "cars",
days: 1,
subject: pkix.Name{CommonName: "aisha", Country: []string{"PK"}, Locality: []string{"Islamabad"}, Organization: []string{"ACME Inc."}, OrganizationalUnit: []string{"Engineering"}},
caName: "cars",
days: 1,
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
},
"niko": {
subject: pkix.Name{CommonName: "niko", Country: []string{"JP"}, Locality: []string{"Osaka"}},
caName: "books",
days: 1,
subject: pkix.Name{CommonName: "niko", Country: []string{"JP"}, Locality: []string{"Osaka"}},
caName: "books",
days: 1,
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
},
"tony": {
subject: pkix.Name{CommonName: "tony", Country: []string{"IT"}, Locality: []string{"Rome"}},
caName: "pets",
days: 1,
extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
},
} {
testCerts[name] = make(map[string][]byte)
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(data.subject, testCerts[data.caName], data.days)
testCerts[name]["tls.crt"], testCerts[name]["tls.key"] = issueCertificate(data.subject, testCerts[data.caName], data.days, data.extKeyUsage)
}
}

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

func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte, []byte) {
func TestExtendedKeyUsageMismatch(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

selector, _ := k8s_labels.Parse("app=all")
mtls := NewMTLSIdentity("mtls", selector, "ns1", testMTLSK8sClient, context.TODO())
pipeline := mock_auth.NewMockAuthPipeline(ctrl)

// tony (ca: pets / extKeyUsage: server auth)
pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{
Attributes: &envoy_auth.AttributeContext{
Source: &envoy_auth.AttributeContext_Peer{
Certificate: url.QueryEscape(string(testCerts["tony"]["tls.crt"])),
},
},
})
obj, err := mtls.Call(pipeline, context.TODO())
assert.Check(t, obj == nil)
assert.ErrorContains(t, err, "certificate specifies an incompatible key usage")
}

func issueCertificate(subject pkix.Name, ca map[string][]byte, days int, extKeyUsage []x509.ExtKeyUsage) ([]byte, []byte) {
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
isCA := ca == nil
cert := &x509.Certificate{
Expand All @@ -300,6 +332,8 @@ func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, days),
IsCA: isCA,
ExtKeyUsage: extKeyUsage,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: isCA,
}
key, _ := rsa.GenerateKey(rand.Reader, 2048)
Expand All @@ -308,6 +342,7 @@ func issueCertificate(subject pkix.Name, ca map[string][]byte, days int) ([]byte
if !isCA {
parent = decodeCertificate(ca["tls.crt"])
privKey = decodePrivateKey(ca["tls.key"])
cert.KeyUsage = x509.KeyUsageDigitalSignature
}
certBytes, _ := x509.CreateCertificate(rand.Reader, cert, parent, &key.PublicKey, privKey)
return encodeCertificate(certBytes), encodePrivateKey(key)
Expand Down

0 comments on commit a0b013d

Please sign in to comment.