Skip to content

Commit

Permalink
Merge pull request #513 from jcmoraisjr/jm-auth-tls-strict
Browse files Browse the repository at this point in the history
Add auth-tls-strict configuration key
  • Loading branch information
jcmoraisjr authored Feb 3, 2020
2 parents 09b7707 + 919246d commit 883c073
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 27 deletions.
19 changes: 11 additions & 8 deletions docs/content/en/docs/configuration/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion pkg/common/ingress/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions pkg/common/net/ssl/ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -388,25 +388,23 @@ 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,

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
}
21 changes: 18 additions & 3 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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
}
Expand All @@ -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) {
Expand Down
21 changes: 14 additions & 7 deletions pkg/converters/ingress/annotations/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
136 changes: 136 additions & 0 deletions pkg/converters/ingress/annotations/host_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
2 changes: 2 additions & 0 deletions pkg/converters/ingress/annotations/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ func NewUpdater(haproxy haproxy.Config, options *ingtypes.ConverterOptions) Upda
haproxy: haproxy,
logger: options.Logger,
cache: options.Cache,
fakeCA: options.FakeCAFile,
}
}

type updater struct {
haproxy haproxy.Config
logger types.Logger
cache convtypes.Cache
fakeCA convtypes.CrtFile
}

type globalData struct {
Expand Down
19 changes: 19 additions & 0 deletions pkg/converters/ingress/annotations/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,6 +35,11 @@ import (
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

const (
fakeCAFilename = "/var/haproxy/ssl/fake-ca.crt"
fakeCAHash = "1"
)

type testConfig struct {
t *testing.T
haproxy haproxy.Config
Expand All @@ -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,
},
}
}

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/converters/ingress/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (

func createDefaults() map[string]string {
return map[string]string{
types.HostAuthTLSStrict: "false",
types.HostTimeoutClient: "50s",
types.HostTimeoutClientFin: "50s",
//
Expand Down
2 changes: 2 additions & 0 deletions pkg/converters/ingress/types/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -38,6 +39,7 @@ var (
HostAppRoot: {},
HostAuthTLSErrorPage: {},
HostAuthTLSSecret: {},
HostAuthTLSStrict: {},
HostAuthTLSVerifyClient: {},
HostCertSigner: {},
HostServerAlias: {},
Expand Down
Loading

0 comments on commit 883c073

Please sign in to comment.