Skip to content

Commit

Permalink
add auth-tls-strict configuration key
Browse files Browse the repository at this point in the history
`auth-tls-strict` allows to build a strict configuration if an invalid auth-tls configuration is provided, either due to misconfiguration or due to asynchronous events that's going to happen and will fix the temporarily broken config.

The strict config is made using a self-generated certificate authority in the `ca-file` option, which will lead to two desired effects:

1. A certificate, if provided by the client, will be identified by the server. HAProxy doesn't allow to configure `verify optional` without a `ca-file`. If this option wasn't used, we couldn't distinguish between a request with or without a certificate on configs whose crt is optional
2. The request will always be denied due to invalid certificate authority (when verify-client is `on`) or when a client certificate is used (when verify-client is `optional`) because, since the right configuration is broken, there is no way to know if the certificate is a valid one.
  • Loading branch information
jcmoraisjr committed Jan 31, 2020
1 parent 108006d commit 2fc72ce
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 15 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 @@ -409,13 +410,14 @@ 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-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-headers-prefix` | `Global` | `X-SSL` | |

Configure client authentication with X509 certificate. The following headers are
added to the request:
Expand All @@ -433,7 +435,8 @@ 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-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.
* `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 `auth-tls-secret` provides a secret name, `off` otherwise.
* `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.

See also:
Expand Down
15 changes: 15 additions & 0 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 @@ -128,6 +129,7 @@ func (hc *HAProxyController) configController() {
AnnotationPrefix: hc.cfg.AnnPrefix,
DefaultBackend: hc.cfg.DefaultService,
DefaultSSLFile: hc.createDefaultSSLFile(),
FakeCAFile: hc.createFakeCAFile(),
AcmeTrackTLSAnn: hc.cfg.AcmeTrackTLSAnn,
}
}
Expand Down Expand Up @@ -173,6 +175,19 @@ func (hc *HAProxyController) createDefaultSSLFile() (tlsFile convtypes.CrtFile)
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
1 change: 1 addition & 0 deletions pkg/converters/ingress/types/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type ConverterOptions struct {
DefaultConfig func() map[string]string
DefaultBackend string
DefaultSSLFile convtypes.CrtFile
FakeCAFile convtypes.CrtFile
AnnotationPrefix string
AcmeTrackTLSAnn bool
}

0 comments on commit 2fc72ce

Please sign in to comment.