diff --git a/docs/features.md b/docs/features.md index 644deb74..0b7201e8 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 diff --git a/docs/user-guides/mtls-authentication.md b/docs/user-guides/mtls-authentication.md index c43b4879..a9bc86a6 100644 --- a/docs/user-guides/mtls-authentication.md +++ b/docs/user-guides/mtls-authentication.md @@ -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: @@ -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] @@ -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 @@ -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 @@ -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 diff --git a/pkg/evaluators/identity/mtls.go b/pkg/evaluators/identity/mtls.go index 7faa104d..503b5e07 100644 --- a/pkg/evaluators/identity/mtls.go +++ b/pkg/evaluators/identity/mtls.go @@ -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 } diff --git a/pkg/evaluators/identity/mtls_test.go b/pkg/evaluators/identity/mtls_test.go index 288d694b..62e3818c 100644 --- a/pkg/evaluators/identity/mtls_test.go +++ b/pkg/evaluators/identity/mtls_test.go @@ -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 @@ -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) } } @@ -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{ @@ -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) @@ -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)