Skip to content

Commit

Permalink
Merge pull request #407 from Kuadrant/mtls-require-ext-client-auth
Browse files Browse the repository at this point in the history
Check 'client auth' key usage on mtls identity
  • Loading branch information
guicassolato authored Feb 21, 2024
2 parents 1f45425 + a0b013d commit aa46e55
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 aa46e55

Please sign in to comment.