diff --git a/docs/content/en/docs/configuration/keys.md b/docs/content/en/docs/configuration/keys.md index e4e7341c8..63b66f899 100644 --- a/docs/content/en/docs/configuration/keys.md +++ b/docs/content/en/docs/configuration/keys.md @@ -103,6 +103,7 @@ The table below describes all supported configuration keys. | [`auth-tls-cert-header`](#auth-tls) | [true\|false] | Backend | | | [`auth-tls-error-page`](#auth-tls) | url | Host | | | [`auth-tls-secret`](#auth-tls) | namespace/secret name | Host | | +| [`auth-tls-strict`](#auth-tls) | [true\|false] | Host | | | [`auth-tls-verify-client`](#auth-tls) | [off\|optional\|on\|optional_no_ca] | Host | | | `auth-type` | "basic" | Backend | | | [`backend-check-interval`](#health-check) | time with suffix | Backend | `2s` | @@ -411,14 +412,15 @@ See also: ## Auth TLS -| Configuration key | Scope | Default | Since | -|--------------------------|-----------|---------|-------| -| `auth-tls-cert-header` | `Backend` | `false` | | -| `auth-tls-error-page` | `Host` | | | -| `auth-tls-secret` | `Host` | | | -| `auth-tls-verify-client` | `Host` | | | -| `ssl-fingerprint-lower` | `Backend` | `false` | v0.10 | -| `ssl-headers-prefix` | `Global` | `X-SSL` | | +| Configuration key | Scope | Default | Since | +|--------------------------|-----------|---------|--------| +| `auth-tls-cert-header` | `Backend` | `false` | | +| `auth-tls-error-page` | `Host` | | | +| `auth-tls-secret` | `Host` | | | +| `auth-tls-strict` | `Host` | `false` | v0.8.1 | +| `auth-tls-verify-client` | `Host` | | | +| `ssl-fingerprint-lower` | `Backend` | `false` | v0.10 | +| `ssl-headers-prefix` | `Global` | `X-SSL` | | Configure client authentication with X509 certificate. The following headers are added to the request: @@ -436,6 +438,7 @@ The following keys are supported: * `auth-tls-cert-header`: If `true` HAProxy will add `X-SSL-Client-Cert` http header with a base64 encoding of the X509 certificate provided by the client. Default is to not provide the client certificate. * `auth-tls-error-page`: Optional URL of the page to redirect the user if he doesn't provide a certificate or the certificate is invalid. * `auth-tls-secret`: Mandatory secret name with `ca.crt` key providing all certificate authority bundles used to validate client certificates. Since v0.9, an optional `ca.crl` key can also provide a CRL in PEM format for the server to verify against. +* `auth-tls-strict`: Defines if a wrong or incomplete configuration, eg missing secret with `ca.crt`, should forbid connection attempts. If `false`, the default value, a wrong or incomplete configuration will ignore the authentication config, allowing anonymous connection. If `true`, a strict configuration is used: all requests will be rejected with HTTP 495 or 496, or redirected to the error page if configured, until a proper `ca.crt` is provided. Strict configuration will only be used if `auth-tls-secret` has a secret name and `auth-tls-verify-client` is missing or is not configured as `off`. * `auth-tls-verify-client`: Optional configuration of Client Verification behavior. Supported values are `off`, `on`, `optional` and `optional_no_ca`. The default value is `on` if a valid secret is provided, `off` otherwise. * `ssl-fingerprint-lower`: Defines if the certificate fingerprint should be in lowercase hexadecimal digits. The default value is `false`, which uses uppercase digits. * `ssl-headers-prefix`: Configures which prefix should be used on HTTP headers. Since [RFC 6648](https://tools.ietf.org/html/rfc6648) `X-` prefix on unstandardized headers changed from a convention to deprecation. This configuration allows to select which pattern should be used on header names. diff --git a/pkg/common/ingress/controller/controller.go b/pkg/common/ingress/controller/controller.go index b24b94c6d..dc979f640 100644 --- a/pkg/common/ingress/controller/controller.go +++ b/pkg/common/ingress/controller/controller.go @@ -244,7 +244,9 @@ func (ic *GenericController) Start() { // CreateDefaultSSLCertificate ... func (ic *GenericController) CreateDefaultSSLCertificate() (path, hash string, crt *x509.Certificate) { - defCert, defKey := ssl.GetFakeSSLCert() + defCert, defKey := ssl.GetFakeSSLCert( + []string{"Acme Co"}, "Kubernetes Ingress Controller Fake Certificate", []string{"ingress.local"}, + ) c, err := ssl.AddOrUpdateCertAndKey("default-fake-certificate", defCert, defKey, []byte{}) if err != nil { glog.Fatalf("Error generating self signed certificate: %v", err) diff --git a/pkg/common/net/ssl/ssl.go b/pkg/common/net/ssl/ssl.go index 20f8fd637..14f609f9a 100644 --- a/pkg/common/net/ssl/ssl.go +++ b/pkg/common/net/ssl/ssl.go @@ -363,7 +363,7 @@ func AddOrUpdateDHParam(name string, dh []byte) (string, error) { // GetFakeSSLCert creates a Self Signed Certificate // Based in the code https://golang.org/src/crypto/tls/generate_cert.go -func GetFakeSSLCert() ([]byte, []byte) { +func GetFakeSSLCert(o []string, cn string, dns []string) (cert, key []byte) { var priv interface{} var err error @@ -388,8 +388,8 @@ func GetFakeSSLCert() ([]byte, []byte) { template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - Organization: []string{"Acme Co"}, - CommonName: "Kubernetes Ingress Controller Fake Certificate", + Organization: o, + CommonName: cn, }, NotBefore: notBefore, NotAfter: notAfter, @@ -397,16 +397,14 @@ func GetFakeSSLCert() ([]byte, []byte) { KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - DNSNames: []string{"ingress.local"}, + DNSNames: dns, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.(*rsa.PrivateKey).PublicKey, priv) if err != nil { glog.Fatalf("Failed to create fake certificate: %s", err) } - cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - - key := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey))}) - + cert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + key = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey))}) return cert, key } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 3fc92d9df..82763e239 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -32,6 +32,7 @@ import ( "github.com/jcmoraisjr/haproxy-ingress/pkg/acme" "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" configmapconverter "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/configmap" ingressconverter "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress" ingtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" @@ -127,7 +128,8 @@ func (hc *HAProxyController) configController() { Cache: hc.cache, AnnotationPrefix: hc.cfg.AnnPrefix, DefaultBackend: hc.cfg.DefaultService, - DefaultSSLFile: hc.createDefaultSSLFile(hc.cache), + DefaultSSLFile: hc.createDefaultSSLFile(), + FakeCAFile: hc.createFakeCAFile(), AcmeTrackTLSAnn: hc.cfg.AcmeTrackTLSAnn, } } @@ -153,9 +155,9 @@ func (hc *HAProxyController) startServices() { } } -func (hc *HAProxyController) createDefaultSSLFile(cache convtypes.Cache) (tlsFile convtypes.CrtFile) { +func (hc *HAProxyController) createDefaultSSLFile() (tlsFile convtypes.CrtFile) { if hc.cfg.DefaultSSLCertificate != "" { - tlsFile, err := cache.GetTLSSecretPath("", hc.cfg.DefaultSSLCertificate) + tlsFile, err := hc.cache.GetTLSSecretPath("", hc.cfg.DefaultSSLCertificate) if err == nil { return tlsFile } @@ -173,6 +175,19 @@ func (hc *HAProxyController) createDefaultSSLFile(cache convtypes.Cache) (tlsFil return tlsFile } +func (hc *HAProxyController) createFakeCAFile() (crtFile convtypes.CrtFile) { + fakeCA, _ := ssl.GetFakeSSLCert([]string{}, "Fake CA", []string{}) + fakeCAFile, err := ssl.AddCertAuth("fake-ca", fakeCA, []byte{}) + if err != nil { + glog.Fatalf("error generating fake CA: %v", err) + } + crtFile = convtypes.CrtFile{ + Filename: fakeCAFile.PemFileName, + SHA1Hash: fakeCAFile.PemSHA, + } + return crtFile +} + // OnStartedLeading ... // implements LeaderSubscriber func (hc *HAProxyController) OnStartedLeading(ctx context.Context) { diff --git a/pkg/converters/ingress/annotations/host.go b/pkg/converters/ingress/annotations/host.go index 2fc9bc741..8eb9cd94d 100644 --- a/pkg/converters/ingress/annotations/host.go +++ b/pkg/converters/ingress/annotations/host.go @@ -29,17 +29,24 @@ func (c *updater) buildHostAuthTLS(d *hostData) { if verify.Value == "off" { return } + tls := &d.host.TLS if cafile, crlfile, err := c.cache.GetCASecretPath(tlsSecret.Source.Namespace, tlsSecret.Value); err == nil { - d.host.TLS.CAFilename = cafile.Filename - d.host.TLS.CAHash = cafile.SHA1Hash - d.host.TLS.CRLFilename = crlfile.Filename - d.host.TLS.CRLHash = crlfile.SHA1Hash - d.host.TLS.CAVerifyOptional = verify.Value == "optional" || verify.Value == "optional_no_ca" - d.host.TLS.CAErrorPage = d.mapper.Get(ingtypes.HostAuthTLSErrorPage).Value + tls.CAFilename = cafile.Filename + tls.CAHash = cafile.SHA1Hash + tls.CRLFilename = crlfile.Filename + tls.CRLHash = crlfile.SHA1Hash } else { c.logger.Error("error building TLS auth config on %s: %v", tlsSecret.Source, err) - return } + if tls.CAFilename == "" && d.mapper.Get(ingtypes.HostAuthTLSStrict).Bool() { + // Here we have a misconfigured auth-tls and auth-tls-strict as `true`. + // Using a fake and self-generated CA so any connection attempt will fail with + // HTTP 495 (invalid crt) or 496 (crt wasn't provided) instead of allow the request. + tls.CAFilename = c.fakeCA.Filename + tls.CAHash = c.fakeCA.SHA1Hash + } + tls.CAVerifyOptional = verify.Value == "optional" || verify.Value == "optional_no_ca" + tls.CAErrorPage = d.mapper.Get(ingtypes.HostAuthTLSErrorPage).Value } func (c *updater) buildHostCertSigner(d *hostData) { diff --git a/pkg/converters/ingress/annotations/host_test.go b/pkg/converters/ingress/annotations/host_test.go new file mode 100644 index 000000000..4c9675cac --- /dev/null +++ b/pkg/converters/ingress/annotations/host_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2020 The HAProxy Ingress Controller Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "testing" + + ingtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" + hatypes "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/types" +) + +func TestAuthTLS(t *testing.T) { + testCases := []struct { + annDefault map[string]string + ann map[string]string + expected hatypes.HostTLSConfig + logging string + }{ + // 0 + {}, + // 1 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "caerr", + }, + expected: hatypes.HostTLSConfig{}, + logging: "ERROR error building TLS auth config on ingress 'system/ing1': secret not found: 'system/caerr'", + }, + // 2 + { + ann: map[string]string{ + ingtypes.HostAuthTLSStrict: "true", + }, + }, + // 3 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "caerr", + ingtypes.HostAuthTLSStrict: "true", + }, + expected: hatypes.HostTLSConfig{ + CAFilename: fakeCAFilename, + CAHash: fakeCAHash, + }, + logging: "ERROR error building TLS auth config on ingress 'system/ing1': secret not found: 'system/caerr'", + }, + // 4 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "cafile", + }, + expected: hatypes.HostTLSConfig{ + CAFilename: "/path/ca.crt", + CAHash: "c0e1bf73caf75d7353cf3ecdd20ceb2f6fa1cab1", + }, + }, + // 5 + { + ann: map[string]string{ + ingtypes.HostAuthTLSVerifyClient: "optional", + }, + }, + // 6 + { + ann: map[string]string{ + ingtypes.HostAuthTLSStrict: "true", + ingtypes.HostAuthTLSVerifyClient: "optional", + }, + }, + // 7 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "caerr", + ingtypes.HostAuthTLSStrict: "true", + ingtypes.HostAuthTLSVerifyClient: "optional", + }, + expected: hatypes.HostTLSConfig{ + CAFilename: fakeCAFilename, + CAHash: fakeCAHash, + CAVerifyOptional: true, + }, + logging: "ERROR error building TLS auth config on ingress 'system/ing1': secret not found: 'system/caerr'", + }, + // 8 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "cafile", + ingtypes.HostAuthTLSStrict: "true", + ingtypes.HostAuthTLSVerifyClient: "optional", + }, + expected: hatypes.HostTLSConfig{ + CAFilename: "/path/ca.crt", + CAHash: "c0e1bf73caf75d7353cf3ecdd20ceb2f6fa1cab1", + CAVerifyOptional: true, + }, + }, + // 9 + { + ann: map[string]string{ + ingtypes.HostAuthTLSSecret: "cafile", + ingtypes.HostAuthTLSVerifyClient: "optional", + }, + expected: hatypes.HostTLSConfig{ + CAFilename: "/path/ca.crt", + CAHash: "c0e1bf73caf75d7353cf3ecdd20ceb2f6fa1cab1", + CAVerifyOptional: true, + }, + }, + } + source := &Source{Namespace: "system", Name: "ing1", Type: "ingress"} + for i, test := range testCases { + c := setup(t) + c.cache.SecretCAPath = map[string]string{ + "system/cafile": "/path/ca.crt", + } + d := c.createHostData(source, test.ann, test.annDefault) + c.createUpdater().buildHostAuthTLS(d) + c.compareObjects("auth-tls", i, d.host.TLS, test.expected) + c.logger.CompareLogging(test.logging) + c.teardown() + } +} diff --git a/pkg/converters/ingress/annotations/updater.go b/pkg/converters/ingress/annotations/updater.go index ef2e2bea9..0d6491f78 100644 --- a/pkg/converters/ingress/annotations/updater.go +++ b/pkg/converters/ingress/annotations/updater.go @@ -41,6 +41,7 @@ func NewUpdater(haproxy haproxy.Config, options *ingtypes.ConverterOptions) Upda haproxy: haproxy, logger: options.Logger, cache: options.Cache, + fakeCA: options.FakeCAFile, } } @@ -48,6 +49,7 @@ type updater struct { haproxy haproxy.Config logger types.Logger cache convtypes.Cache + fakeCA convtypes.CrtFile } type globalData struct { diff --git a/pkg/converters/ingress/annotations/updater_test.go b/pkg/converters/ingress/annotations/updater_test.go index e511b92f2..9950a0ed1 100644 --- a/pkg/converters/ingress/annotations/updater_test.go +++ b/pkg/converters/ingress/annotations/updater_test.go @@ -23,6 +23,7 @@ import ( "testing" conv_helper "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/helper_test" + convtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/types" "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy" hatypes "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/types" types_helper "github.com/jcmoraisjr/haproxy-ingress/pkg/types/helper_test" @@ -34,6 +35,11 @@ import ( * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +const ( + fakeCAFilename = "/var/haproxy/ssl/fake-ca.crt" + fakeCAHash = "1" +) + type testConfig struct { t *testing.T haproxy haproxy.Config @@ -60,6 +66,10 @@ func (c *testConfig) createUpdater() *updater { haproxy: c.haproxy, cache: c.cache, logger: c.logger, + fakeCA: convtypes.CrtFile{ + Filename: fakeCAFilename, + SHA1Hash: fakeCAHash, + }, } } @@ -108,6 +118,15 @@ func (c *testConfig) createBackendMappingData( return d } +func (c *testConfig) createHostData(source *Source, ann, annDefault map[string]string) *hostData { + mapper := NewMapBuilder(c.logger, "", annDefault).NewMapper() + mapper.AddAnnotations(source, "/", ann) + return &hostData{ + host: &hatypes.Host{}, + mapper: mapper, + } +} + func (c *testConfig) compareObjects(name string, index int, actual, expected interface{}) { if !reflect.DeepEqual(actual, expected) { c.t.Errorf("%s on %d differs - expected: %v - actual: %v", name, index, expected, actual) diff --git a/pkg/converters/ingress/defaults.go b/pkg/converters/ingress/defaults.go index 1c6d19272..356d023c2 100644 --- a/pkg/converters/ingress/defaults.go +++ b/pkg/converters/ingress/defaults.go @@ -30,6 +30,7 @@ const ( func createDefaults() map[string]string { return map[string]string{ + types.HostAuthTLSStrict: "false", types.HostTimeoutClient: "50s", types.HostTimeoutClientFin: "50s", // diff --git a/pkg/converters/ingress/types/annotations.go b/pkg/converters/ingress/types/annotations.go index 93749ec5a..7b5f15910 100644 --- a/pkg/converters/ingress/types/annotations.go +++ b/pkg/converters/ingress/types/annotations.go @@ -21,6 +21,7 @@ const ( HostAppRoot = "app-root" HostAuthTLSErrorPage = "auth-tls-error-page" HostAuthTLSSecret = "auth-tls-secret" + HostAuthTLSStrict = "auth-tls-strict" HostAuthTLSVerifyClient = "auth-tls-verify-client" HostCertSigner = "cert-signer" HostServerAlias = "server-alias" @@ -38,6 +39,7 @@ var ( HostAppRoot: {}, HostAuthTLSErrorPage: {}, HostAuthTLSSecret: {}, + HostAuthTLSStrict: {}, HostAuthTLSVerifyClient: {}, HostCertSigner: {}, HostServerAlias: {}, diff --git a/pkg/converters/ingress/types/options.go b/pkg/converters/ingress/types/options.go index 00c00bf53..60ee307eb 100644 --- a/pkg/converters/ingress/types/options.go +++ b/pkg/converters/ingress/types/options.go @@ -28,6 +28,7 @@ type ConverterOptions struct { DefaultConfig func() map[string]string DefaultBackend string DefaultSSLFile convtypes.CrtFile + FakeCAFile convtypes.CrtFile AnnotationPrefix string AcmeTrackTLSAnn bool }