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,12 +1,12 @@
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.",
detailed = [[ detailed = [[
Features an HTTP 1.0 and 1.1 streaming interface to reading Features an HTTP 1.0 and 1.1 streaming interface to reading
bodies using coroutines, for predictable memory usage in Lua bodies using coroutines, for predictable memory usage in Lua
land. Alternative simple interface for singleshot requests land. Alternative simple interface for singleshot requests
@ -17,17 +17,17 @@ description = {
Recommended by the OpenResty maintainer as a long-term Recommended by the OpenResty maintainer as a long-term
replacement for internal requests through ngx.location.capture. replacement for internal requests through ngx.location.capture.
]], ]],
homepage = "https://github.com/pintsized/lua-resty-http", homepage = "https://github.com/pintsized/lua-resty-http",
license = "2-clause BSD", license = "2-clause BSD",
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",
modules = { modules = {
["resty.http"] = "lib/resty/http.lua", ["resty.http"] = "lib/resty/http.lua",
["resty.http_headers"] = "lib/resty/http_headers.lua" ["resty.http_headers"] = "lib/resty/http_headers.lua"
} }
} }

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,9 +220,9 @@ 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
// template loaded ready to be used to generate the nginx configuration file // template loaded ready to be used to generate the nginx configuration file
@ -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
} }
@ -276,17 +274,25 @@ func newDefaultNginxCfg() *nginxConfiguration {
// NewManager ... // NewManager ...
func NewManager(kubeClient *client.Client, defaultSvc, customErrorSvc Service) *NginxManager { func NewManager(kubeClient *client.Client, defaultSvc, customErrorSvc Service) *NginxManager {
ngx := &NginxManager{ ngx := &NginxManager{
ConfigFile: "/etc/nginx/nginx.conf", ConfigFile: "/etc/nginx/nginx.conf",
defBackend: defaultSvc, defBackend: defaultSvc,
defCfg: newDefaultNginxCfg(), defCfg: newDefaultNginxCfg(),
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
}