Remove custom ssl code and add TLS support in Ingress rules

This commit is contained in:
Manuel de Brito Fontes 2016-03-16 11:12:45 -03:00
parent 5feb452ce4
commit 6cb0e41737
11 changed files with 190 additions and 226 deletions

View file

@ -243,8 +243,6 @@ func (lbc *loadBalancerController) sync() {
} }
func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]nginx.Upstream, []nginx.Server) { func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]nginx.Upstream, []nginx.Server) {
pems := make(map[string]string)
upstreams := make(map[string]nginx.Upstream) upstreams := make(map[string]nginx.Upstream)
servers := make(map[string]nginx.Server) servers := make(map[string]nginx.Server)
@ -297,6 +295,8 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
} }
} }
pems := lbc.getPemsFromIngress(data)
for _, rule := range ing.Spec.Rules { for _, rule := range ing.Spec.Rules {
var server nginx.Server var server nginx.Server
if existent, ok := servers[rule.Host]; ok { if existent, ok := servers[rule.Host]; ok {
@ -317,6 +317,18 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
loc := nginx.Location{Path: path.Path} loc := nginx.Location{Path: path.Path}
upsName := ing.GetNamespace() + "-" + path.Backend.ServiceName upsName := ing.GetNamespace() + "-" + path.Backend.ServiceName
svcKey := ing.GetNamespace() + "/" + path.Backend.ServiceName
_, svcExists, err := lbc.svcLister.Store.GetByKey(svcKey)
if err != nil {
glog.Infof("error getting service %v from the cache: %v", svcKey, err)
continue
}
if !svcExists {
glog.Warningf("service %v does no exists. skipping Ingress rule", svcKey)
continue
}
for _, ups := range upstreams { for _, ups := range upstreams {
if upsName == ups.Name { if upsName == ups.Name {
loc.Upstream = ups loc.Upstream = ups
@ -350,6 +362,41 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
return aUpstreams, aServers return aUpstreams, aServers
} }
func (lbc *loadBalancerController) getPemsFromIngress(data []interface{}) map[string]string {
pems := make(map[string]string)
for _, ingIf := range data {
ing := ingIf.(*extensions.Ingress)
for _, tls := range ing.Spec.TLS {
secretName := tls.SecretName
secret, err := lbc.client.Secrets(ing.Namespace).Get(secretName)
if err != nil {
glog.Warningf("Error retriveing secret %v for ing %v: %v", secretName, ing.Name, err)
continue
}
cert, ok := secret.Data[api.TLSCertKey]
if !ok {
glog.Warningf("Secret %v has no private key", secretName)
continue
}
key, ok := secret.Data[api.TLSPrivateKeyKey]
if !ok {
glog.Warningf("Secret %v has no cert", secretName)
continue
}
pemFileName := lbc.nginx.AddOrUpdateCertAndKey(secretName, string(cert), string(key))
for _, host := range tls.Hosts {
pems[host] = pemFileName
}
}
}
return pems
}
// getEndpoints returns a list of <endpoint ip>:<port> for a given service/target port combination. // getEndpoints returns a list of <endpoint ip>:<port> for a given service/target port combination.
func (lbc *loadBalancerController) getEndpoints(s *api.Service, servicePort intstr.IntOrString) []nginx.UpstreamServer { func (lbc *loadBalancerController) getEndpoints(s *api.Service, servicePort intstr.IntOrString) []nginx.UpstreamServer {
ep, err := lbc.endpLister.GetServiceEndpoints(s) ep, err := lbc.endpLister.GetServiceEndpoints(s)
@ -415,9 +462,7 @@ func (lbc *loadBalancerController) Run() {
go lbc.svcController.Run(lbc.stopCh) go lbc.svcController.Run(lbc.stopCh)
// periodic check for changes in configuration // periodic check for changes in configuration
go wait.Until(lbc.sync, 5*time.Second, wait.NeverStop) go wait.Until(lbc.sync, 10*time.Second, wait.NeverStop)
time.Sleep(5 * time.Second)
<-lbc.stopCh <-lbc.stopCh
glog.Infof("shutting down NGINX loadbalancer controller") glog.Infof("shutting down NGINX loadbalancer controller")

View file

@ -5,7 +5,10 @@ function openURL(status, page)
local res, err = httpc:request_uri(page, { local res, err = httpc:request_uri(page, {
path = "/", path = "/",
method = "GET" method = "GET",
headers = {
["Content-Type"] = ngx.var.httpReturnType or "text/html",
}
}) })
if not res then if not res then

View file

@ -56,6 +56,7 @@ server {
content_by_lua ' content_by_lua '
-- For simple singleshot requests, use the URI interface. -- For simple singleshot requests, use the URI interface.
local http = require "resty.http"
local httpc = http.new() local httpc = http.new()
local res, err = httpc:request_uri("http://example.com/helloworld", { local res, err = httpc:request_uri("http://example.com/helloworld", {
method = "POST", method = "POST",

View file

@ -67,7 +67,7 @@ end
local _M = { local _M = {
_VERSION = '0.06', _VERSION = '0.07',
} }
_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version _M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version
@ -196,7 +196,7 @@ function _M.parse_uri(self, uri)
m[3] = 80 m[3] = 80
end end
end end
if not m[4] then m[4] = "/" end if not m[4] or "" == m[4] then m[4] = "/" end
return m, nil return m, nil
end end
end end
@ -805,7 +805,11 @@ function _M.proxy_response(self, response, chunksize)
end end
if chunk then if chunk then
ngx.print(chunk) local res, err = ngx.print(chunk)
if not res then
ngx_log(ngx_ERR, err)
break
end
end end
until not chunk until not chunk
end end

View file

@ -1,8 +1,8 @@
package = "lua-resty-http" package = "lua-resty-http"
version = "0.06-0" version = "0.07-0"
source = { source = {
url = "git://github.com/pintsized/lua-resty-http", url = "git://github.com/pintsized/lua-resty-http",
tag = "v0.06" tag = "v0.07"
} }
description = { description = {
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.", summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
@ -22,7 +22,7 @@ description = {
maintainer = "James Hurst <james@pintsized.co.uk>" maintainer = "James Hurst <james@pintsized.co.uk>"
} }
dependencies = { dependencies = {
"lua >= 5.1", "lua >= 5.1"
} }
build = { build = {
type = "builtin", type = "builtin",

View file

@ -0,0 +1,52 @@
# vim:set ft= ts=4 sw=4 et:
use Test::Nginx::Socket;
use Cwd qw(cwd);
plan tests => repeat_each() * (blocks() * 3);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
error_log logs/error.log debug;
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
no_long_string();
#no_diff();
run_tests();
__DATA__
=== TEST 1: request_uri (check the default path)
--- http_config eval: $::HttpConfig
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://127.0.0.1:"..ngx.var.server_port)
if res and 200 == res.status then
ngx.say("OK")
else
ngx.say("FAIL")
end
';
}
location =/ {
content_by_lua '
ngx.print("OK")
';
}
--- request
GET /lua
--- response_body
OK
--- no_error_log
[error]

View file

@ -1,4 +1,4 @@
{{ $cfg := .cfg }}{{ $sslCertificates := .sslCertificates }}{{ $defErrorSvc := .defErrorSvc }}{{ $defBackend := .defBackend }} {{ $cfg := .cfg }}{{ $defErrorSvc := .defErrorSvc }}{{ $defBackend := .defBackend }}
daemon off; daemon off;
worker_processes {{ $cfg.WorkerProcesses }}; worker_processes {{ $cfg.WorkerProcesses }};
@ -17,13 +17,13 @@ http {
lua_package_path '.?.lua;./etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/lua-resty-http/lib/?.lua;'; lua_package_path '.?.lua;./etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/lua-resty-http/lib/?.lua;';
init_by_lua_block { init_by_lua_block {
def_backend = "http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }}"
{{ if $defErrorSvc }}{{/* only if exists a custom error service */}} {{ if $defErrorSvc }}{{/* only if exists a custom error service */}}
dev_error_url = "http://{{ $defErrorSvc.ServiceName }}.{{ $defErrorSvc.Namespace }}.svc.cluster.local:{{ $defErrorSvc.ServicePort }}" dev_error_url = "http://{{ $defErrorSvc.ServiceName }}.{{ $defErrorSvc.Namespace }}.svc.cluster.local:{{ $defErrorSvc.ServicePort }}"
{{ else }} {{ else }}
dev_error_url = nil dev_error_url = def_backend
{{ end }} {{ end }}
local options = {}
def_backend = "http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }}"
require("error_page") require("error_page")
} }
@ -178,25 +178,6 @@ http {
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }} {{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
} }
{{ if ge (len .sslCertificates) 1 }}
# SSL
# TODO: support more than one certificate
server {
listen 443 ssl http2 default_server;
{{ range $sslCert := .sslCertificates }}{{ if $sslCert.Default }}
# default certificate in case no match
ssl_certificate "{{ $sslCert.Cert }}";
ssl_certificate_key "{{ $sslCert.Key }}";
{{ end }}{{ end }}
location / {
proxy_pass http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }};
}
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
}
{{ end }}
{{range $name, $upstream := .upstreams}} {{range $name, $upstream := .upstreams}}
upstream {{$upstream.Name}} { upstream {{$upstream.Name}} {
least_conn; least_conn;
@ -256,6 +237,17 @@ http {
} }
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }} {{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
} }
# default server for services without endpoints
server {
listen 8081;
location / {
content_by_lua_block {
openURL(503, dev_error_url)
}
}
}
} }
# TCP services # TCP services

View file

@ -17,6 +17,7 @@ limitations under the License.
package nginx package nginx
import ( import (
"os"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -27,7 +28,6 @@ import (
"k8s.io/contrib/ingress/controllers/nginx-third-party/ssl" "k8s.io/contrib/ingress/controllers/nginx-third-party/ssl"
"k8s.io/kubernetes/pkg/client/record"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
k8sruntime "k8s.io/kubernetes/pkg/runtime" k8sruntime "k8s.io/kubernetes/pkg/runtime"
) )
@ -220,8 +220,8 @@ type NginxManager struct {
// path to the configuration file to be used by nginx // path to the configuration file to be used by nginx
ConfigFile string ConfigFile string
sslCertificates []ssl.Certificate
sslDHParam string sslDHParam string
servicesL4 []Service servicesL4 []Service
client *client.Client client *client.Client
@ -231,8 +231,6 @@ type NginxManager struct {
// obj runtime object to be used in events // obj runtime object to be used in events
obj k8sruntime.Object obj k8sruntime.Object
recorder record.EventRecorder
reloadLock *sync.Mutex reloadLock *sync.Mutex
} }
@ -282,11 +280,19 @@ func NewManager(kubeClient *client.Client, defaultSvc, customErrorSvc Service) *
defError: customErrorSvc, defError: customErrorSvc,
defResolver: strings.Join(getDnsServers(), " "), defResolver: strings.Join(getDnsServers(), " "),
reloadLock: &sync.Mutex{}, reloadLock: &sync.Mutex{},
sslDHParam: ssl.SearchDHParamFile(sslDirectory),
sslCertificates: ssl.CreateSSLCerts(sslDirectory),
} }
ngx.createCertsDir(sslDirectory)
ngx.sslDHParam = ssl.SearchDHParamFile(sslDirectory)
ngx.loadTemplate() ngx.loadTemplate()
return ngx return ngx
} }
func (nginx *NginxManager) createCertsDir(base string) {
if err := os.Mkdir(base, os.ModeDir); err != nil {
glog.Fatalf("Couldn't create directory %v: %v", base, err)
}
}

View file

@ -16,13 +16,11 @@ limitations under the License.
package nginx package nginx
// NGINXController Updates NGINX configuration, starts and reloads NGINX import (
type NGINXController struct { "os"
resolver string
nginxConfdPath string "github.com/golang/glog"
nginxCertsPath string )
local bool
}
// IngressNGINXConfig describes an NGINX configuration // IngressNGINXConfig describes an NGINX configuration
type IngressNGINXConfig struct { type IngressNGINXConfig struct {
@ -113,3 +111,25 @@ func NewUpstream(name string) Upstream {
Backends: []UpstreamServer{}, Backends: []UpstreamServer{},
} }
} }
// AddOrUpdateCertAndKey creates a .pem file wth the cert and the key with the specified name
func (nginx *NginxManager) AddOrUpdateCertAndKey(name string, cert string, key string) string {
pemFileName := sslDirectory + "/" + name + ".pem"
pem, err := os.Create(pemFileName)
if err != nil {
glog.Fatalf("Couldn't create pem file %v: %v", pemFileName, err)
}
defer pem.Close()
_, err = pem.WriteString(string(key))
if err != nil {
glog.Fatalf("Couldn't write to pem file %v: %v", pemFileName, err)
}
_, err = pem.WriteString(string(cert))
if err != nil {
glog.Fatalf("Couldn't write to pem file %v: %v", pemFileName, err)
}
return pemFileName
}

View file

@ -25,12 +25,9 @@ import (
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/golang/glog" "github.com/golang/glog"
"k8s.io/contrib/ingress/controllers/nginx-third-party/ssl"
) )
var funcMap = template.FuncMap{ var funcMap = template.FuncMap{
"getSSLHost": ssl.GetSSLHost,
"empty": func(input interface{}) bool { "empty": func(input interface{}) bool {
check, ok := input.(string) check, ok := input.(string)
if ok { if ok {
@ -66,7 +63,6 @@ func (ngx *NginxManager) writeCfg(cfg *nginxConfiguration, upstreams []Upstream,
curNginxCfg := merge(toMap, fromMap) curNginxCfg := merge(toMap, fromMap)
conf := make(map[string]interface{}) conf := make(map[string]interface{})
conf["sslCertificates"] = ngx.sslCertificates
conf["upstreams"] = upstreams conf["upstreams"] = upstreams
conf["servers"] = servers conf["servers"] = servers
conf["tcpServices"] = servicesL4 conf["tcpServices"] = servicesL4

View file

@ -17,139 +17,13 @@ limitations under the License.
package ssl package ssl
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"regexp"
"strings"
"github.com/golang/glog" "github.com/golang/glog"
) )
// Certificate contains the cert, key and the list of valid hostnames
type Certificate struct {
Cert string
Key string
Cname []string
Valid bool
Default bool
}
// CreateSSLCerts reads the content of the /etc/nginx-ssl directory and
// verifies the cert and key extracting the common names for this pair
func CreateSSLCerts(baseDir string) []Certificate {
sslCerts := []Certificate{}
glog.Infof("inspecting directory %v for SSL certificates\n", baseDir)
files, _ := ioutil.ReadDir(baseDir)
for _, file := range files {
if !file.IsDir() {
continue
}
// the name of the secret could be different than the certificate file
cert, key, err := getCert(fmt.Sprintf("%v/%v", baseDir, file.Name()))
if err != nil {
glog.Errorf("error checking certificate: %v", err)
continue
}
hosts, err := checkSSLCertificate(cert, key)
if err == nil {
sslCert := Certificate{
Cert: cert,
Key: key,
Cname: hosts,
Valid: true,
}
if file.Name() == "default" {
sslCert.Default = true
}
sslCerts = append(sslCerts, sslCert)
} else {
glog.Errorf("error checking certificate: %v", err)
}
}
if len(sslCerts) == 1 {
sslCerts[0].Default = true
}
glog.Infof("ssl certificates found: %v", sslCerts)
return sslCerts
}
// checkSSLCertificate check if the certificate and key file are valid
// returning the result of the validation and the list of hostnames
// contained in the common name/s
func checkSSLCertificate(certFile, keyFile string) ([]string, error) {
_, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
glog.Errorf("Error checking certificate and key file %v/%v: %v", certFile, keyFile, err)
return []string{}, err
}
pemCerts, err := ioutil.ReadFile(certFile)
if err != nil {
return []string{}, err
}
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
glog.Errorf("Error checking certificate and key file %v/%v: %v", certFile, keyFile, err)
return []string{}, err
}
cn := []string{cert.Subject.CommonName}
if len(cert.DNSNames) > 0 {
cn = append(cn, cert.DNSNames...)
}
glog.Infof("DNS %v %v\n", cn, len(cn))
return cn, nil
}
func verifyHostname(certFile, host string) bool {
pemCerts, err := ioutil.ReadFile(certFile)
if err != nil {
return false
}
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
cert, err := x509.ParseCertificate(block.Bytes)
err = cert.VerifyHostname(host)
if err == nil {
return true
}
return false
}
// GetSSLHost checks if in one of the secrets that contains SSL
// certificates could be used for the specified server name
func GetSSLHost(serverName string, certs []Certificate) Certificate {
for _, sslCert := range certs {
if verifyHostname(sslCert.Cert, serverName) {
return sslCert
}
}
return Certificate{}
}
// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory // SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory
// in order to find a file with the name dhparam.pem. If such file exists it will // in order to find a file with the name dhparam.pem. If such file exists it will
// returns the path. If not it just returns an empty string // returns the path. If not it just returns an empty string
@ -170,32 +44,3 @@ func SearchDHParamFile(baseDir string) string {
glog.Warning("no file dhparam.pem found in secrets") glog.Warning("no file dhparam.pem found in secrets")
return "" return ""
} }
// getCert returns the pair cert-key if exists or an error
func getCert(certDir string) (cert string, key string, err error) {
// we search for a file with extension crt
filepath.Walk(certDir, func(path string, f os.FileInfo, _ error) error {
if !f.IsDir() {
r, err := regexp.MatchString(".crt", f.Name())
if err == nil && r {
cert = f.Name()
return nil
}
}
return nil
})
cert = fmt.Sprintf("%v/%v", certDir, cert)
if _, err := os.Stat(cert); os.IsNotExist(err) {
return "", "", fmt.Errorf("No certificate found in directory %v: %v", certDir, err)
}
key = strings.Replace(cert, ".crt", ".key", 1)
if _, err := os.Stat(key); os.IsNotExist(err) {
return "", "", fmt.Errorf("No certificate key found in directory %v: %v", certDir, err)
}
return
}