Merge pull request #2923 from Shopify/dynamic-certs-controller
Add dynamic certificate serving feature to controller
This commit is contained in:
commit
ab1072e2db
12 changed files with 342 additions and 29 deletions
|
@ -147,6 +147,10 @@ Requires the update-status parameter.`)
|
||||||
`Dynamically refresh backends on topology changes instead of reloading NGINX.
|
`Dynamically refresh backends on topology changes instead of reloading NGINX.
|
||||||
Feature backed by OpenResty Lua libraries.`)
|
Feature backed by OpenResty Lua libraries.`)
|
||||||
|
|
||||||
|
dynamicCertificatesEnabled = flags.Bool("enable-dynamic-certificates", false,
|
||||||
|
`Dynamically update SSL certificates instead of reloading NGINX.
|
||||||
|
Feature backed by OpenResty Lua libraries. Requires that OCSP stapling is not enabled`)
|
||||||
|
|
||||||
httpPort = flags.Int("http-port", 80, `Port to use for servicing HTTP traffic.`)
|
httpPort = flags.Int("http-port", 80, `Port to use for servicing HTTP traffic.`)
|
||||||
httpsPort = flags.Int("https-port", 443, `Port to use for servicing HTTPS traffic.`)
|
httpsPort = flags.Int("https-port", 443, `Port to use for servicing HTTPS traffic.`)
|
||||||
statusPort = flags.Int("status-port", 18080, `Port to use for exposing NGINX status pages.`)
|
statusPort = flags.Int("status-port", 18080, `Port to use for exposing NGINX status pages.`)
|
||||||
|
@ -213,6 +217,11 @@ Feature backed by OpenResty Lua libraries.`)
|
||||||
glog.Warningf("SSL certificate chain completion is disabled (--enable-ssl-chain-completion=false)")
|
glog.Warningf("SSL certificate chain completion is disabled (--enable-ssl-chain-completion=false)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*enableSSLChainCompletion || !*dynamicConfigurationEnabled) && *dynamicCertificatesEnabled {
|
||||||
|
return false, nil, fmt.Errorf(`SSL certificate chain completion cannot be enabled and dynamic configration cannot be disabled when
|
||||||
|
dynamic certificates functionality is enabled. Please check the flags --enable-ssl-chain-completion and --enable-dynamic-configuration`)
|
||||||
|
}
|
||||||
|
|
||||||
// LuaJIT is not available on arch s390x and ppc64le
|
// LuaJIT is not available on arch s390x and ppc64le
|
||||||
disableLua := false
|
disableLua := false
|
||||||
if runtime.GOARCH == "s390x" || runtime.GOARCH == "ppc64le" {
|
if runtime.GOARCH == "s390x" || runtime.GOARCH == "ppc64le" {
|
||||||
|
@ -248,6 +257,7 @@ Feature backed by OpenResty Lua libraries.`)
|
||||||
SyncRateLimit: *syncRateLimit,
|
SyncRateLimit: *syncRateLimit,
|
||||||
DynamicConfigurationEnabled: *dynamicConfigurationEnabled,
|
DynamicConfigurationEnabled: *dynamicConfigurationEnabled,
|
||||||
DisableLua: disableLua,
|
DisableLua: disableLua,
|
||||||
|
DynamicCertificatesEnabled: *dynamicCertificatesEnabled,
|
||||||
ListenPorts: &ngx_config.ListenPorts{
|
ListenPorts: &ngx_config.ListenPorts{
|
||||||
Default: *defServerPort,
|
Default: *defServerPort,
|
||||||
Health: *healthzPort,
|
Health: *healthzPort,
|
||||||
|
|
|
@ -685,6 +685,7 @@ type TemplateConfig struct {
|
||||||
ListenPorts *ListenPorts
|
ListenPorts *ListenPorts
|
||||||
PublishService *apiv1.Service
|
PublishService *apiv1.Service
|
||||||
DynamicConfigurationEnabled bool
|
DynamicConfigurationEnabled bool
|
||||||
|
DynamicCertificatesEnabled bool
|
||||||
DisableLua bool
|
DisableLua bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,8 @@ type Configuration struct {
|
||||||
DynamicConfigurationEnabled bool
|
DynamicConfigurationEnabled bool
|
||||||
|
|
||||||
DisableLua bool
|
DisableLua bool
|
||||||
|
|
||||||
|
DynamicCertificatesEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublishService returns the Service used to set the load-balancer status of Ingresses.
|
// GetPublishService returns the Service used to set the load-balancer status of Ingresses.
|
||||||
|
@ -197,7 +199,7 @@ func (n *NGINXController) syncIngress(interface{}) error {
|
||||||
// it takes time for NGINX to start listening on the configured ports
|
// it takes time for NGINX to start listening on the configured ports
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
err := configureDynamically(pcfg, n.cfg.ListenPorts.Status)
|
err := configureDynamically(pcfg, n.cfg.ListenPorts.Status, n.cfg.DynamicCertificatesEnabled)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
glog.Infof("Dynamic reconfiguration succeeded.")
|
glog.Infof("Dynamic reconfiguration succeeded.")
|
||||||
} else {
|
} else {
|
||||||
|
@ -1071,6 +1073,12 @@ func (n *NGINXController) createServers(data []*extensions.Ingress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n.cfg.DynamicCertificatesEnabled {
|
||||||
|
// useless placeholders: just to shut up NGINX configuration loader errors:
|
||||||
|
cert.PemFileName = defaultPemFileName
|
||||||
|
cert.PemSHA = defaultPemSHA
|
||||||
|
}
|
||||||
|
|
||||||
servers[host].SSLCert = *cert
|
servers[host].SSLCert = *cert
|
||||||
|
|
||||||
if cert.ExpireTime.Before(time.Now().Add(240 * time.Hour)) {
|
if cert.ExpireTime.Before(time.Now().Add(240 * time.Hour)) {
|
||||||
|
|
|
@ -118,7 +118,8 @@ func NewNGINXController(config *Configuration, mc metric.Collector, fs file.File
|
||||||
config.ResyncPeriod,
|
config.ResyncPeriod,
|
||||||
config.Client,
|
config.Client,
|
||||||
fs,
|
fs,
|
||||||
n.updateCh)
|
n.updateCh,
|
||||||
|
config.DynamicCertificatesEnabled)
|
||||||
|
|
||||||
n.syncQueue = task.NewTaskQueue(n.syncIngress)
|
n.syncQueue = task.NewTaskQueue(n.syncIngress)
|
||||||
|
|
||||||
|
@ -592,6 +593,7 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
|
||||||
ListenPorts: n.cfg.ListenPorts,
|
ListenPorts: n.cfg.ListenPorts,
|
||||||
PublishService: n.GetPublishService(),
|
PublishService: n.GetPublishService(),
|
||||||
DynamicConfigurationEnabled: n.cfg.DynamicConfigurationEnabled,
|
DynamicConfigurationEnabled: n.cfg.DynamicConfigurationEnabled,
|
||||||
|
DynamicCertificatesEnabled: n.cfg.DynamicCertificatesEnabled,
|
||||||
DisableLua: n.cfg.DisableLua,
|
DisableLua: n.cfg.DisableLua,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,6 +725,18 @@ func (n *NGINXController) setupSSLProxy() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to clear Certificates from the ingress configuration since they should be ignored when
|
||||||
|
// checking if the new configuration changes can be applied dynamically if dynamic certificates is on
|
||||||
|
func clearCertificates(config *ingress.Configuration) {
|
||||||
|
var clearedServers []*ingress.Server
|
||||||
|
for _, server := range config.Servers {
|
||||||
|
copyOfServer := *server
|
||||||
|
copyOfServer.SSLCert = ingress.SSLCert{}
|
||||||
|
clearedServers = append(clearedServers, ©OfServer)
|
||||||
|
}
|
||||||
|
config.Servers = clearedServers
|
||||||
|
}
|
||||||
|
|
||||||
// IsDynamicConfigurationEnough returns whether a Configuration can be
|
// IsDynamicConfigurationEnough returns whether a Configuration can be
|
||||||
// dynamically applied, without reloading the backend.
|
// dynamically applied, without reloading the backend.
|
||||||
func (n *NGINXController) IsDynamicConfigurationEnough(pcfg *ingress.Configuration) bool {
|
func (n *NGINXController) IsDynamicConfigurationEnough(pcfg *ingress.Configuration) bool {
|
||||||
|
@ -732,12 +746,17 @@ func (n *NGINXController) IsDynamicConfigurationEnough(pcfg *ingress.Configurati
|
||||||
copyOfRunningConfig.Backends = []*ingress.Backend{}
|
copyOfRunningConfig.Backends = []*ingress.Backend{}
|
||||||
copyOfPcfg.Backends = []*ingress.Backend{}
|
copyOfPcfg.Backends = []*ingress.Backend{}
|
||||||
|
|
||||||
|
if n.cfg.DynamicCertificatesEnabled {
|
||||||
|
clearCertificates(©OfRunningConfig)
|
||||||
|
clearCertificates(©OfPcfg)
|
||||||
|
}
|
||||||
|
|
||||||
return copyOfRunningConfig.Equal(©OfPcfg)
|
return copyOfRunningConfig.Equal(©OfPcfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureDynamically encodes new Backends in JSON format and POSTs the
|
// configureDynamically encodes new Backends in JSON format and POSTs the
|
||||||
// payload to an internal HTTP endpoint handled by Lua.
|
// payload to an internal HTTP endpoint handled by Lua.
|
||||||
func configureDynamically(pcfg *ingress.Configuration, port int) error {
|
func configureDynamically(pcfg *ingress.Configuration, port int, isDynamicCertificatesEnabled bool) error {
|
||||||
backends := make([]*ingress.Backend, len(pcfg.Backends))
|
backends := make([]*ingress.Backend, len(pcfg.Backends))
|
||||||
|
|
||||||
for i, backend := range pcfg.Backends {
|
for i, backend := range pcfg.Backends {
|
||||||
|
@ -770,14 +789,53 @@ func configureDynamically(pcfg *ingress.Configuration, port int) error {
|
||||||
backends[i] = luaBackend
|
backends[i] = luaBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := json.Marshal(backends)
|
url := fmt.Sprintf("http://localhost:%d/configuration/backends", port)
|
||||||
|
err := post(url, backends)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Infof("Posting backends configuration: %s", buf)
|
if isDynamicCertificatesEnabled {
|
||||||
|
err = configureCertificates(pcfg, port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureCertificates JSON encodes certificates and POSTs it to an internal HTTP endpoint
|
||||||
|
// that is handled by Lua
|
||||||
|
func configureCertificates(pcfg *ingress.Configuration, port int) error {
|
||||||
|
var servers []*ingress.Server
|
||||||
|
|
||||||
|
for _, server := range pcfg.Servers {
|
||||||
|
servers = append(servers, &ingress.Server{
|
||||||
|
Hostname: server.Hostname,
|
||||||
|
SSLCert: ingress.SSLCert{
|
||||||
|
PemCertKey: server.SSLCert.PemCertKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://localhost:%d/configuration/servers", port)
|
||||||
|
err := post(url, servers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func post(url string, data interface{}) error {
|
||||||
|
buf, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.V(2).Infof("Posting to %s: %s", url, buf)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://localhost:%d/configuration/backends", port)
|
|
||||||
resp, err := http.Post(url, "application/json", bytes.NewReader(buf))
|
resp, err := http.Post(url, "application/json", bytes.NewReader(buf))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
@ -52,6 +53,9 @@ func TestIsDynamicConfigurationEnough(t *testing.T) {
|
||||||
Backend: "fakenamespace-myapp-80",
|
Backend: "fakenamespace-myapp-80",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SSLCert: ingress.SSLCert{
|
||||||
|
PemCertKey: "fake-certificate",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
commonConfig := &ingress.Configuration{
|
commonConfig := &ingress.Configuration{
|
||||||
|
@ -64,6 +68,9 @@ func TestIsDynamicConfigurationEnough(t *testing.T) {
|
||||||
Backends: backends,
|
Backends: backends,
|
||||||
Servers: servers,
|
Servers: servers,
|
||||||
},
|
},
|
||||||
|
cfg: &Configuration{
|
||||||
|
DynamicCertificatesEnabled: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := commonConfig
|
newConfig := commonConfig
|
||||||
|
@ -87,11 +94,53 @@ func TestIsDynamicConfigurationEnough(t *testing.T) {
|
||||||
t.Errorf("Expected to be dynamically configurable when only backends change")
|
t.Errorf("Expected to be dynamically configurable when only backends change")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.cfg.DynamicCertificatesEnabled = true
|
||||||
|
|
||||||
|
newServers := []*ingress.Server{{
|
||||||
|
Hostname: "myapp1.fake",
|
||||||
|
Locations: []*ingress.Location{
|
||||||
|
{
|
||||||
|
Path: "/",
|
||||||
|
Backend: "fakenamespace-myapp-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SSLCert: ingress.SSLCert{
|
||||||
|
PemCertKey: "fake-certificate",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
newConfig = &ingress.Configuration{
|
||||||
|
Backends: backends,
|
||||||
|
Servers: newServers,
|
||||||
|
}
|
||||||
|
if n.IsDynamicConfigurationEnough(newConfig) {
|
||||||
|
t.Errorf("Expected to not be dynamically configurable when dynamic certificates is enabled and a non-certificate field in servers is updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
newServers[0].Hostname = "myapp.fake"
|
||||||
|
newServers[0].SSLCert.PemCertKey = "new-fake-certificate"
|
||||||
|
|
||||||
|
newConfig = &ingress.Configuration{
|
||||||
|
Backends: backends,
|
||||||
|
Servers: newServers,
|
||||||
|
}
|
||||||
|
if !n.IsDynamicConfigurationEnough(newConfig) {
|
||||||
|
t.Errorf("Expected to be dynamically configurable when only SSLCert changes")
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig = &ingress.Configuration{
|
||||||
|
Backends: []*ingress.Backend{{Name: "a-backend-8080"}},
|
||||||
|
Servers: newServers,
|
||||||
|
}
|
||||||
|
if !n.IsDynamicConfigurationEnough(newConfig) {
|
||||||
|
t.Errorf("Expected to be dynamically configurable when backend and SSLCert changes")
|
||||||
|
}
|
||||||
|
|
||||||
if !n.runningConfig.Equal(commonConfig) {
|
if !n.runningConfig.Equal(commonConfig) {
|
||||||
t.Errorf("Expected running config to not change")
|
t.Errorf("Expected running config to not change")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !newConfig.Equal(&ingress.Configuration{Backends: []*ingress.Backend{{Name: "a-backend-8080"}}, Servers: servers}) {
|
if !newConfig.Equal(&ingress.Configuration{Backends: []*ingress.Backend{{Name: "a-backend-8080"}}, Servers: newServers}) {
|
||||||
t.Errorf("Expected new config to not change")
|
t.Errorf("Expected new config to not change")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +206,7 @@ func TestConfigureDynamically(t *testing.T) {
|
||||||
port := ts.Listener.Addr().(*net.TCPAddr).Port
|
port := ts.Listener.Addr().(*net.TCPAddr).Port
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
err := configureDynamically(commonConfig, port)
|
err := configureDynamically(commonConfig, port, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error posting dynamic configuration: %v", err)
|
t.Errorf("unexpected error posting dynamic configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -167,6 +216,56 @@ func TestConfigureDynamically(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigureCertificates(t *testing.T) {
|
||||||
|
|
||||||
|
servers := []*ingress.Server{{
|
||||||
|
Hostname: "myapp.fake",
|
||||||
|
SSLCert: ingress.SSLCert{
|
||||||
|
PemCertKey: "fake-cert",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
commonConfig := &ingress.Configuration{
|
||||||
|
Servers: servers,
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
||||||
|
if r.Method != "POST" {
|
||||||
|
t.Errorf("expected a 'POST' request, got '%s'", r.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var postedServers []ingress.Server
|
||||||
|
err = json.Unmarshal(b, &postedServers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(servers) != len(postedServers) {
|
||||||
|
t.Errorf("Expected servers to be the same length as the posted servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, server := range servers {
|
||||||
|
if !server.Equal(&postedServers[i]) {
|
||||||
|
t.Errorf("Expected servers and posted servers to be equal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
port := ts.Listener.Addr().(*net.TCPAddr).Port
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
err := configureCertificates(commonConfig, port)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error posting dynamic certificate configuration: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNginxHashBucketSize(t *testing.T) {
|
func TestNginxHashBucketSize(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
n int
|
n int
|
||||||
|
|
|
@ -98,11 +98,18 @@ func (s k8sStore) getPemCertificate(secretName string) (*ingress.SSLCert, error)
|
||||||
return nil, fmt.Errorf("key 'tls.key' missing from Secret %q", secretName)
|
return nil, fmt.Errorf("key 'tls.key' missing from Secret %q", secretName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.isDynamicCertificatesEnabled {
|
||||||
|
sslCert, err = ssl.CreateSSLCert(nsSecName, cert, key, ca)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected error creating SSL Cert: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// If 'ca.crt' is also present, it will allow this secret to be used in the
|
// If 'ca.crt' is also present, it will allow this secret to be used in the
|
||||||
// 'nginx.ingress.kubernetes.io/auth-tls-secret' annotation
|
// 'nginx.ingress.kubernetes.io/auth-tls-secret' annotation
|
||||||
sslCert, err = ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca, s.filesystem)
|
sslCert, err = ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca, s.filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("unexpected error creating pem file: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("Configuring Secret %q for TLS encryption (CN: %v)", secretName, sslCert.CN)
|
msg := fmt.Sprintf("Configuring Secret %q for TLS encryption (CN: %v)", secretName, sslCert.CN)
|
||||||
|
|
|
@ -212,6 +212,8 @@ type k8sStore struct {
|
||||||
mu *sync.Mutex
|
mu *sync.Mutex
|
||||||
|
|
||||||
defaultSSLCertificate string
|
defaultSSLCertificate string
|
||||||
|
|
||||||
|
isDynamicCertificatesEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new object store to be used in the ingress controller
|
// New creates a new object store to be used in the ingress controller
|
||||||
|
@ -220,7 +222,8 @@ func New(checkOCSP bool,
|
||||||
resyncPeriod time.Duration,
|
resyncPeriod time.Duration,
|
||||||
client clientset.Interface,
|
client clientset.Interface,
|
||||||
fs file.Filesystem,
|
fs file.Filesystem,
|
||||||
updateCh *channels.RingChannel) Storer {
|
updateCh *channels.RingChannel,
|
||||||
|
isDynamicCertificatesEnabled bool) Storer {
|
||||||
|
|
||||||
store := &k8sStore{
|
store := &k8sStore{
|
||||||
isOCSPCheckEnabled: checkOCSP,
|
isOCSPCheckEnabled: checkOCSP,
|
||||||
|
@ -233,6 +236,7 @@ func New(checkOCSP bool,
|
||||||
mu: &sync.Mutex{},
|
mu: &sync.Mutex{},
|
||||||
secretIngressMap: NewObjectRefMap(),
|
secretIngressMap: NewObjectRefMap(),
|
||||||
defaultSSLCertificate: defaultSSLCertificate,
|
defaultSSLCertificate: defaultSSLCertificate,
|
||||||
|
isDynamicCertificatesEnabled: isDynamicCertificatesEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBroadcaster := record.NewBroadcaster()
|
eventBroadcaster := record.NewBroadcaster()
|
||||||
|
|
|
@ -68,7 +68,8 @@ func TestStore(t *testing.T) {
|
||||||
10*time.Minute,
|
10*time.Minute,
|
||||||
clientSet,
|
clientSet,
|
||||||
fs,
|
fs,
|
||||||
updateCh)
|
updateCh,
|
||||||
|
false)
|
||||||
|
|
||||||
storer.Run(stopCh)
|
storer.Run(stopCh)
|
||||||
|
|
||||||
|
@ -155,7 +156,8 @@ func TestStore(t *testing.T) {
|
||||||
10*time.Minute,
|
10*time.Minute,
|
||||||
clientSet,
|
clientSet,
|
||||||
fs,
|
fs,
|
||||||
updateCh)
|
updateCh,
|
||||||
|
false)
|
||||||
|
|
||||||
storer.Run(stopCh)
|
storer.Run(stopCh)
|
||||||
|
|
||||||
|
@ -302,7 +304,8 @@ func TestStore(t *testing.T) {
|
||||||
10*time.Minute,
|
10*time.Minute,
|
||||||
clientSet,
|
clientSet,
|
||||||
fs,
|
fs,
|
||||||
updateCh)
|
updateCh,
|
||||||
|
false)
|
||||||
|
|
||||||
storer.Run(stopCh)
|
storer.Run(stopCh)
|
||||||
|
|
||||||
|
@ -390,7 +393,8 @@ func TestStore(t *testing.T) {
|
||||||
10*time.Minute,
|
10*time.Minute,
|
||||||
clientSet,
|
clientSet,
|
||||||
fs,
|
fs,
|
||||||
updateCh)
|
updateCh,
|
||||||
|
false)
|
||||||
|
|
||||||
storer.Run(stopCh)
|
storer.Run(stopCh)
|
||||||
|
|
||||||
|
@ -501,7 +505,8 @@ func TestStore(t *testing.T) {
|
||||||
10*time.Minute,
|
10*time.Minute,
|
||||||
clientSet,
|
clientSet,
|
||||||
fs,
|
fs,
|
||||||
updateCh)
|
updateCh,
|
||||||
|
false)
|
||||||
|
|
||||||
storer.Run(stopCh)
|
storer.Run(stopCh)
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ type SSLCert struct {
|
||||||
CN []string `json:"cn"`
|
CN []string `json:"cn"`
|
||||||
// ExpiresTime contains the expiration of this SSL certificate in timestamp format
|
// ExpiresTime contains the expiration of this SSL certificate in timestamp format
|
||||||
ExpireTime time.Time `json:"expires"`
|
ExpireTime time.Time `json:"expires"`
|
||||||
|
// Pem encoded certificate and key concatenated
|
||||||
|
PemCertKey string `json:"pemCertKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectKind implements the ObjectKind interface as a noop
|
// GetObjectKind implements the ObjectKind interface as a noop
|
||||||
|
|
|
@ -505,6 +505,9 @@ func (s1 *SSLCert) Equal(s2 *SSLCert) bool {
|
||||||
if s1.FullChainPemFileName != s2.FullChainPemFileName {
|
if s1.FullChainPemFileName != s2.FullChainPemFileName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if s1.PemCertKey != s2.PemCertKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for _, cn1 := range s1.CN {
|
for _, cn1 := range s1.CN {
|
||||||
found := false
|
found := false
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package ssl
|
package ssl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -187,6 +188,86 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte,
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSSLCert creates an SSLCert and avoids writing on disk
|
||||||
|
func CreateSSLCert(name string, cert, key, ca []byte) (*ingress.SSLCert, error) {
|
||||||
|
var pemCertBuffer bytes.Buffer
|
||||||
|
|
||||||
|
pemCertBuffer.Write(cert)
|
||||||
|
pemCertBuffer.Write([]byte("\n"))
|
||||||
|
pemCertBuffer.Write(key)
|
||||||
|
|
||||||
|
pemBlock, _ := pem.Decode(pemCertBuffer.Bytes())
|
||||||
|
if pemBlock == nil {
|
||||||
|
return nil, fmt.Errorf("no valid PEM formatted block found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file does not start with 'BEGIN CERTIFICATE' it's invalid and must not be used.
|
||||||
|
if pemBlock.Type != "CERTIFICATE" {
|
||||||
|
return nil, fmt.Errorf("certificate %v contains invalid data, and must be created with 'kubectl create secret tls'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemCert, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ensure that certificate and private key have a matching public key
|
||||||
|
if _, err := tls.X509KeyPair(cert, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cn := sets.NewString(pemCert.Subject.CommonName)
|
||||||
|
for _, dns := range pemCert.DNSNames {
|
||||||
|
if !cn.Has(dns) {
|
||||||
|
cn.Insert(dns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pemCert.Extensions) > 0 {
|
||||||
|
glog.V(3).Info("parsing ssl certificate extensions")
|
||||||
|
for _, ext := range getExtension(pemCert, oidExtensionSubjectAltName) {
|
||||||
|
dns, _, _, err := parseSANExtension(ext.Value)
|
||||||
|
if err != nil {
|
||||||
|
glog.Warningf("unexpected error parsing certificate extensions: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dns := range dns {
|
||||||
|
if !cn.Has(dns) {
|
||||||
|
cn.Insert(dns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ca) > 0 {
|
||||||
|
bundle := x509.NewCertPool()
|
||||||
|
bundle.AppendCertsFromPEM(ca)
|
||||||
|
opts := x509.VerifyOptions{
|
||||||
|
Roots: bundle,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := pemCert.Verify(opts)
|
||||||
|
if err != nil {
|
||||||
|
oe := fmt.Sprintf("failed to verify certificate chain: \n\t%s\n", err)
|
||||||
|
return nil, errors.New(oe)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemCertBuffer.Write([]byte("\n"))
|
||||||
|
pemCertBuffer.Write(ca)
|
||||||
|
pemCertBuffer.Write([]byte("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &ingress.SSLCert{
|
||||||
|
Certificate: pemCert,
|
||||||
|
CN: cn.List(),
|
||||||
|
ExpireTime: pemCert.NotAfter,
|
||||||
|
PemCertKey: pemCertBuffer.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getExtension(c *x509.Certificate, id asn1.ObjectIdentifier) []pkix.Extension {
|
func getExtension(c *x509.Certificate, id asn1.ObjectIdentifier) []pkix.Extension {
|
||||||
var exts []pkix.Extension
|
var exts []pkix.Extension
|
||||||
for _, ext := range c.Extensions {
|
for _, ext := range c.Extensions {
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package ssl
|
package ssl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -147,3 +148,37 @@ func newFS(t *testing.T) file.Filesystem {
|
||||||
}
|
}
|
||||||
return fs
|
return fs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateSSLCert(t *testing.T) {
|
||||||
|
cert, _, err := generateRSACerts("echoheaders")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating SSL certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("test-%v", time.Now().UnixNano())
|
||||||
|
|
||||||
|
c := certutil.EncodeCertPEM(cert.Cert)
|
||||||
|
k := certutil.EncodePrivateKeyPEM(cert.Key)
|
||||||
|
|
||||||
|
ngxCert, err := CreateSSLCert(name, c, k, []byte{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error checking SSL certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var certKeyBuf bytes.Buffer
|
||||||
|
certKeyBuf.Write(c)
|
||||||
|
certKeyBuf.Write([]byte("\n"))
|
||||||
|
certKeyBuf.Write(k)
|
||||||
|
|
||||||
|
if ngxCert.PemCertKey != certKeyBuf.String() {
|
||||||
|
t.Fatalf("expected concatenated PEM cert and key but returned %v", ngxCert.PemCertKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ngxCert.CN) == 0 {
|
||||||
|
t.Fatalf("expected at least one cname but none returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ngxCert.CN[0] != "echoheaders" {
|
||||||
|
t.Fatalf("expected cname echoheaders but %v returned", ngxCert.CN[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue