From 411f604c4e457ce6500dad33f505b150c87a98b3 Mon Sep 17 00:00:00 2001 From: Joao Morais Date: Sun, 10 Mar 2019 08:10:44 -0300 Subject: [PATCH] add TLS authentication --- pkg/common/ingress/controller/controller.go | 15 +- pkg/controller/cache.go | 38 ++-- pkg/controller/controller.go | 39 +++- pkg/converters/ingress/annotations/global.go | 4 +- pkg/converters/ingress/annotations/host.go | 10 +- .../ingress/helper_test/cachemock.go | 30 +++- pkg/converters/ingress/ingress.go | 43 ++--- pkg/converters/ingress/ingress_test.go | 37 ++-- pkg/converters/ingress/types/annotations.go | 1 + pkg/converters/ingress/types/interfaces.go | 6 +- pkg/converters/ingress/types/options.go | 8 +- pkg/haproxy/config.go | 48 +++-- pkg/haproxy/helper_test/bindutilsmock.go | 4 +- pkg/haproxy/instance_test.go | 169 ++++++++++++------ pkg/haproxy/types/frontend.go | 6 +- pkg/haproxy/types/types.go | 3 +- rootfs/etc/haproxy/template/haproxy.tmpl | 58 +++--- 17 files changed, 318 insertions(+), 201 deletions(-) diff --git a/pkg/common/ingress/controller/controller.go b/pkg/common/ingress/controller/controller.go index c24a5a6e1..11ad53a7a 100644 --- a/pkg/common/ingress/controller/controller.go +++ b/pkg/common/ingress/controller/controller.go @@ -1532,15 +1532,17 @@ func (ic GenericController) Stop() error { return fmt.Errorf("shutdown already in progress") } +// StartControllers ... +func (ic *GenericController) StartControllers() { + ic.cacheController.Run(ic.stopCh) +} + // Start starts the Ingress controller. func (ic *GenericController) Start() { glog.Infof("starting Ingress controller") - ic.cacheController.Run(ic.stopCh) - - createDefaultSSLCertificate() - if ic.cfg.V07 { + ic.CreateDefaultSSLCertificate() time.Sleep(5 * time.Second) // initial sync of secrets to avoid unnecessary reloads glog.Info("running initial sync of secrets") @@ -1587,7 +1589,8 @@ func (ic *GenericController) SetForceReload(shouldReload bool) { } } -func createDefaultSSLCertificate() { +// CreateDefaultSSLCertificate ... +func (ic *GenericController) CreateDefaultSSLCertificate() (path, hash string) { defCert, defKey := ssl.GetFakeSSLCert() c, err := ssl.AddOrUpdateCertAndKey(fakeCertificate, defCert, defKey, []byte{}) if err != nil { @@ -1596,4 +1599,6 @@ func createDefaultSSLCertificate() { fakeCertificateSHA = c.PemSHA fakeCertificatePath = c.PemFileName + + return fakeCertificatePath, fakeCertificateSHA } diff --git a/pkg/controller/cache.go b/pkg/controller/cache.go index 81aae20f2..2c676cfdb 100644 --- a/pkg/controller/cache.go +++ b/pkg/controller/cache.go @@ -22,9 +22,11 @@ import ( api "k8s.io/api/core/v1" + "github.com/jcmoraisjr/haproxy-ingress/pkg/common/file" "github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress" "github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/controller" "github.com/jcmoraisjr/haproxy-ingress/pkg/common/net/ssl" + ingtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" ) type cache struct { @@ -56,44 +58,52 @@ func (c *cache) GetPod(podName string) (*api.Pod, error) { return c.listers.Pod.GetPod(sname[0], sname[1]) } -func (c *cache) GetTLSSecretPath(secretName string) (string, error) { +func (c *cache) GetTLSSecretPath(secretName string) (ingtypes.File, error) { sslCert, err := c.controller.GetCertificate(secretName) if err != nil { - return "", err + return ingtypes.File{}, err } if sslCert.PemFileName == "" { - return "", fmt.Errorf("secret '%s' does not have keys 'tls.crt' and 'tls.key'", secretName) + return ingtypes.File{}, fmt.Errorf("secret '%s' does not have keys 'tls.crt' and 'tls.key'", secretName) } - return sslCert.PemFileName, nil + return ingtypes.File{ + Filename: sslCert.PemFileName, + SHA1Hash: sslCert.PemSHA, + }, nil } -func (c *cache) GetCASecretPath(secretName string) (string, error) { +func (c *cache) GetCASecretPath(secretName string) (ingtypes.File, error) { sslCert, err := c.controller.GetCertificate(secretName) if err != nil { - return "", err + return ingtypes.File{}, err } if sslCert.CAFileName == "" { - return "", fmt.Errorf("secret '%s' does not have key 'ca.crt'", secretName) + return ingtypes.File{}, fmt.Errorf("secret '%s' does not have key 'ca.crt'", secretName) } - return sslCert.CAFileName, nil + return ingtypes.File{ + Filename: sslCert.CAFileName, + SHA1Hash: sslCert.PemSHA, + }, nil } -func (c *cache) GetDHSecretPath(secretName string) (string, error) { +func (c *cache) GetDHSecretPath(secretName string) (ingtypes.File, error) { secret, err := c.listers.Secret.GetByName(secretName) if err != nil { - return "", err + return ingtypes.File{}, err } dh, found := secret.Data[dhparamFilename] if !found { - return "", fmt.Errorf("secret '%s' does not have key '%s'", secretName, dhparamFilename) + return ingtypes.File{}, fmt.Errorf("secret '%s' does not have key '%s'", secretName, dhparamFilename) } pem := strings.Replace(secretName, "/", "_", -1) pemFileName, err := ssl.AddOrUpdateDHParam(pem, dh) if err != nil { - return "", fmt.Errorf("error creating dh-param file '%s': %v", pem, err) + return ingtypes.File{}, fmt.Errorf("error creating dh-param file '%s': %v", pem, err) } - // file.SHA1(pemFileName) - return pemFileName, nil + return ingtypes.File{ + Filename: pemFileName, + SHA1Hash: file.SHA1(pemFileName), + }, nil } func (c *cache) GetSecretContent(secretName, keyName string) ([]byte, error) { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index c74dc1bc6..16bda7205 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -80,6 +80,7 @@ func (hc *HAProxyController) Info() *ingress.BackendInfo { // Start starts the controller func (hc *HAProxyController) Start() { hc.controller = controller.NewIngressController(hc) + hc.controller.StartControllers() hc.configController() hc.controller.Start() } @@ -96,13 +97,6 @@ func (hc *HAProxyController) configController() { // starting v0.8 only config logger := &logger{depth: 1} - hc.converterOptions = &ingtypes.ConverterOptions{ - Logger: logger, - Cache: newCache(hc.storeLister, hc.controller), - AnnotationPrefix: "ingress.kubernetes.io", - DefaultBackend: hc.cfg.DefaultService, - DefaultSSLSecret: hc.cfg.DefaultSSLCertificate, - } instanceOptions := haproxy.InstanceOptions{ HAProxyCmd: "haproxy", ReloadCmd: "/haproxy-reload.sh", @@ -114,6 +108,32 @@ func (hc *HAProxyController) configController() { if err := hc.instance.ParseTemplates(); err != nil { glog.Fatalf("error creating HAProxy instance: %v", err) } + cache := newCache(hc.storeLister, hc.controller) + hc.converterOptions = &ingtypes.ConverterOptions{ + Logger: logger, + Cache: cache, + AnnotationPrefix: "ingress.kubernetes.io", + DefaultBackend: hc.cfg.DefaultService, + DefaultSSLFile: hc.createDefaultSSLFile(cache), + } +} + +func (hc *HAProxyController) createDefaultSSLFile(cache *cache) (tlsFile ingtypes.File) { + if hc.cfg.DefaultSSLCertificate != "" { + tlsFile, err := cache.GetTLSSecretPath(hc.cfg.DefaultSSLCertificate) + if err == nil { + return tlsFile + } + glog.Warningf("using auto generated fake certificate due to an error reading default TLS certificate: %v", err) + } else { + glog.Info("using auto generated fake certificate") + } + path, hash := hc.controller.CreateDefaultSSLCertificate() + tlsFile = ingtypes.File{ + Filename: path, + SHA1Hash: hash, + } + return tlsFile } // CreateX509CertsDir hard link files from certs to a single directory. @@ -126,11 +146,12 @@ func (hc *HAProxyController) CreateX509CertsDir(bindName string, certs []string) return "", err } for _, cert := range certs { - file, err := os.Stat(cert) + srcFile, err := os.Stat(cert) if err != nil { return "", err } - if err := os.Link(cert, x509dir+"/"+file.Name()); err != os.ErrExist { + dstFile := x509dir + "/" + srcFile.Name() + if err := os.Link(cert, dstFile); err != nil { return "", err } } diff --git a/pkg/converters/ingress/annotations/global.go b/pkg/converters/ingress/annotations/global.go index 1cde762ec..5b5d60828 100644 --- a/pkg/converters/ingress/annotations/global.go +++ b/pkg/converters/ingress/annotations/global.go @@ -90,8 +90,8 @@ func (c *updater) buildGlobalSSL(d *globalData) { d.global.SSL.Ciphers = d.config.SSLCiphers d.global.SSL.Options = d.config.SSLOptions if d.config.SSLDHParam != "" { - if dhFilename, err := c.cache.GetDHSecretPath(d.config.SSLDHParam); err == nil { - d.global.SSL.DHParam.Filename = dhFilename + if dhFile, err := c.cache.GetDHSecretPath(d.config.SSLDHParam); err == nil { + d.global.SSL.DHParam.Filename = dhFile.Filename } else { c.logger.Error("error reading DH params: %v", err) } diff --git a/pkg/converters/ingress/annotations/host.go b/pkg/converters/ingress/annotations/host.go index 6ed0b9084..bed6ace48 100644 --- a/pkg/converters/ingress/annotations/host.go +++ b/pkg/converters/ingress/annotations/host.go @@ -20,10 +20,18 @@ func (c *updater) buildHostAuthTLS(d *hostData) { if d.ann.AuthTLSSecret == "" { return } + verify := d.ann.AuthTLSVerifyClient + if verify == "off" { + return + } if cafile, err := c.cache.GetCASecretPath(d.ann.AuthTLSSecret); err == nil { - d.host.TLS.CAFilename = cafile + d.host.TLS.CAFilename = cafile.Filename + d.host.TLS.CAHash = cafile.SHA1Hash + d.host.TLS.CAVerifyOptional = verify == "optional" || verify == "optional_no_ca" d.host.TLS.CAErrorPage = d.ann.AuthTLSErrorPage d.host.TLS.AddCertHeader = d.ann.AuthTLSCertHeader + } else { + c.logger.Error("error building TLS auth config: %v", err) } } diff --git a/pkg/converters/ingress/helper_test/cachemock.go b/pkg/converters/ingress/helper_test/cachemock.go index 5399cdd10..b14139355 100644 --- a/pkg/converters/ingress/helper_test/cachemock.go +++ b/pkg/converters/ingress/helper_test/cachemock.go @@ -17,10 +17,13 @@ limitations under the License. package helper_test import ( + "crypto/sha1" "fmt" "strings" api "k8s.io/api/core/v1" + + ingtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" ) // SecretContent ... @@ -68,27 +71,36 @@ func (c *CacheMock) GetPod(podName string) (*api.Pod, error) { } // GetTLSSecretPath ... -func (c *CacheMock) GetTLSSecretPath(secretName string) (string, error) { +func (c *CacheMock) GetTLSSecretPath(secretName string) (ingtypes.File, error) { if path, found := c.SecretTLSPath[secretName]; found { - return path, nil + return ingtypes.File{ + Filename: path, + SHA1Hash: fmt.Sprintf("%x", sha1.Sum([]byte(path))), + }, nil } - return "", fmt.Errorf("secret not found: '%s'", secretName) + return ingtypes.File{}, fmt.Errorf("secret not found: '%s'", secretName) } // GetCASecretPath ... -func (c *CacheMock) GetCASecretPath(secretName string) (string, error) { +func (c *CacheMock) GetCASecretPath(secretName string) (ingtypes.File, error) { if path, found := c.SecretCAPath[secretName]; found { - return path, nil + return ingtypes.File{ + Filename: path, + SHA1Hash: fmt.Sprintf("%x", sha1.Sum([]byte(path))), + }, nil } - return "", fmt.Errorf("secret not found: '%s'", secretName) + return ingtypes.File{}, fmt.Errorf("secret not found: '%s'", secretName) } // GetDHSecretPath ... -func (c *CacheMock) GetDHSecretPath(secretName string) (string, error) { +func (c *CacheMock) GetDHSecretPath(secretName string) (ingtypes.File, error) { if path, found := c.SecretDHPath[secretName]; found { - return path, nil + return ingtypes.File{ + Filename: path, + SHA1Hash: fmt.Sprintf("%x", sha1.Sum([]byte(path))), + }, nil } - return "", fmt.Errorf("secret not found: '%s'", secretName) + return ingtypes.File{}, fmt.Errorf("secret not found: '%s'", secretName) } // GetSecretContent ... diff --git a/pkg/converters/ingress/ingress.go b/pkg/converters/ingress/ingress.go index b2f2fbd94..460d4de4b 100644 --- a/pkg/converters/ingress/ingress.go +++ b/pkg/converters/ingress/ingress.go @@ -48,6 +48,7 @@ func NewIngressConverter(options *ingtypes.ConverterOptions, haproxy haproxy.Con hostAnnotations: map[*hatypes.Host]*ingtypes.HostAnnotations{}, backendAnnotations: map[*hatypes.Backend]*ingtypes.BackendAnnotations{}, } + haproxy.ConfigDefaultX509Cert(options.DefaultSSLFile.Filename) if options.DefaultBackend != "" { if backend, err := c.addBackend(options.DefaultBackend, 0, &ingtypes.BackendAnnotations{}); err == nil { haproxy.ConfigDefaultBackend(backend) @@ -121,19 +122,16 @@ func (c *converter) syncIngress(ing *extensions.Ingress) { for _, tls := range ing.Spec.TLS { for _, tlshost := range tls.Hosts { if tlshost == hostname { - tlsPath, err := c.addTLS(ing.Namespace, tls.SecretName) - if err == nil { - if host.TLS.TLSFilename == "" { - host.TLS.TLSFilename = tlsPath - } else if host.TLS.TLSFilename != tlsPath { - err = fmt.Errorf("TLS of host '%s' was already assigned", host.Hostname) - } - } - if err != nil { + tlsPath := c.addTLS(ing.Namespace, tls.SecretName) + if host.TLS.TLSHash == "" { + host.TLS.TLSFilename = tlsPath.Filename + host.TLS.TLSHash = tlsPath.SHA1Hash + } else if host.TLS.TLSHash != tlsPath.SHA1Hash { + msg := fmt.Sprintf("TLS of host '%s' was already assigned", host.Hostname) if tls.SecretName != "" { - c.logger.Warn("skipping TLS secret '%s' of ingress '%s': %v", tls.SecretName, fullIngName, err) + c.logger.Warn("skipping TLS secret '%s' of ingress '%s': %s", tls.SecretName, fullIngName, msg) } else { - c.logger.Warn("skipping default TLS secret of ingress '%s': %v", fullIngName, err) + c.logger.Warn("skipping default TLS secret of ingress '%s': %s", fullIngName, msg) } } } @@ -230,25 +228,16 @@ func (c *converter) addHTTPPassthrough(fullSvcName string, ingFrontAnn *ingtypes } } -func (c *converter) addTLS(namespace, secretName string) (string, error) { - defaultSecret := c.options.DefaultSSLSecret - tlsSecretName := defaultSecret +func (c *converter) addTLS(namespace, secretName string) ingtypes.File { if secretName != "" { - tlsSecretName = namespace + "/" + secretName - } - tlsPath, err := c.cache.GetTLSSecretPath(tlsSecretName) - if err != nil { - if tlsSecretName == defaultSecret { - return "", err - } - tlsSecretErr := err - tlsPath, err = c.cache.GetTLSSecretPath(defaultSecret) - if err != nil { - return "", fmt.Errorf("failed to use custom and default certificate. custom: %v; default: %v", tlsSecretErr, err) + tlsSecretName := namespace + "/" + secretName + tlsFile, err := c.cache.GetTLSSecretPath(tlsSecretName) + if err == nil { + return tlsFile } - c.logger.Warn("using default certificate due to an error reading secret '%s': %v", tlsSecretName, tlsSecretErr) + c.logger.Warn("using default certificate due to an error reading secret '%s': %v", tlsSecretName, err) } - return tlsPath, nil + return c.options.DefaultSSLFile } func (c *converter) addEndpoints(svc *api.Service, servicePort int, backend *hatypes.Backend) error { diff --git a/pkg/converters/ingress/ingress_test.go b/pkg/converters/ingress/ingress_test.go index f0dd5a514..d366b58ce 100644 --- a/pkg/converters/ingress/ingress_test.go +++ b/pkg/converters/ingress/ingress_test.go @@ -428,25 +428,7 @@ func TestSyncRedeclareTLSCustomFirst(t *testing.T) { WARN skipping default TLS secret of ingress 'default/echo2': TLS of host 'echo.example.com' was already assigned`) } -func TestSyncNoDefaultNoTLS(t *testing.T) { - c := setup(t) - defer c.teardown() - - c.cache.SecretTLSPath = map[string]string{} - c.createSvc1Auto() - c.Sync(c.createIngTLS1("default/echo", "echo.example.com", "/", "echo:8080", "")) - - c.compareConfigFront(` -- hostname: echo.example.com - paths: - - path: / - backend: default_echo_8080`) - - c.compareLogging(` -WARN skipping default TLS secret of ingress 'default/echo': secret not found: 'system/ingress-default'`) -} - -func TestSyncNoDefaultInvalidTLS(t *testing.T) { +func TestSyncInvalidTLS(t *testing.T) { c := setup(t) defer c.teardown() @@ -458,10 +440,12 @@ func TestSyncNoDefaultInvalidTLS(t *testing.T) { - hostname: echo.example.com paths: - path: / - backend: default_echo_8080`) + backend: default_echo_8080 + tls: + tlsfilename: /tls/tls-default.pem`) c.compareLogging(` -WARN skipping TLS secret 'tls-invalid' of ingress 'default/echo': failed to use custom and default certificate. custom: secret not found: 'default/tls-invalid'; default: secret not found: 'system/ingress-default'`) +WARN using default certificate due to an error reading secret 'default/tls-invalid': secret not found: 'default/tls-invalid'`) } func TestSyncRootPathDefault(t *testing.T) { @@ -977,10 +961,13 @@ var defaultBackendConfig = ` func (c *testConfig) SyncDef(config map[string]string, ing ...*extensions.Ingress) { conv := NewIngressConverter( &ingtypes.ConverterOptions{ - Cache: c.cache, - Logger: c.logger, - DefaultBackend: "system/default", - DefaultSSLSecret: "system/ingress-default", + Cache: c.cache, + Logger: c.logger, + DefaultBackend: "system/default", + DefaultSSLFile: ingtypes.File{ + Filename: "/tls/tls-default.pem", + SHA1Hash: "1", + }, AnnotationPrefix: "ingress.kubernetes.io", }, c.hconfig, diff --git a/pkg/converters/ingress/types/annotations.go b/pkg/converters/ingress/types/annotations.go index e57b6bbf3..9a4bf0925 100644 --- a/pkg/converters/ingress/types/annotations.go +++ b/pkg/converters/ingress/types/annotations.go @@ -22,6 +22,7 @@ type HostAnnotations struct { AppRoot string `json:"app-root"` AuthTLSCertHeader bool `json:"auth-tls-cert-header"` AuthTLSErrorPage string `json:"auth-tls-error-page"` + AuthTLSVerifyClient string `json:"auth-tls-verify-client"` AuthTLSSecret string `json:"auth-tls-secret"` ServerAlias string `json:"server-alias"` ServerAliasRegex string `json:"server-alias-regex"` diff --git a/pkg/converters/ingress/types/interfaces.go b/pkg/converters/ingress/types/interfaces.go index 08a875876..db60ab13f 100644 --- a/pkg/converters/ingress/types/interfaces.go +++ b/pkg/converters/ingress/types/interfaces.go @@ -25,8 +25,8 @@ type Cache interface { GetService(serviceName string) (*api.Service, error) GetEndpoints(service *api.Service) (*api.Endpoints, error) GetPod(podName string) (*api.Pod, error) - GetTLSSecretPath(secretName string) (string, error) - GetCASecretPath(secretName string) (string, error) - GetDHSecretPath(secretName string) (string, error) + GetTLSSecretPath(secretName string) (File, error) + GetCASecretPath(secretName string) (File, error) + GetDHSecretPath(secretName string) (File, error) GetSecretContent(secretName, keyName string) ([]byte, error) } diff --git a/pkg/converters/ingress/types/options.go b/pkg/converters/ingress/types/options.go index eac64d5cf..dcc49cbe7 100644 --- a/pkg/converters/ingress/types/options.go +++ b/pkg/converters/ingress/types/options.go @@ -20,11 +20,17 @@ import ( "github.com/jcmoraisjr/haproxy-ingress/pkg/types" ) +// File ... +type File struct { + Filename string + SHA1Hash string +} + // ConverterOptions ... type ConverterOptions struct { Logger types.Logger Cache Cache DefaultBackend string - DefaultSSLSecret string + DefaultSSLFile File AnnotationPrefix string } diff --git a/pkg/haproxy/config.go b/pkg/haproxy/config.go index 97b418077..285fe5564 100644 --- a/pkg/haproxy/config.go +++ b/pkg/haproxy/config.go @@ -193,11 +193,12 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { } frontends, sslpassthrough := hatypes.BuildRawFrontends(c.hosts) for _, frontend := range frontends { - prefix := c.mapsDir + "/" + frontend.Name - frontend.BackendsMap = prefix + ".map" - frontend.TLSInvalidCrtErrorPagesMap = prefix + "_inv_crt.map" - frontend.TLSNoCrtErrorPagesMap = prefix + "_no_crt.map" - frontend.VarNamespaceMap = prefix + "_k8s_ns.map" + mapPrefix := c.mapsDir + "/" + frontend.Name + frontend.HostBackendsMap = mapPrefix + "_host.map" + frontend.SNIBackendsMap = mapPrefix + "_sni.map" + frontend.TLSInvalidCrtErrorPagesMap = mapPrefix + "_inv_crt.map" + frontend.TLSNoCrtErrorPagesMap = mapPrefix + "_no_crt.map" + frontend.VarNamespaceMap = mapPrefix + "_k8s_ns.map" } fgroup := &hatypes.FrontendGroup{ Frontends: frontends, @@ -214,7 +215,8 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { var bindName string if len(bind.Hosts) == 1 { bindName = bind.Hosts[0].Hostname - bind.TLS.TLSCert = bind.Hosts[0].TLS.TLSFilename + bind.TLS.TLSCert = c.defaultX509Cert + bind.TLS.TLSCertDir = bind.Hosts[0].TLS.TLSFilename } else { i++ bindName = fmt.Sprintf("_socket%03d", i) @@ -236,15 +238,15 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { bind.Name = "_public" bind.Socket = ":443" if len(bind.Hosts) == 1 { - bind.TLS.TLSCert = bind.Hosts[0].TLS.TLSFilename + bind.TLS.TLSCert = c.defaultX509Cert + bind.TLS.TLSCertDir = bind.Hosts[0].TLS.TLSFilename } else { x509dir, err := c.createCertsDir(bind.Name, bind.Hosts) if err != nil { return nil, err } - tls := &frontends[0].Binds[0].TLS - tls.TLSCert = c.defaultX509Cert - tls.TLSCertDir = x509dir + frontends[0].Binds[0].TLS.TLSCert = c.defaultX509Cert + frontends[0].Binds[0].TLS.TLSCertDir = x509dir } } type mapEntry struct { @@ -272,7 +274,8 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { } } for _, f := range frontends { - var backendsMap []mapEntry + var hostBackendsMap []mapEntry + var sniBackendsMap []mapEntry var invalidCrtMap []mapEntry var noCrtMap []mapEntry var varNamespaceMap []mapEntry @@ -282,7 +285,11 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { Key: host.Hostname + path.Path, Value: path.BackendID, } - backendsMap = append(backendsMap, entry) + if host.HasTLSAuth() { + sniBackendsMap = append(sniBackendsMap, entry) + } else { + hostBackendsMap = append(hostBackendsMap, entry) + } if path.Backend.SSLRedirect { fgroup.HasRedirectHTTPS = true } else { @@ -301,10 +308,15 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { Value: host.TLS.CAErrorPage, } invalidCrtMap = append(invalidCrtMap, entry) - noCrtMap = append(noCrtMap, entry) + if !host.TLS.CAVerifyOptional { + noCrtMap = append(noCrtMap, entry) + } } } - if err := c.mapsTemplate.WriteOutput(backendsMap, f.BackendsMap); err != nil { + if err := c.mapsTemplate.WriteOutput(hostBackendsMap, f.HostBackendsMap); err != nil { + return nil, err + } + if err := c.mapsTemplate.WriteOutput(sniBackendsMap, f.SNIBackendsMap); err != nil { return nil, err } if err := c.mapsTemplate.WriteOutput(invalidCrtMap, f.TLSInvalidCrtErrorPagesMap); err != nil { @@ -329,11 +341,17 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { func (c *config) createCertsDir(bindName string, hosts []*hatypes.Host) (string, error) { certs := make([]string, 0, len(hosts)) + added := map[string]bool{} for _, host := range hosts { - if host.TLS.TLSFilename != "" { + filename := host.TLS.TLSFilename + if filename != "" && !added[filename] && filename != c.defaultX509Cert { certs = append(certs, host.TLS.TLSFilename) + added[filename] = true } } + if len(certs) == 0 { + return "", nil + } return c.bindUtils.CreateX509CertsDir(bindName, certs) } diff --git a/pkg/haproxy/helper_test/bindutilsmock.go b/pkg/haproxy/helper_test/bindutilsmock.go index e2414a96a..9b17e4032 100644 --- a/pkg/haproxy/helper_test/bindutilsmock.go +++ b/pkg/haproxy/helper_test/bindutilsmock.go @@ -18,7 +18,7 @@ package helper_test // BindUtilsMock ... type BindUtilsMock struct { - CertDirs []*CertDir + CertDirs []CertDir } // CertDir ... @@ -30,7 +30,7 @@ type CertDir struct { // CreateX509CertsDir ... func (b *BindUtilsMock) CreateX509CertsDir(bindName string, certs []string) (string, error) { dir := "/var/haproxy/certs/" + bindName - b.CertDirs = append(b.CertDirs, &CertDir{ + b.CertDirs = append(b.CertDirs, CertDir{ Dir: dir, Certs: certs, }) diff --git a/pkg/haproxy/instance_test.go b/pkg/haproxy/instance_test.go index bb24be3eb..444e64c6b 100644 --- a/pkg/haproxy/instance_test.go +++ b/pkg/haproxy/instance_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/kylelemons/godebug/diff" + yaml "gopkg.in/yaml.v2" ha_helper "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/helper_test" hatypes "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/types" @@ -82,9 +83,9 @@ frontend _front__http default_backend _error404 frontend https-front_empty mode http - bind :443 - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_empty.map,_nomatch) - use_backend %%[var(req.backend)] unless { var(req.backend) _nomatch } + bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_empty_host.map,_nomatch) + use_backend %%[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _error404 ` @@ -94,7 +95,7 @@ frontend https-front_empty c.checkMap("http-front.map", ` empty/ default_empty_8080`) - c.checkMap("https-front_empty.map", ` + c.checkMap("https-front_empty_host.map", ` empty/ default_empty_8080`) c.logger.CompareLogging(defaultLogging) @@ -117,7 +118,6 @@ func TestInstanceDefaultHost(t *testing.T) { h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h.VarNamespace = true b = c.config.AcquireBackend("d2", "app", 8080) @@ -125,7 +125,6 @@ func TestInstanceDefaultHost(t *testing.T) { h.AddPath(b, "/app") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h.VarNamespace = true c.instance.Update() @@ -149,14 +148,14 @@ frontend https-front_d2.local mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem http-request set-var(txn.namespace) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d2.local_k8s_ns.map,-) - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d2.local.map,_nomatch) - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d2.local_host.map,_nomatch) + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } use_backend d1_app_8080 `) c.checkMap("https-front_d2.local_k8s_ns.map", ` d2.local/app d2`) - c.checkMap("https-front_d2.local.map", ` + c.checkMap("https-front_d2.local_host.map", ` d2.local/app d2_app_8080`) c.logger.CompareLogging(defaultLogging) @@ -170,7 +169,6 @@ func TestInstanceSingleFrontendSingleBind(t *testing.T) { def := c.config.AcquireBackend("default", "default-backend", 8080) def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) - c.config.ConfigDefaultX509Cert("/var/haproxy/ssl/certs/default.pem") var h *hatypes.Host var b *hatypes.Backend @@ -213,18 +211,25 @@ frontend _front_001 mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem crt /var/haproxy/certs/_public http-request set-var(txn.namespace) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_k8s_ns.map,-) - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001.map,_nomatch) - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _default_backend `) c.checkMap("_front_001_k8s_ns.map", ` d1.local/ d1 d2.local/app -`) - c.checkMap("_front_001.map", ` + c.checkMap("_front_001_host.map", ` d1.local/ d1_app_8080 d2.local/app d2_app_8080`) + c.checkCerts(` +certdirs: +- dir: /var/haproxy/certs/_public + certs: + - /var/haproxy/ssl/certs/d1.pem + - /var/haproxy/ssl/certs/d2.pem`) + c.logger.CompareLogging(defaultLogging) } @@ -245,14 +250,12 @@ func TestInstanceSingleFrontendTwoBindsCA(t *testing.T) { h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h.TLS.CAFilename = "/var/haproxy/ssl/ca/d1.local.pem" h.TLS.CAHash = "1" h.TLS.CAErrorPage = "http://d1.local/error.html" h = c.config.AcquireHost("d2.local") h.AddPath(b, "/") - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h.TLS.CAFilename = "/var/haproxy/ssl/ca/d2.local.pem" h.TLS.CAHash = "2" @@ -271,9 +274,9 @@ listen _front__tls tcp-request content accept if { req.ssl_hello_type 1 } ## _front_001 use-server _server_d1.local if { req.ssl_sni -i d1.local } - server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 + server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 weight 0 use-server _server_d2.local if { req.ssl_sni -i d2.local } - server _server_d2.local unix@/var/run/front_d2.local.sock send-proxy-v2 + server _server_d2.local unix@/var/run/front_d2.local.sock send-proxy-v2 weight 0 # TODO default backend frontend _front__http mode http @@ -285,14 +288,20 @@ frontend _front_001 mode http bind unix@/var/run/front_d1.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all bind unix@/var/run/front_d2.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001.map,_nomatch) + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + http-request set-header x-ha-base %[ssl_fc_sni]%[path] + http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_sni.map,_nomatch) acl tls-invalid-crt ssl_c_ca_err gt 0 acl tls-invalid-crt ssl_c_err gt 0 acl tls-has-crt ssl_c_used - http-request set-var(req.tls_invalid_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt.map,_internal) if tls-invalid-crt - redirect location %[var(req.tls_invalid_redir)] code 303 if { var(req.tls_invalid_redir) -m found } !{ var(req.tls_invalid_redir) _internal } - use_backend _error495 if { var(req.tls_invalid_redir) _internal } { ssl_fc_sni -i d1.local d2.local } - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_no_crt.map,_internal) if !tls-has-crt + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt.map,_internal) if tls-invalid-crt + http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } + http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } + use_backend _error496 if { var(req.tls_nocrt_redir) _internal } { ssl_fc_sni -i d1.local d2.local } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i d1.local d2.local } + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend `) @@ -300,7 +309,9 @@ frontend _front_001 d1.local http://d1.local/error.html`) c.checkMap("_front_001_no_crt.map", ` d1.local http://d1.local/error.html`) - c.checkMap("_front_001.map", ` + c.checkMap("_front_001_host.map", ` +`) + c.checkMap("_front_001_sni.map", ` d1.local/ d_app_8080 d2.local/ d_app_8080`) @@ -325,30 +336,39 @@ func TestInstanceTwoFrontendsThreeBindsCA(t *testing.T) { b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} h.Timeout.Client = "1s" - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h.TLS.CAFilename = "/var/haproxy/ssl/ca/d1.local.pem" h.TLS.CAHash = "1" h.TLS.CAVerifyOptional = true h.TLS.CAErrorPage = "http://d1.local/error.html" - h = c.config.AcquireHost("d2.local") + h = c.config.AcquireHost("d21.local") h.AddPath(b, "/") h.Timeout.Client = "2s" - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" + h.TLS.TLSFilename = "/var/haproxy/ssl/certs/d.pem" + h.TLS.TLSHash = "1" + h.TLS.CAFilename = "/var/haproxy/ssl/ca/d2.local.pem" + h.TLS.CAHash = "1" + h.TLS.CAVerifyOptional = true + h.TLS.CAErrorPage = "http://d21.local/error.html" + + h = c.config.AcquireHost("d22.local") + h.AddPath(b, "/") + h.Timeout.Client = "2s" + h.TLS.TLSFilename = "/var/haproxy/ssl/certs/d.pem" + h.TLS.TLSHash = "1" h.TLS.CAFilename = "/var/haproxy/ssl/ca/d2.local.pem" h.TLS.CAHash = "1" + h.TLS.CAErrorPage = "http://d22.local/error.html" b = c.config.AcquireBackend("d", "app", 8080) h = c.config.AcquireHost("d3.local") h.AddPath(b, "/") b.Endpoints = []*hatypes.Endpoint{endpointS21} h.Timeout.Client = "2s" - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" h = c.config.AcquireHost("d4.local") h.AddPath(b, "/") h.Timeout.Client = "2s" - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" c.instance.Update() c.checkConfig(` @@ -367,50 +387,56 @@ listen _front__tls tcp-request inspect-delay 5s tcp-request content accept if { req.ssl_hello_type 1 } ## _front_001 - use-server _server_d2.local if { req.ssl_sni -i d2.local } - server _server_d2.local unix@/var/run/front_d2.local.sock send-proxy-v2 - use-server _server__socket001 if { req.ssl_sni -i d3.local d4.local } - server _server__socket001 unix@/var/run/front__socket001.sock send-proxy-v2 + use-server _server__socket001 if { req.ssl_sni -i d21.local d22.local } + server _server__socket001 unix@/var/run/front__socket001.sock send-proxy-v2 weight 0 + use-server _server__socket002 if { req.ssl_sni -i d3.local d4.local } + server _server__socket002 unix@/var/run/front__socket002.sock send-proxy-v2 weight 0 ## https-front_d1.local use-server _server_d1.local if { req.ssl_sni -i d1.local } - server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 + server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 weight 0 # TODO default backend frontend _front__http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/http-front.map,_nomatch) - redirect scheme https if { var(req.base) -i -m beg d1.local/ d2.local/ } + redirect scheme https if { var(req.base) -i -m beg d1.local/ d21.local/ d22.local/ } use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _default_backend frontend _front_001 mode http - bind unix@/var/run/front_d2.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all - bind unix@/var/run/front__socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/certs/_socket001 + bind unix@/var/run/front__socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem crt /var/haproxy/certs/_socket001 ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all + bind unix@/var/run/front__socket002.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem timeout client 2s - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001.map,_nomatch) + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + http-request set-header x-ha-base %[ssl_fc_sni]%[path] + http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_sni.map,_nomatch) acl tls-invalid-crt ssl_c_ca_err gt 0 acl tls-invalid-crt ssl_c_err gt 0 acl tls-has-crt ssl_c_used - http-request set-var(req.tls_invalid_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt.map,_internal) if tls-invalid-crt - use_backend _error495 if { var(req.tls_invalid_redir) _internal } { ssl_fc_sni -i d2.local } - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_no_crt.map,_internal) if !tls-has-crt + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt.map,_internal) if tls-invalid-crt + http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } + http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } + use_backend _error496 if { var(req.tls_nocrt_redir) _internal } { ssl_fc_sni -i d22.local } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i d21.local d22.local } + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend frontend https-front_d1.local mode http bind unix@/var/run/front_d1.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all timeout client 1s - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d1.local.map,_nomatch) + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d1.local_host.map,_nomatch) + http-request set-header x-ha-base %[ssl_fc_sni]%[path] + http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d1.local_sni.map,_nomatch) acl tls-invalid-crt ssl_c_ca_err gt 0 acl tls-invalid-crt ssl_c_err gt 0 - acl tls-has-crt ssl_c_used - http-request set-var(req.tls_invalid_redir) ssl_fc_sni,map(/etc/haproxy/maps/https-front_d1.local_inv_crt.map,_internal) if tls-invalid-crt - http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/https-front_d1.local_no_crt.map,_internal) if !tls-has-crt - redirect location %[var(req.tls_invalid_redir)] code 303 if { var(req.tls_invalid_redir) -m found } !{ var(req.tls_invalid_redir) _internal } - redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } - use_backend _error495 if { var(req.tls_invalid_redir) _internal } { ssl_fc_sni -i d1.local } - use_backend _error496 if { var(req.tls_nocrt_redir) _internal } { ssl_fc_sni -i d1.local } - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/https-front_d1.local_inv_crt.map,_internal) if tls-invalid-crt + http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i d1.local } + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend `) @@ -418,20 +444,33 @@ frontend https-front_d1.local d3.local/ d_app_8080 d4.local/ d_app_8080`) c.checkMap("_front_001_inv_crt.map", ` +d21.local http://d21.local/error.html +d22.local http://d22.local/error.html `) c.checkMap("_front_001_no_crt.map", ` +d22.local http://d22.local/error.html `) - c.checkMap("_front_001.map", ` -d2.local/ d_appca_8080 + c.checkMap("_front_001_host.map", ` d3.local/ d_app_8080 d4.local/ d_app_8080`) + c.checkMap("_front_001_sni.map", ` +d21.local/ d_appca_8080 +d22.local/ d_appca_8080`) c.checkMap("https-front_d1.local_inv_crt.map", ` d1.local http://d1.local/error.html`) c.checkMap("https-front_d1.local_no_crt.map", ` -d1.local http://d1.local/error.html`) - c.checkMap("https-front_d1.local.map", ` +`) + c.checkMap("https-front_d1.local_host.map", ` +`) + c.checkMap("https-front_d1.local_sni.map", ` d1.local/ d_appca_8080`) + c.checkCerts(` +certdirs: +- dir: /var/haproxy/certs/_socket001 + certs: + - /var/haproxy/ssl/certs/d.pem`) + c.logger.CompareLogging(defaultLogging) } @@ -452,7 +491,6 @@ func TestInstanceSomePaths(t *testing.T) { h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - h.TLS.TLSFilename = "/var/haproxy/ssl/certs/default.pem" b = c.config.AcquireBackend("d", "app1", 8080) h.AddPath(b, "/app") @@ -498,12 +536,12 @@ frontend _front__http frontend https-front_d.local mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d.local.map,_nomatch) - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d.local_host.map,_nomatch) + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _default_backend `) - c.checkMap("https-front_d.local.map", ` + c.checkMap("https-front_d.local_host.map", ` d.local/sub d_app3_8080 d.local/app/sub d_app2_8080 d.local/app d_app1_8080 @@ -535,7 +573,6 @@ func TestSSLPassthrough(t *testing.T) { h.SSLPassthrough = true b = c.config.AcquireBackend("d3", "app-http", 8080) - b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS41h} h.HTTPPassthroughBackend = b @@ -590,6 +627,7 @@ d3.local/ d3_app-http_8080`) type testConfig struct { t *testing.T logger *helper_test.LoggerMock + bindUtils *ha_helper.BindUtilsMock instance Instance config Config tempdir string @@ -624,14 +662,17 @@ func setup(t *testing.T) *testConfig { ); err != nil { t.Errorf("error parsing map.tmpl: %v", err) } - config := createConfig(&ha_helper.BindUtilsMock{}, options{ + bindUtils := &ha_helper.BindUtilsMock{} + config := createConfig(bindUtils, options{ mapsTemplate: instance.mapsTemplate, mapsDir: tempdir, }) instance.curConfig = config + config.ConfigDefaultX509Cert("/var/haproxy/ssl/certs/default.pem") return &testConfig{ t: t, logger: logger, + bindUtils: bindUtils, instance: instance, config: config, tempdir: tempdir, @@ -764,6 +805,11 @@ INFO (test) check was skipped INFO (test) reload was skipped INFO HAProxy successfully reloaded` +func _yamlMarshal(in interface{}) string { + out, _ := yaml.Marshal(in) + return string(out) +} + func (c *testConfig) checkConfig(backend, frontend string) { c.checkConfigFull(globalConfig + backend + errorPages + frontend) } @@ -778,6 +824,11 @@ func (c *testConfig) checkMap(mapName, expected string) { c.compareText(mapName, actual, expected) } +func (c *testConfig) checkCerts(expected string) { + actual := _yamlMarshal(c.bindUtils) + c.compareText("certs", actual, expected) +} + var replaceComments = regexp.MustCompile(`(?m)^[ \t]{0,2}(#.*)?[\r\n]+`) func (c *testConfig) readConfig(fileName string) string { diff --git a/pkg/haproxy/types/frontend.go b/pkg/haproxy/types/frontend.go index c4887a8b5..af6460988 100644 --- a/pkg/haproxy/types/frontend.go +++ b/pkg/haproxy/types/frontend.go @@ -60,10 +60,10 @@ func (f *Frontend) HasNoCrtErrorPage() bool { return f.HasInvalidErrorPage() } -// HasTLSOptional ... -func (f *Frontend) HasTLSOptional() bool { +// HasTLSMandatory ... +func (f *Frontend) HasTLSMandatory() bool { for _, host := range f.Hosts { - if host.TLS.CAVerifyOptional { + if !host.TLS.CAVerifyOptional { return true } } diff --git a/pkg/haproxy/types/types.go b/pkg/haproxy/types/types.go index b430bf45f..eea0dfcbb 100644 --- a/pkg/haproxy/types/types.go +++ b/pkg/haproxy/types/types.go @@ -99,8 +99,9 @@ type Frontend struct { Binds []*BindConfig Hosts []*Host // - BackendsMap string ConvertLowercase bool + HostBackendsMap string + SNIBackendsMap string Timeout HostTimeoutConfig TLSInvalidCrtErrorPagesMap string TLSNoCrtErrorPagesMap string diff --git a/rootfs/etc/haproxy/template/haproxy.tmpl b/rootfs/etc/haproxy/template/haproxy.tmpl index 530f4e5ea..994272be7 100644 --- a/rootfs/etc/haproxy/template/haproxy.tmpl +++ b/rootfs/etc/haproxy/template/haproxy.tmpl @@ -252,7 +252,7 @@ listen _front__tls {{- range $bind := $frontend.Binds }} use-server _server_{{ $bind.Name }} if { req.ssl_sni -i {{- range $host := $bind.Hosts }} {{ $host.Hostname }}{{ end }} } - server _server_{{ $bind.Name }} {{ $bind.Socket }} send-proxy-v2 + server _server_{{ $bind.Name }} {{ $bind.Socket }} send-proxy-v2 weight 0 {{- end }} {{- end }} # TODO default backend @@ -333,54 +333,62 @@ frontend {{ $frontend.Name }} {{- end }} {{- /*------------------------------------*/}} - http-request set-var(req.backend) base + http-request set-var(req.hostbackend) base {{- if $frontend.ConvertLowercase }},lower{{ end }} {{- "" }},regsub(:[0-9]+/,/) - {{- "" }},map_beg({{ $frontend.BackendsMap }},_nomatch) + {{- "" }},map_beg({{ $frontend.HostBackendsMap }},_nomatch) {{- /*------------------------------------*/}} {{- if $frontend.HasTLSAuth }} -{{- $optional := $frontend.HasTLSOptional }} +{{- /* missing concat converter, fix after 1.9 */}} + http-request set-header x-ha-base %[ssl_fc_sni]%[path] + http-request set-var(req.snibackend) hdr(x-ha-base) + {{- if $frontend.ConvertLowercase }},lower{{ end }} + {{- "" }},regsub(:[0-9]+/,/) + {{- "" }},map_beg({{ $frontend.SNIBackendsMap }},_nomatch) +{{- $mandatory := $frontend.HasTLSMandatory }} acl tls-invalid-crt ssl_c_ca_err gt 0 acl tls-invalid-crt ssl_c_err gt 0 +{{- if $mandatory }} acl tls-has-crt ssl_c_used - http-request set-var(req.tls_invalid_redir) ssl_fc_sni - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},map({{ $frontend.TLSInvalidCrtErrorPagesMap }},_internal) - {{- "" }} if tls-invalid-crt -{{- if $optional }} http-request set-var(req.tls_nocrt_redir) ssl_fc_sni {{- if $frontend.ConvertLowercase }},lower{{ end }} {{- "" }},map({{ $frontend.TLSNoCrtErrorPagesMap }},_internal) {{- "" }} if !tls-has-crt {{- end }} -{{- if $frontend.HasInvalidErrorPage }} - redirect location %[var(req.tls_invalid_redir)] code 303 if - {{- "" }} { var(req.tls_invalid_redir) -m found } !{ var(req.tls_invalid_redir) _internal } -{{- end }} -{{- if and $optional $frontend.HasNoCrtErrorPage }} - redirect location %[var(req.tls_nocrt_redir)] code 303 if + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni + {{- if $frontend.ConvertLowercase }},lower{{ end }} + {{- "" }},map({{ $frontend.TLSInvalidCrtErrorPagesMap }},_internal) + {{- "" }} if tls-invalid-crt +{{- if and $mandatory $frontend.HasNoCrtErrorPage }} + http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if {{- "" }} { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } {{- end }} - use_backend _error495 if - {{- "" }} { var(req.tls_invalid_redir) _internal } - {{- "" }} { ssl_fc_sni -i - {{- range $host := $frontend.Hosts }} - {{- if $host.HasTLSAuth }} {{ $host.Hostname }}{{ end }} - {{- end }} } -{{- if $optional }} +{{- if $frontend.HasInvalidErrorPage }} + http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if + {{- "" }} { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } +{{- end }} +{{- if $mandatory }} use_backend _error496 if {{- "" }} { var(req.tls_nocrt_redir) _internal } {{- "" }} { ssl_fc_sni -i {{- range $host := $frontend.Hosts }} - {{- if and $host.HasTLSAuth $host.TLS.CAVerifyOptional }} {{ $host.Hostname }}{{ end }} + {{- if and $host.HasTLSAuth (not $host.TLS.CAVerifyOptional) }} {{ $host.Hostname }}{{ end }} {{- end }} } {{- end }} + use_backend _error495 if + {{- "" }} { var(req.tls_invalidcrt_redir) _internal } + {{- "" }} { ssl_fc_sni -i + {{- range $host := $frontend.Hosts }} + {{- if $host.HasTLSAuth }} {{ $host.Hostname }}{{ end }} + {{- end }} } {{- end }} {{- /*------------------------------------*/}} - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } - + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } +{{- if $frontend.HasTLSAuth }} + use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } +{{- end }} {{- template "defaultbackend" map $cfg }} {{- end }}