diff --git a/docs/examples/auth/client-certs/README.md b/docs/examples/auth/client-certs/README.md index 17b8bd690..a60aa14e7 100644 --- a/docs/examples/auth/client-certs/README.md +++ b/docs/examples/auth/client-certs/README.md @@ -44,6 +44,12 @@ Authentication to work properly. kubectl create secret generic ca-secret --from-file=tls.crt=server.crt --from-file=tls.key=server.key --from-file=ca.crt=ca.crt ``` +3. If you want to also enable Certificate Revocation List verification you can + create the secret also containing the CRL file in PEM format: + ```bash + kubectl create secret generic ca-secret --from-file=ca.crt=ca.crt --from-file=ca.crl=ca.crl + ``` + Note: The CA Certificate must contain the trusted certificate authority chain to verify client certificates. ## Setup Instructions diff --git a/internal/ingress/controller/store/backend_ssl.go b/internal/ingress/controller/store/backend_ssl.go index af6113e03..1ee1f521c 100644 --- a/internal/ingress/controller/store/backend_ssl.go +++ b/internal/ingress/controller/store/backend_ssl.go @@ -84,6 +84,8 @@ func (s *k8sStore) getPemCertificate(secretName string) (*ingress.SSLCert, error key, okkey := secret.Data[apiv1.TLSPrivateKeyKey] ca := secret.Data["ca.crt"] + crl := secret.Data["ca.crl"] + auth := secret.Data["auth"] // namespace/secretName -> namespace-secretName @@ -117,12 +119,25 @@ func (s *k8sStore) getPemCertificate(secretName string) (*ingress.SSLCert, error if err != nil { return nil, fmt.Errorf("error configuring CA certificate: %v", err) } + + if len(crl) > 0 { + err = ssl.ConfigureCRL(nsSecName, crl, sslCert) + if err != nil { + return nil, fmt.Errorf("error configuring CRL certificate: %v", err) + } + + } } msg := fmt.Sprintf("Configuring Secret %q for TLS encryption (CN: %v)", secretName, sslCert.CN) if ca != nil { msg += " and authentication" } + + if crl != nil { + msg += " and CRL" + } + klog.V(3).Info(msg) } else if len(ca) > 0 { sslCert, err = ssl.CreateCACert(ca) @@ -135,6 +150,12 @@ func (s *k8sStore) getPemCertificate(secretName string) (*ingress.SSLCert, error return nil, fmt.Errorf("error configuring CA certificate: %v", err) } + if len(crl) > 0 { + err = ssl.ConfigureCRL(nsSecName, crl, sslCert) + if err != nil { + return nil, err + } + } // makes this secret in 'syncSecret' to be used for Certificate Authentication // this does not enable Certificate Authentication klog.V(3).Infof("Configuring Secret %q for TLS authentication", secretName) diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go index 77e3c40b4..1bf447b80 100644 --- a/internal/ingress/controller/store/store.go +++ b/internal/ingress/controller/store/store.go @@ -816,9 +816,11 @@ func (s *k8sStore) GetAuthCertificate(name string) (*resolver.AuthSSLCert, error } return &resolver.AuthSSLCert{ - Secret: name, - CAFileName: cert.CAFileName, - CASHA: cert.CASHA, + Secret: name, + CAFileName: cert.CAFileName, + CASHA: cert.CASHA, + CRLFileName: cert.CRLFileName, + CRLSHA: cert.CRLSHA, }, nil } diff --git a/internal/ingress/resolver/main.go b/internal/ingress/resolver/main.go index db8da4af1..4581143a5 100644 --- a/internal/ingress/resolver/main.go +++ b/internal/ingress/resolver/main.go @@ -32,12 +32,11 @@ type Resolver interface { // GetSecret searches for secrets containing the namespace and name using a the character / GetSecret(string) (*apiv1.Secret, error) - // GetAuthCertificate resolves a given secret name into an SSL certificate. - // The secret must contain 3 keys named: + // GetAuthCertificate resolves a given secret name into an SSL certificate and CRL. + // The secret must contain 2 keys named: // ca.crt: contains the certificate chain used for authentication - // tls.crt: contains the server certificate - // tls.key: contains the server key + // ca.crl: contains the revocation list used for authentication GetAuthCertificate(string) (*AuthSSLCert, error) // GetService searches for services containing the namespace and name using a the character / @@ -53,6 +52,10 @@ type AuthSSLCert struct { CAFileName string `json:"caFilename"` // CASHA contains the SHA1 hash of the 'ca.crt' or combinations of (tls.crt, tls.key, tls.crt) depending on certs in secret CASHA string `json:"caSha"` + // CRLFileName contains the path to the secrets 'ca.crl' + CRLFileName string `json:"crlFileName"` + // CRLSHA contains the SHA1 hash of the 'ca.crl' file + CRLSHA string `json:"crlSha"` } // Equal tests for equality between two AuthSSLCert types @@ -74,5 +77,12 @@ func (asslc1 *AuthSSLCert) Equal(assl2 *AuthSSLCert) bool { return false } + if asslc1.CRLFileName != assl2.CRLFileName { + return false + } + if asslc1.CRLSHA != assl2.CRLSHA { + return false + } + return true } diff --git a/internal/ingress/sslcert.go b/internal/ingress/sslcert.go index 12527d2aa..c59fdb93e 100644 --- a/internal/ingress/sslcert.go +++ b/internal/ingress/sslcert.go @@ -37,6 +37,11 @@ type SSLCert struct { // This is used to detect changes in the secret that contains certificates CASHA string `json:"caSha"` + // CRLFileName contains the path to the file with the Certificate Revocation List + CRLFileName string `json:"crlFileName"` + // CRLSHA contains the sha1 of the pem file. + CRLSHA string `json:"crlSha"` + // PemFileName contains the path to the file with the certificate and key concatenated PemFileName string `json:"pemFileName"` diff --git a/internal/net/ssl/ssl.go b/internal/net/ssl/ssl.go index da66ad7a6..d1d3002ff 100644 --- a/internal/net/ssl/ssl.go +++ b/internal/net/ssl/ssl.go @@ -211,6 +211,38 @@ func ConfigureCACertWithCertAndKey(name string, ca []byte, sslCert *ingress.SSLC return ioutil.WriteFile(sslCert.CAFileName, buffer.Bytes(), 0644) } +// ConfigureCRL creates a CRL file and append it into the SSLCert +func ConfigureCRL(name string, crl []byte, sslCert *ingress.SSLCert) error { + + crlName := fmt.Sprintf("crl-%v.pem", name) + crlFileName := fmt.Sprintf("%v/%v", file.DefaultSSLDirectory, crlName) + + pemCRLBlock, _ := pem.Decode(crl) + if pemCRLBlock == nil { + return fmt.Errorf("no valid PEM formatted block found in CRL %v", name) + } + // If the first certificate does not start with 'X509 CRL' it's invalid and must not be used. + if pemCRLBlock.Type != "X509 CRL" { + return fmt.Errorf("CRL file %v contains invalid data, and must be created only with PEM formatted certificates", name) + } + + _, err := x509.ParseCRL(pemCRLBlock.Bytes) + if err != nil { + return fmt.Errorf(err.Error()) + } + + err = ioutil.WriteFile(crlFileName, crl, 0644) + if err != nil { + return fmt.Errorf("could not write CRL file %v: %v", crlFileName, err) + } + + sslCert.CRLFileName = crlFileName + sslCert.CRLSHA = file.SHA1(crlFileName) + + return nil + +} + // ConfigureCACert is similar to ConfigureCACertWithCertAndKey but it creates a separate file // for CA cert and writes only ca into it and then sets relevant fields in sslCert func ConfigureCACert(name string, ca []byte, sslCert *ingress.SSLCert) error { diff --git a/internal/net/ssl/ssl_test.go b/internal/net/ssl/ssl_test.go index 1fd1656f3..eb4434e42 100644 --- a/internal/net/ssl/ssl_test.go +++ b/internal/net/ssl/ssl_test.go @@ -39,6 +39,7 @@ import ( "time" certutil "k8s.io/client-go/util/cert" + "k8s.io/ingress-nginx/internal/file" ) // generateRSACerts generates a self signed certificate using a self generated ca @@ -183,11 +184,60 @@ func TestConfigureCACert(t *testing.T) { if err != nil { t.Fatalf("unexpected error creating SSL certificate: %v", err) } - if sslCert.CAFileName == "" { + + caFilename := fmt.Sprintf("%v/ca-%v.pem", file.DefaultSSLDirectory, cn) + + if sslCert.CAFileName != caFilename { t.Fatalf("expected a valid CA file name") } } +func TestConfigureCRL(t *testing.T) { + // Demo CRL from https://csrc.nist.gov/projects/pki-testing/sample-certificates-and-crls + // Converted to PEM to be tested + // SHA: ef21f9c97ec2ef84ba3b2ab007c858a6f760d813 + var crl = []byte(`-----BEGIN X509 CRL----- +MIIBYDCBygIBATANBgkqhkiG9w0BAQUFADBDMRMwEQYKCZImiZPyLGQBGRYDY29t +MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTETMBEGA1UEAxMKRXhhbXBsZSBDQRcN +MDUwMjA1MTIwMDAwWhcNMDUwMjA2MTIwMDAwWjAiMCACARIXDTA0MTExOTE1NTcw +M1owDDAKBgNVHRUEAwoBAaAvMC0wHwYDVR0jBBgwFoAUCGivhTPIOUp6+IKTjnBq +SiCELDIwCgYDVR0UBAMCAQwwDQYJKoZIhvcNAQEFBQADgYEAItwYffcIzsx10NBq +m60Q9HYjtIFutW2+DvsVFGzIF20f7pAXom9g5L2qjFXejoRvkvifEBInr0rUL4Xi +NkR9qqNMJTgV/wD9Pn7uPSYS69jnK2LiK8NGgO94gtEVxtCccmrLznrtZ5mLbnCB +fUNCdMGmr8FVF6IzTNYGmCuk/C4= +-----END X509 CRL-----`) + + cn := "demo-crl" + _, ca, err := generateRSACerts(cn) + if err != nil { + t.Fatalf("unexpected error creating SSL certificate: %v", err) + } + c := encodeCertPEM(ca.Cert) + + sslCert, err := CreateCACert(c) + if err != nil { + t.Fatalf("unexpected error creating SSL certificate: %v", err) + } + if sslCert.CRLFileName != "" { + t.Fatalf("expected CRLFileName to be empty") + } + if sslCert.Certificate == nil { + t.Fatalf("expected Certificate to be set") + } + + err = ConfigureCRL(cn, crl, sslCert) + if err != nil { + t.Fatalf("unexpected error creating CRL file: %v", err) + } + + crlFilename := fmt.Sprintf("%v/crl-%v.pem", file.DefaultSSLDirectory, cn) + if sslCert.CRLFileName != crlFilename { + t.Fatalf("expected a valid CRL file name") + } + if sslCert.CRLSHA != "ef21f9c97ec2ef84ba3b2ab007c858a6f760d813" { + t.Fatalf("the expected CRL SHA wasn't found") + } +} func TestCreateSSLCert(t *testing.T) { cert, _, err := generateRSACerts("echoheaders") if err != nil { diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 079d7a16d..86e64d36c 100755 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -811,6 +811,12 @@ stream { ssl_client_certificate {{ $server.CertificateAuth.CAFileName }}; ssl_verify_client {{ $server.CertificateAuth.VerifyClient }}; ssl_verify_depth {{ $server.CertificateAuth.ValidationDepth }}; + + {{ if not (empty $server.CertificateAuth.CRLFileName) }} + # PEM sha: {{ $server.CertificateAuth.CRLSHA }} + ssl_crl {{ $server.CertificateAuth.CRLFileName }}; + {{ end }} + {{ if not (empty $server.CertificateAuth.ErrorPage)}} error_page 495 496 = {{ $server.CertificateAuth.ErrorPage }}; {{ end }}