This repository has been archived by the owner on Apr 27, 2021. It is now read-only.
forked from kubernetes/ingress-nginx
-
Notifications
You must be signed in to change notification settings - Fork 25
Add ssl certificate lua directive #60
Closed
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
5ac4880
[WIP] Add ssl certificate lua directive
hnrytrn ddb46c2
Extract certificate logic into seperate module
hnrytrn e5af39e
Add certificate unit test
hnrytrn a642d14
refactor certificate.lua
hnrytrn 2f94089
merge set cert and priv key functions
hnrytrn 496c42c
Use dynamic certificate enabled flag
hnrytrn 8e97f66
Check if cert from dictionary is an empty string
hnrytrn c1fbe1c
Update get pem certificate name
hnrytrn f9cebbc
Add dynamic certificates e2e tests
hnrytrn 6409091
Update unit tests
hnrytrn afd22c0
Ensure certificates are being dynamically served in e2e tests
hnrytrn e0763d5
Update tests
hnrytrn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
local ssl = require("ngx.ssl") | ||
local configuration = require("configuration") | ||
|
||
local _M = {} | ||
|
||
local function set_pem_cert_key(pem_cert_key) | ||
local der_cert, der_cert_err = ssl.cert_pem_to_der(pem_cert_key) | ||
if not der_cert then | ||
return "failed to convert certificate chain from PEM to DER: " .. der_cert_err | ||
end | ||
|
||
local set_cert_ok, set_cert_err = ssl.set_der_cert(der_cert) | ||
if not set_cert_ok then | ||
return "failed to set DER cert: " .. set_cert_err | ||
end | ||
|
||
local der_priv_key, dev_priv_key_err = ssl.priv_key_pem_to_der(pem_cert_key) | ||
if not der_priv_key then | ||
return "failed to convert private key from PEM to DER: " .. dev_priv_key_err | ||
end | ||
|
||
local set_priv_key_ok, set_priv_key_err = ssl.set_der_priv_key(der_priv_key) | ||
if not set_priv_key_ok then | ||
return "failed to set DER private key: " .. set_priv_key_err | ||
end | ||
end | ||
|
||
function _M.call() | ||
local hostname, hostname_err = ssl.server_name() | ||
|
||
if hostname_err then | ||
ngx.log(ngx.ERR, "Error getting the hostname: " .. hostname_err) | ||
return ngx.exit(ngx.ERROR) | ||
end | ||
|
||
local pem_cert_key = configuration.get_pem_cert_key(hostname) | ||
if not pem_cert_key then | ||
ngx.log(ngx.ERR, "Certificate not found for the given hostname: " .. hostname) | ||
return ngx.exit(ngx.ERROR) | ||
end | ||
|
||
local clear_ok, clear_err = ssl.clear_certs() | ||
if not clear_ok then | ||
ngx.log(ngx.ERR, "failed to clear existing (fallback) certificates: " .. clear_err) | ||
return ngx.exit(ngx.ERROR) | ||
end | ||
|
||
local set_pem_cert_key_err = set_pem_cert_key(pem_cert_key) | ||
if set_pem_cert_key_err then | ||
ngx.log(ngx.ERR, set_pem_cert_key_err) | ||
return ngx.exit(ngx.ERROR) | ||
end | ||
end | ||
|
||
return _M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
local certificate = require("certificate") | ||
|
||
local unmocked_ngx = _G.ngx | ||
|
||
describe("Certificate", function() | ||
describe("call", function() | ||
local ssl = require("ngx.ssl") | ||
local match = require("luassert.match") | ||
|
||
ssl.server_name = function() return "hostname", nil end | ||
ssl.clear_certs = function() return true, "" end | ||
ssl.set_der_cert = function(cert) return true, "" end | ||
ssl.set_der_priv_key = function(priv_key) return true, "" end | ||
|
||
before_each(function() | ||
ngx.exit = function(status) end | ||
end) | ||
|
||
after_each(function() | ||
ngx = unmocked_ngx | ||
end) | ||
|
||
it("does not clear fallback certificates and logs error message when host is not in dictionary", function() | ||
spy.on(ngx, "log") | ||
spy.on(ssl, "clear_certs") | ||
spy.on(ssl, "set_der_cert") | ||
spy.on(ssl, "set_der_priv_key") | ||
|
||
assert.has_no.errors(certificate.call) | ||
assert.spy(ngx.log).was_called_with(ngx.ERR, "Certificate not found for the given hostname: hostname") | ||
assert.spy(ssl.clear_certs).was_not_called() | ||
assert.spy(ssl.set_der_cert).was_not_called() | ||
assert.spy(ssl.set_der_priv_key).was_not_called() | ||
end) | ||
|
||
it("successfully sets SSL certificate and key when hostname is found in dictionary", function() | ||
local _ = match._ | ||
local pem_cert_key = "-----BEGIN CERTIFICATE-----\nMIID6DCCAlCgAwIBAgIQcfG0mA7BIFqhlnr/Zwh6TzANBgkqhkiG9w0BAQsFADBC\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExIDAeBgNVBAsMF2hlbnJ5\ndHJhbkBPVFQtSGVucnlUcmFuMB4XDTE4MDcwOTIwMTY1MloXDTI4MDcwOTIwMTY1\nMlowSzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMSAw\nHgYDVQQLDBdoZW5yeXRyYW5AT1RULUhlbnJ5VHJhbjCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBALIrsgHzjZZyKWPn3rGzTkaj9jADYAMhM+0wY3iky2Dx\ndr2YbKnZbbGxKLfVukYRsUUOK0SnBMTX15fsGanirj2hflMHfGvHilaVkVAkPJgD\nBTf2PkxFff99hS7/Ncz20MR6+E/vqp7Hx7IKDrg9lC9u1n82aotfN3gPhif8HyQu\n+P9cltsr9PewyPe4573WQmzXhTKaFm9+U9xZ2qS1J0DMEizRs45vuM040hxtiwVz\nM4Lm8DVpaYxMBWNI/zo9EZzoSJZH1sYUpTMwhNj+caEX+LK9PCM4Sht/yhPUc6aD\nnIEqraz+bS8dNFH5Ehp7n1SZL7YH6xT6da4F3ci7jEECAwEAAaNRME8wDgYDVR0P\nAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwGgYD\nVR0RBBMwEYIPbXltaW5pa3ViZS5pbmZvMA0GCSqGSIb3DQEBCwUAA4IBgQC/TEpx\nJkL/ek37L2XKwGq96hNT10IZ9yE+RlndNGNY6eAc8y313sXBHTFbDCWQ2s0pWZZS\n+va20dnTQNyWzJAxFpdNvcKOCUGat4RPD/j+pBTEYk5n/oo7s2FWkG8kW6tKDilf\njWHpk7m9uYCO2sOZFiQPR81idR5PLox46SpJmIhDVfCi6VS4N+8fAT8Tbt9xkPmS\nmODhpnuIUt0NVTi62eqnxeO185qAt73xhz9Gj1KHntAK1ebcx0k3UxKRXQp9WY76\nF39sSz8OuhEhvv9ayl6uS6ZdSLvvb6kJrRRneKr01ridCOtiYB7cuXykDL1c6PUk\nugxDgTyCjiuPnRl0CLwxWT659PVozA2SO1YCW6UcoGdj2KMvsXezeWKpNGx3NHXO\nufdlxSbzWlamn+sPunWP3v2tfV0J8sHG3n1roeBO2N52197/ennGuCZfnF8C5MoG\n9YfMjKg9Z03G8sDpk9g5bHp9p28TO1X+Ht30PQzkUNhx3fjTO2DDvCyGk2k=\n-----END CERTIFICATE-----\n\n-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCyK7IB842Wcilj\n596xs05Go/YwA2ADITPtMGN4pMtg8Xa9mGyp2W2xsSi31bpGEbFFDitEpwTE19eX\n7Bmp4q49oX5TB3xrx4pWlZFQJDyYAwU39j5MRX3/fYUu/zXM9tDEevhP76qex8ey\nCg64PZQvbtZ/NmqLXzd4D4Yn/B8kLvj/XJbbK/T3sMj3uOe91kJs14UymhZvflPc\nWdqktSdAzBIs0bOOb7jNONIcbYsFczOC5vA1aWmMTAVjSP86PRGc6EiWR9bGFKUz\nMITY/nGhF/iyvTwjOEobf8oT1HOmg5yBKq2s/m0vHTRR+RIae59UmS+2B+sU+nWu\nBd3Iu4xBAgMBAAECggEBAIbuUIDp0fB9xJrEnwI0qLMWuPrjk3LLUmfunWZgZyWj\nuCkdpi17XHeVkyCl28v02itR77KuSg5I6B1F0Km34f0KsIBwyulU1I999e6bgsgc\ngXdAJS3d8u3qQVK2NChlQvWJq0PeXXiiE7nhpAQjnnXNmuP8cfPayEdEenUNmwfq\nxjEh2/oDzUTPD/4z5Hpw8n728SItgBolMNgGvmv5cC4JNLqujCUPwkLiZ3a2YbTY\nrmOO1xDkZnQWqyNP+baOwYwpu/kISPM3IveP5GGBNQsUsDxs6t80HNW/w8Ry0f50\n+gNTIuJVOLXfpVLIo87wTMEtRAqMzT4vxQIi+vj2XYECgYEA6QftSRKqur1G4P6Y\n9cseDnljJFWIjqex2q3NrvMaHbnlXp6AtPROoNz6L8H+PnBy8o1yZJwWnhKTvPaD\nsi+a1g7dqQIM4TjKLlidV57lt5ENw4ueW1o7Jbk75gawLhrBPSCFxR5xSqF+kQxn\nmWGjLnZoomD6fM2CG7EF1fg+wjsCgYEAw7t6db9tjGGM9J0rUe+jVtxMWiG9ArT0\nhmaLZQlKrOSFeEf8c4ZYBNxp/X+/jg0GWBX8P4KRubAz6bbn0A+07K9TClrvMFWq\nveqnK1JUsMGWsYQPp8dX8VS/jOFzYdiji9Ekyzs9RiXW8jzp0wzrccSjEr0de8HK\niEa9CH7cZ7MCgYB1V5mD51NzbyZG281YT+yVq0hiHnQCKa1kiYp+I0ouV9KJP9Vd\nyXvigwO0ksIc3PD09Ib65KJ6/K3KRHPygQg97ARwO2kS7E7a4aJxYcEZG4DLy/10\n0M3h5BGmdg23WZ+e0UarCPZRd1rNXWq5kLHkDpoH0j+wIqf2m8Bti3DGywKBgBEK\nn6zkz9rrG2So0n69yJDleVhXm6dCrg+NmhFf77qB4wUH73j3d25k6m2B0+HATI8a\nyu2Upq9uIfb1T9WTqIL6+NXr+OtSah1C8u8YqfsBv+cQwnQvLP78C/luH6ejPwoL\nWZLAQ6N54+8PUqRneZBcOH6HLKv7wXCACDFXKkV1AoGAfDb5GJ0NsovWhLfU5WEB\nSfdzHBplbp72q08S0aqTNm0wlTiCGYgm2Lle4IaOGoJ+7ipirL0KzuwisAZJFTvJ\nhsMqOmH/Ckledmf2JpLxyg8KB5KVA+RVQkrfVEv8yhqLcKQU6Z2n4jSTon7hXb1T\nf8neDpZ8DwO0W9cOdYLYyTg=\n-----END PRIVATE KEY-----" | ||
ngx.shared.certificate_data:set("hostname", pem_cert_key) | ||
|
||
spy.on(ngx, "log") | ||
spy.on(ssl, "set_der_cert") | ||
spy.on(ssl, "set_der_priv_key") | ||
|
||
assert.has_no.errors(certificate.call) | ||
assert.spy(ngx.log).was_not_called_with(ngx.ERR, _) | ||
assert.spy(ssl.set_der_cert).was_called_with(ssl.cert_pem_to_der(pem_cert_key)) | ||
assert.spy(ssl.set_der_priv_key).was_called_with(ssl.priv_key_pem_to_der(pem_cert_key)) | ||
end) | ||
|
||
it("logs error message when certificate in dictionary is invalid", function() | ||
ngx.shared.certificate_data:set("hostname", "") | ||
|
||
spy.on(ngx, "log") | ||
spy.on(ssl, "set_der_cert") | ||
spy.on(ssl, "set_der_priv_key") | ||
|
||
assert.has_no.errors(certificate.call) | ||
assert.spy(ngx.log).was_called_with(ngx.ERR, "failed to convert certificate chain from PEM to DER: PEM_read_bio_X509_AUX() failed") | ||
assert.spy(ssl.set_der_cert).was_not_called() | ||
assert.spy(ssl.set_der_priv_key).was_not_called() | ||
end) | ||
|
||
it("does not clear fallback certificates and logs error message when hostname could not be fetched", function() | ||
ssl.server_name = function() return nil, "error" end | ||
|
||
spy.on(ngx, "log") | ||
spy.on(ssl, "clear_certs") | ||
spy.on(ssl, "set_der_cert") | ||
spy.on(ssl, "set_der_priv_key") | ||
|
||
assert.has_no.errors(certificate.call) | ||
assert.spy(ngx.log).was_called_with(ngx.ERR, "Error getting the hostname: error") | ||
assert.spy(ssl.clear_certs).was_not_called() | ||
assert.spy(ssl.set_der_cert).was_not_called() | ||
assert.spy(ssl.set_der_priv_key).was_not_called() | ||
end) | ||
end) | ||
end) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
/* | ||
Copyright 2018 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package lua | ||
|
||
import ( | ||
"crypto/tls" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
"github.com/parnurzeal/gorequest" | ||
|
||
appsv1beta1 "k8s.io/api/apps/v1beta1" | ||
extensions "k8s.io/api/extensions/v1beta1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
|
||
"k8s.io/ingress-nginx/test/e2e/framework" | ||
) | ||
|
||
var _ = framework.IngressNginxDescribe("Dynamic Certificate", func() { | ||
f := framework.NewDefaultFramework("dynamic-certificate") | ||
|
||
BeforeEach(func() { | ||
err := enableDynamicCertificates(f.IngressController.Namespace, f.KubeClientSet) | ||
Expect(err).NotTo(HaveOccurred()) | ||
|
||
err = f.NewEchoDeploymentWithReplicas(1) | ||
Expect(err).NotTo(HaveOccurred()) | ||
|
||
host := "foo.com" | ||
ing, err := ensureIngress(f, host) | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(ing).NotTo(BeNil()) | ||
|
||
time.Sleep(waitForLuaSync) | ||
|
||
resp, _, errs := gorequest.New(). | ||
Get(f.IngressController.HTTPURL). | ||
Set("Host", host). | ||
End() | ||
Expect(len(errs)).Should(BeNumerically("==", 0)) | ||
Expect(resp.StatusCode).Should(Equal(http.StatusOK)) | ||
|
||
Expect(err).ToNot(HaveOccurred()) | ||
}) | ||
|
||
Context("when only servers change", func() { | ||
It("should handle SSL certificate only changes", func() { | ||
ingress, err := f.KubeClientSet.ExtensionsV1beta1().Ingresses(f.IngressController.Namespace).Get("foo.com", metav1.GetOptions{}) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
ingress.Spec.TLS = []extensions.IngressTLS{ | ||
{ | ||
Hosts: []string{"foo.com"}, | ||
SecretName: "foo.com", | ||
}, | ||
} | ||
|
||
_, err = framework.CreateIngressTLSSecret(f.KubeClientSet, | ||
ingress.Spec.TLS[0].Hosts, | ||
ingress.Spec.TLS[0].SecretName, | ||
ingress.Namespace) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
resp, _, errs := gorequest.New(). | ||
Get(fmt.Sprintf("%s?id=certificate_only_changes", f.IngressController.HTTPURL)). | ||
Set("Host", "foo.com"). | ||
End() | ||
Expect(len(errs)).Should(BeNumerically("==", 0)) | ||
Expect(resp.StatusCode).Should(Equal(http.StatusOK)) | ||
|
||
_, err = f.KubeClientSet.ExtensionsV1beta1().Ingresses(f.IngressController.Namespace).Update(ingress) | ||
Expect(err).ToNot(HaveOccurred()) | ||
time.Sleep(waitForLuaSync) | ||
|
||
log, err := f.NginxLogs() | ||
Expect(err).ToNot(HaveOccurred()) | ||
Expect(log).ToNot(BeEmpty()) | ||
|
||
index := strings.Index(log, "id=certificate_only_changes") | ||
restOfLogs := log[index:] | ||
|
||
By("POSTing new certificates to Lua endpoint") | ||
Expect(restOfLogs).To(ContainSubstring(logDynamicConfigSuccess)) | ||
Expect(restOfLogs).ToNot(ContainSubstring(logDynamicConfigFailure)) | ||
|
||
By("skipping Nginx reload") | ||
Expect(restOfLogs).ToNot(ContainSubstring(logRequireBackendReload)) | ||
Expect(restOfLogs).ToNot(ContainSubstring(logBackendReloadSuccess)) | ||
Expect(restOfLogs).To(ContainSubstring(logSkipBackendReload)) | ||
Expect(restOfLogs).ToNot(ContainSubstring(logInitialConfigSync)) | ||
}) | ||
}) | ||
|
||
Context("when certificates are requested", func() { | ||
It("should serve certificates dynamically from Lua", func() { | ||
ingress, err := f.KubeClientSet.ExtensionsV1beta1().Ingresses(f.IngressController.Namespace).Get("foo.com", metav1.GetOptions{}) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
ingress.Spec.TLS = []extensions.IngressTLS{ | ||
{ | ||
Hosts: []string{"foo.com"}, | ||
SecretName: "foo.com", | ||
}, | ||
} | ||
|
||
_, err = framework.CreateIngressTLSSecret(f.KubeClientSet, | ||
ingress.Spec.TLS[0].Hosts, | ||
ingress.Spec.TLS[0].SecretName, | ||
ingress.Namespace) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
_, err = f.KubeClientSet.ExtensionsV1beta1().Ingresses(f.IngressController.Namespace).Update(ingress) | ||
Expect(err).ToNot(HaveOccurred()) | ||
time.Sleep(waitForLuaSync) | ||
|
||
By("checking SSL Certificate using the NGINX IP address") | ||
resp, _, errs := gorequest.New(). | ||
Get(f.IngressController.HTTPSURL). | ||
Set("Host", ingress.Spec.TLS[0].Hosts[0]). | ||
TLSClientConfig(&tls.Config{ | ||
InsecureSkipVerify: true, | ||
}). | ||
End() | ||
|
||
Expect(len(errs)).Should(BeNumerically("==", 0)) | ||
Expect(len(resp.TLS.PeerCertificates)).Should(BeNumerically("==", 1)) | ||
Expect(resp.TLS.PeerCertificates[0].DNSNames[0]).Should(Equal("foo.com")) | ||
|
||
By("checking that the controller isn't configuring the server's certificate in the configuration file") | ||
err = f.WaitForNginxServer(ingress.Spec.TLS[0].Hosts[0], | ||
func(server string) bool { | ||
return strings.Contains(server, "ssl_certificate /etc/ingress-controller/ssl/default-fake-certificate.pem;") && | ||
strings.Contains(server, "ssl_certificate_key /etc/ingress-controller/ssl/default-fake-certificate.pem;") | ||
}) | ||
Expect(err).NotTo(HaveOccurred()) | ||
}) | ||
}) | ||
}) | ||
|
||
func enableDynamicCertificates(namespace string, kubeClientSet kubernetes.Interface) error { | ||
return framework.UpdateDeployment(kubeClientSet, namespace, "nginx-ingress-controller", 1, | ||
func(deployment *appsv1beta1.Deployment) error { | ||
args := deployment.Spec.Template.Spec.Containers[0].Args | ||
args = append(args, "--enable-dynamic-configuration") | ||
args = append(args, "--enable-dynamic-certificates") | ||
args = append(args, "--enable-ssl-chain-completion=false") | ||
deployment.Spec.Template.Spec.Containers[0].Args = args | ||
_, err := kubeClientSet.AppsV1beta1().Deployments(namespace).Update(deployment) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
}) | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is this coming from? you have to declare it first, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is coming from the
dynamic_configuration.go
file, which is in the same package as this file.ingress/test/e2e/lua/dynamic_configuration.go
Line 44 in afd22c0