Compare commits

..

16 Commits

Author SHA1 Message Date
Ludovic Fernandez
254dc38c3d Prepare release v1.7.16 2019-09-13 15:04:04 +02:00
mpl
24d084d7e6 implement Flusher and Hijacker for codeCatcher 2019-09-13 14:32:03 +02:00
Ludovic Fernandez
df5f530058 Prepare release v1.7.15 2019-09-12 18:10:05 +02:00
mpl
753d173965 error pages: do not buffer response when it's not an error 2019-09-12 16:20:05 +02:00
Daniel Tomcej
ffd1f122de Add TLS minversion constraint 2019-09-12 14:48:05 +02:00
Piotr Majkrzak
f98b57fdf4 Fix wrong handling of insecure tls auth forward ingress annotation 2019-09-12 11:44:05 +02:00
Damien Duportal
2d37f08864 Improve Access Logs Documentation page 2019-09-11 18:14:03 +02:00
Nicholas Wiersma
a7dbcc282c Consider default cert domain in certificate store
Co-authored-by: Nicolas Mengin <nmengin.pro@gmail.com>
2019-09-11 17:46:04 +02:00
David Dymko
f4f62e7fb3 Update Acme doc - Vultr Wildcard & Root 2019-09-09 09:26:04 +02:00
mpl
4cae8bcb10 Finish kubernetes throttling refactoring 2019-08-31 05:10:04 -07:00
Ben Weissmann
bee370ec6b Throttle Kubernetes config refresh 2019-08-30 03:16:04 -07:00
pitan
f1d016b893 Typo in basic auth usersFile label consul-catalog 2019-08-21 01:36:03 -07:00
Erik Wegner
4defbbe848 Kubernetes support for Auth.HeaderField 2019-08-21 01:30:05 -07:00
Ludovic Fernandez
f397342f16 Prepare release v1.7.14 2019-08-14 02:06:03 -07:00
Ludovic Fernandez
989a59cc29 Update to go1.12.8 2019-08-14 01:52:05 -07:00
Julien Levesy
c5b71592c8 Make hijackConnectionTracker.Close thread safe 2019-08-12 02:34:04 -07:00
26 changed files with 552 additions and 151 deletions

20
.semaphoreci/golang.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
curl -O https://dl.google.com/go/go1.12.linux-amd64.tar.gz
tar -xvf go1.12.linux-amd64.tar.gz
rm -rf go1.12.linux-amd64.tar.gz
sudo mkdir -p /usr/local/golang/1.12/go
sudo mv go /usr/local/golang/1.12/
sudo rm /usr/local/bin/go
sudo chmod +x /usr/local/golang/1.12/go/bin/go
sudo ln -s /usr/local/golang/1.12/go/bin/go /usr/local/bin/go
export GOROOT="/usr/local/golang/1.12/go"
export GOTOOLDIR="/usr/local/golang/1.12/go/pkg/tool/linux_amd64"
go version

View File

@@ -1,5 +1,35 @@
# Change Log
## [v1.7.16](https://github.com/containous/traefik/tree/v1.7.16) (2019-09-13)
[All Commits](https://github.com/containous/traefik/compare/v1.7.15...v1.7.16)
**Bug fixes:**
- **[middleware,websocket]** implement Flusher and Hijacker for codeCatcher ([#5376](https://github.com/containous/traefik/pull/5376) by [mpl](https://github.com/mpl))
## [v1.7.15](https://github.com/containous/traefik/tree/v1.7.15) (2019-09-12)
[All Commits](https://github.com/containous/traefik/compare/v1.7.14...v1.7.15)
**Bug fixes:**
- **[authentication,k8s/ingress]** Kubernetes support for Auth.HeaderField ([#5235](https://github.com/containous/traefik/pull/5235) by [ErikWegner](https://github.com/ErikWegner))
- **[k8s,k8s/ingress]** Finish kubernetes throttling refactoring ([#5269](https://github.com/containous/traefik/pull/5269) by [mpl](https://github.com/mpl))
- **[k8s]** Throttle Kubernetes config refresh ([#4716](https://github.com/containous/traefik/pull/4716) by [benweissmann](https://github.com/benweissmann))
- **[k8s]** Fix wrong handling of insecure tls auth forward ingress annotation ([#5319](https://github.com/containous/traefik/pull/5319) by [majkrzak](https://github.com/majkrzak))
- **[middleware]** error pages: do not buffer response when it&#39;s not an error ([#5285](https://github.com/containous/traefik/pull/5285) by [mpl](https://github.com/mpl))
- **[tls]** Consider default cert domain in certificate store ([#5353](https://github.com/containous/traefik/pull/5353) by [nrwiersma](https://github.com/nrwiersma))
- **[tls]** Add TLS minversion constraint ([#5356](https://github.com/containous/traefik/pull/5356) by [dtomcej](https://github.com/dtomcej))
**Documentation:**
- **[acme]** Update Acme doc - Vultr Wildcard &amp; Root ([#5320](https://github.com/containous/traefik/pull/5320) by [ddymko](https://github.com/ddymko))
- **[consulcatalog]** Typo in basic auth usersFile label consul-catalog ([#5230](https://github.com/containous/traefik/pull/5230) by [pitan](https://github.com/pitan))
- **[logs]** Improve Access Logs Documentation page ([#5238](https://github.com/containous/traefik/pull/5238) by [dduportal](https://github.com/dduportal))
## [v1.7.14](https://github.com/containous/traefik/tree/v1.7.14) (2019-08-14)
[All Commits](https://github.com/containous/traefik/compare/v1.7.13...v1.7.14)
**Bug fixes:**
- Update to go1.12.8 ([#5201](https://github.com/containous/traefik/pull/5201) by [ldez](https://github.com/ldez)). HTTP/2 Denial of Service [CVE-2019-9512](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9512) and [CVE-2019-9514](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9514)
- **[server]** Make hijackConnectionTracker.Close thread safe ([#5194](https://github.com/containous/traefik/pull/5194) by [jlevesy](https://github.com/jlevesy))
## [v1.7.13](https://github.com/containous/traefik/tree/v1.7.13) (2019-08-07)
[All Commits](https://github.com/containous/traefik/compare/v1.7.12...v1.7.13)

View File

@@ -13,7 +13,7 @@ You need to run the `binary` target. This will create binaries for Linux platfor
$ make binary
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
Sending build context to Docker daemon 295.3 MB
Step 0 : FROM golang:1.11-alpine
Step 0 : FROM golang:1.12-alpine
---> 8c6473912976
Step 1 : RUN go get github.com/golang/dep/cmd/dep
[...]

View File

@@ -76,7 +76,7 @@ test-integration: build ## run the integration tests
TEST_HOST=1 ./script/make.sh test-integration
validate: build ## validate code, vendor and autogen
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint validate-misspell validate-vendor validate-autogen
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-golint validate-misspell validate-vendor validate-autogen
build: dist
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .

View File

@@ -1365,7 +1365,9 @@ var _templatesKubernetesTmpl = []byte(`[backends]
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -1,4 +1,4 @@
FROM golang:1.11-alpine
FROM golang:1.12-alpine
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar ca-certificates tzdata \
@@ -6,7 +6,6 @@ RUN apk --update upgrade \
&& rm -rf /var/cache/apk/*
RUN go get golang.org/x/lint/golint \
&& go get github.com/kisielk/errcheck \
&& go get github.com/client9/misspell/cmd/misspell
# Which docker version to test on

View File

@@ -212,6 +212,12 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
}
}
// Thanks to SSLv3 being enabled by mistake in golang 1.12,
// If no minVersion is set, apply TLS1.0 as the minimum.
if entryPoint.TLS != nil && len(entryPoint.TLS.MinVersion) == 0 {
entryPoint.TLS.MinVersion = "VersionTLS10"
}
if entryPoint.TLS != nil && entryPoint.TLS.DefaultCertificate == nil && len(entryPoint.TLS.Certificates) > 0 {
log.Infof("No tls.defaultCertificate given for %s: using the first item in tls.certificates as a fallback.", entryPointName)
entryPoint.TLS.DefaultCertificate = &entryPoint.TLS.Certificates[0]

View File

@@ -12,6 +12,7 @@ import (
"github.com/containous/traefik/provider"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/provider/file"
"github.com/containous/traefik/tls"
"github.com/stretchr/testify/assert"
)
@@ -269,3 +270,69 @@ func TestInitACMEProvider(t *testing.T) {
})
}
}
func TestSetEffectiveConfigurationTLSMinVersion(t *testing.T) {
testCases := []struct {
desc string
provided EntryPoint
expected EntryPoint
}{
{
desc: "Entrypoint with no TLS",
provided: EntryPoint{
Address: ":80",
},
expected: EntryPoint{
Address: ":80",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
},
},
{
desc: "Entrypoint with TLS Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
},
{
desc: "Entrypoint with TLS without Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS10",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
EntryPoints: map[string]*EntryPoint{
"foo": &test.provided,
},
}
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, &test.expected, gc.EntryPoints["foo"])
})
}
}

View File

@@ -336,7 +336,7 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet |
| [Versio](https://www.versio.nl/domeinnamen) | `versio` | `VERSIO_USERNAME`, `VERSIO_PASSWORD` | YES |
| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | YES |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | YES |
| [Zone.ee](https://www.zone.ee) | `zoneee` | `ZONEEE_API_USER`, `ZONEEE_API_KEY` | YES |
- (1): more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/)

View File

@@ -130,10 +130,10 @@ Additional settings can be defined using Consul Catalog tags.
| `<prefix>.frontend.auth.basic=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). |
| `<prefix>.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.basic.users=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash`. |
| `<prefix>.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.digest.users=EXPR` | Sets digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. |
| `<prefix>.frontend.auth.digest.usersfile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.usersFile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. |
| `<prefix>.frontend.auth.forward.authResponseHeaders=EXPR` | Sets the forward authentication authResponseHeaders in CSV format: `X-Auth-User,X-Auth-Header` |
| `<prefix>.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. |

View File

@@ -73,6 +73,14 @@ See also [Kubernetes user guide](/user-guide/kubernetes).
#
# enablePassTLSCert = true
# Throttle how frequently we refresh our configuration from Ingresses when there
# are frequent changes.
#
# Optional
# Default: 0 (no throttling)
#
# throttleDuration = 10s
# Override default configuration template.
#
# Optional
@@ -210,7 +218,7 @@ infos:
serialnumber: true
```
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If at least one flag of the `infos` part is set, it will add a `X-Forwarded-Tls-Client-Cert-Infos` header that contains an escaped string composed of the client certificate data selected by the infos flags.
This infos part is composed like the following example (not escaped):
```Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2```
@@ -231,7 +239,7 @@ rateset:
```
<5> `traefik.ingress.kubernetes.io/rule-type`
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
<6> `traefik.ingress.kubernetes.io/service-weights`:
Service weights enable to split traffic across multiple backing services in a fine-grained manner.

View File

@@ -105,10 +105,10 @@ logLevel = "ERROR"
## Access Logs
Access logs are written when `[accessLog]` is defined.
By default it will write to stdout and produce logs in the textual [Common Log Format (CLF)](#clf-common-log-format), extended with additional fields.
Access logs are written when the entry `[accessLog]` is defined (or the command line flag `--accesslog`).
By default it writes to stdout and produces logs in the textual [Common Log Format (CLF)](#clf-common-log-format), extended with additional fields.
To enable access logs using the default settings just add the `[accessLog]` entry:
To enable access logs using the default settings, add the `[accessLog]` entry in your `traefik.toml` configuration file:
```toml
[accessLog]
@@ -175,21 +175,41 @@ format = "json" # Default: "common"
minDuration = "10ms"
```
To customize logs format, you must switch to the JSON format:
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
### Customize Fields
You can customize the fields written in the access logs.
The list of available fields is found below: [List of All Available Fields](#list-of-all-available-fields).
Each field has a "mode" which defines if it is written or not in the access log lines.
The possible values for the mode are:
* `keep`: the field and its value are written on the access log line. This is the default behavior.
* `drop`: the field is not written at all on the access log.
To customize the fields, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all fields (default is `keep`)
* OR Define the fields which does not follow the default mode
```toml
[accessLog]
filePath = "/path/to/access.log"
format = "json" # Default: "common"
[accessLog.filters]
# statusCodes keep only access logs with status codes in the specified range
#
# Optional
# Default: []
#
statusCodes = ["200", "300-302"]
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.fields]
@@ -206,6 +226,43 @@ format = "json" # Default: "common"
[accessLog.fields.names]
"ClientUsername" = "drop"
# ...
```
### Customize Headers
Access logs prints the headers of each request, as fields of the access log line.
You can customize which and how the headers are printed, likewise the other fields (see ["Customize Fields" section](#customize-fields)).
Each header has a "mode" which defines how it is written in the access log lines.
The possible values for the mode are:
* `keep`: the header and its value are written on the access log line. This is the default behavior.
* `drop`: the header is not written at all on the access log.
* `redacted`: the header is written, but its value is redacted to avoid leaking sensitive information.
To customize the headers, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all headers (default is `keep`)
* OR Define the headers which does not follow the default mode
!!! important
The headers are written with the prefix `request_` in the access log.
This prefix must not be included when specifying a header in the TOML configuration.
* Do: `"User-Agent" = "drop"`
* Don't: `"redacted_User-Agent" = "drop"`
```toml
[accessLog]
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.fields.headers]
# defaultMode
@@ -224,7 +281,7 @@ format = "json" # Default: "common"
# ...
```
### List of all available fields
### List of All Available Fields
| Field | Description |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -259,6 +316,8 @@ format = "json" # Default: "common"
| `Overhead` | The processing time overhead caused by Traefik. |
| `RetryAttempts` | The amount of attempts the request was retried. |
### Depreciation Notice
Deprecated way (before 1.4):
!!! danger "DEPRECATED"
@@ -272,14 +331,6 @@ Deprecated way (before 1.4):
accessLogsFile = "log/access.log"
```
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
## Log Rotation
Traefik will close and reopen its log files, assuming they're configured, on receipt of a USR1 signal.

View File

@@ -12,7 +12,7 @@ RUN yarn install
RUN npm run build
# BUILD
FROM golang:1.11-alpine as gobuild
FROM golang:1.12-alpine as gobuild
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar ca-certificates tzdata \

View File

@@ -74,25 +74,28 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
return
}
recorder := newResponseRecorder(w)
next.ServeHTTP(recorder, req)
catcher := newCodeCatcher(w, h.httpCodeRanges)
next.ServeHTTP(catcher, req)
if !catcher.isError {
return
}
// check the recorder code against the configured http status code ranges
for _, block := range h.httpCodeRanges {
if recorder.GetCode() >= block[0] && recorder.GetCode() <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", recorder.GetCode())
if catcher.code >= block[0] && catcher.code <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", catcher.code)
var query string
if len(h.backendQuery) > 0 {
query = "/" + strings.TrimPrefix(h.backendQuery, "/")
query = strings.Replace(query, "{status}", strconv.Itoa(recorder.GetCode()), -1)
query = strings.Replace(query, "{status}", strconv.Itoa(catcher.code), -1)
}
pageReq, err := newRequest(h.backendURL + query)
if err != nil {
log.Error(err)
w.WriteHeader(recorder.GetCode())
fmt.Fprint(w, http.StatusText(recorder.GetCode()))
w.WriteHeader(catcher.code)
fmt.Fprint(w, http.StatusText(catcher.code))
return
}
@@ -102,16 +105,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context()))
utils.CopyHeaders(w.Header(), recorderErrorPage.Header())
w.WriteHeader(recorder.GetCode())
w.WriteHeader(catcher.code)
w.Write(recorderErrorPage.GetBody().Bytes())
return
}
}
// did not catch a configured status code so proceed with the request
utils.CopyHeaders(w.Header(), recorder.Header())
w.WriteHeader(recorder.GetCode())
w.Write(recorder.GetBody().Bytes())
}
func newRequest(baseURL string) (*http.Request, error) {
@@ -129,6 +127,88 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil
}
// codeCatcher is a response writer that detects as soon as possible whether the
// response is a code within the ranges of codes it watches for. If it is, it
// simply drops the data from the response. Otherwise, it forwards it directly to
// the original client (its responseWriter) without any buffering.
type codeCatcher struct {
headerMap http.Header
code int
httpCodeRanges types.HTTPCodeRanges
firstWrite bool
isError bool
responseWriter http.ResponseWriter
err error
}
func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) *codeCatcher {
catcher := &codeCatcher{
headerMap: make(http.Header),
code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200.
responseWriter: rw,
httpCodeRanges: httpCodeRanges,
firstWrite: true,
}
return catcher
}
func (cc *codeCatcher) Header() http.Header {
if cc.headerMap == nil {
cc.headerMap = make(http.Header)
}
return cc.headerMap
}
func (cc *codeCatcher) Write(buf []byte) (int, error) {
if cc.err != nil {
return 0, cc.err
}
if !cc.firstWrite {
if cc.isError {
// We don't care about the contents of the response,
// since we want to serve the ones from the error page,
// so we just drop them.
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
for _, block := range cc.httpCodeRanges {
if cc.code >= block[0] && cc.code <= block[1] {
cc.isError = true
break
}
}
cc.firstWrite = false
if !cc.isError {
utils.CopyHeaders(cc.responseWriter.Header(), cc.Header())
cc.responseWriter.WriteHeader(cc.code)
} else {
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
func (cc *codeCatcher) WriteHeader(code int) {
cc.code = code
}
// Hijack hijacks the connection
func (cc *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cc.responseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", cc.responseWriter)
}
// Flush sends any buffered data to the client.
func (cc *codeCatcher) Flush() {
if flusher, ok := cc.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
type responseRecorder interface {
http.ResponseWriter
http.Flusher

View File

@@ -34,6 +34,18 @@ func TestHandler(t *testing.T) {
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusOK))
},
},
{
desc: "no error, but not a 200",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
backendCode: http.StatusPartialContent,
backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "My error page.")
}),
validate: func(t *testing.T, recorder *httptest.ResponseRecorder) {
assert.Equal(t, http.StatusPartialContent, recorder.Code, "HTTP status")
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusPartialContent))
},
},
{
desc: "in the range",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"net/http/httptrace"
"strconv"
"strings"
"testing"
@@ -35,7 +36,7 @@ func TestRetry(t *testing.T) {
desc: "no retry when max request attempts is one",
maxRequestAttempts: 1,
wantRetryAttempts: 0,
wantResponseStatus: http.StatusInternalServerError,
wantResponseStatus: http.StatusBadGateway,
amountFaultyEndpoints: 1,
},
{
@@ -56,7 +57,7 @@ func TestRetry(t *testing.T) {
desc: "max attempts exhausted delivers the 5xx response",
maxRequestAttempts: 3,
wantRetryAttempts: 2,
wantResponseStatus: http.StatusInternalServerError,
wantResponseStatus: http.StatusBadGateway,
amountFaultyEndpoints: 3,
},
}
@@ -82,17 +83,18 @@ func TestRetry(t *testing.T) {
t.Fatalf("Error creating load balancer: %s", err)
}
basePort := 33444
// out of range port
basePort := 1133444
for i := 0; i < test.amountFaultyEndpoints; i++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + string(basePort+i)))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + strconv.Itoa(basePort+i)))
}
// add the functioning server to the end of the load balancer list
loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
retryListener := &countingRetryListener{}
retry := NewRetry(test.maxRequestAttempts, loadBalancer, retryListener)
@@ -154,17 +156,18 @@ func TestRetryWebsocket(t *testing.T) {
t.Fatalf("Error creating load balancer: %s", err)
}
basePort := 33444
// out of range port
basePort := 1133444
for i := 0; i < test.amountFaultyEndpoints; i++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + string(basePort+i)))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + strconv.Itoa(basePort+i)))
}
// add the functioning server to the end of the load balancer list
loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
retryListener := &countingRetryListener{}
retry := NewRetry(test.maxRequestAttempts, loadBalancer, retryListener)

View File

@@ -16,6 +16,7 @@ import (
"time"
"github.com/cenk/backoff"
"github.com/containous/flaeg"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
@@ -68,6 +69,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"`
IngressEndpoint *IngressEndpoint `description:"Kubernetes Ingress Endpoint"`
ThrottleDuration flaeg.Duration `description:"Ingress refresh throttle duration"`
lastConfiguration safe.Safe
}
@@ -137,16 +139,29 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
return nil
}
}
throttleDuration := time.Duration(p.ThrottleDuration)
throttledChan := throttleEvents(throttleDuration, stop, eventsChan)
if throttledChan != nil {
eventsChan = throttledChan
}
for {
select {
case <-stop:
return nil
case event := <-eventsChan:
// Note that event is the *first* event that came in during this
// throttling interval -- if we're hitting our throttle, we may have
// dropped events. This is fine, because we don't treat different
// event types differently. But if we do in the future, we'll need to
// track more information about the dropped events.
log.Debugf("Received Kubernetes event kind %T", event)
templateObjects, err := p.loadIngresses(k8sClient)
if err != nil {
return err
}
if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) {
log.Debugf("Skipping Kubernetes event kind %T", event)
} else {
@@ -156,6 +171,11 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
Configuration: p.loadConfig(*templateObjects),
}
}
// If we're throttling, we sleep here for the throttle duration to
// enforce that we don't refresh faster than our throttle. time.Sleep
// returns immediately if p.ThrottleDuration is 0 (no throttle).
time.Sleep(throttleDuration)
}
}
}
@@ -599,6 +619,39 @@ func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, tem
return nil
}
func throttleEvents(throttleDuration time.Duration, stop chan bool, eventsChan <-chan interface{}) chan interface{} {
if throttleDuration == 0 {
return nil
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling)
eventsChanBuffered := make(chan interface{}, 1)
// Run a goroutine that reads events from eventChan and does a
// non-blocking write to pendingEvent. This guarantees that writing to
// eventChan will never block, and that pendingEvent will have
// something in it if there's been an event since we read from that channel.
go func() {
for {
select {
case <-stop:
return
case nextEvent := <-eventsChan:
select {
case eventsChanBuffered <- nextEvent:
default:
// We already have an event in eventsChanBuffered, so we'll
// do a refresh as soon as our throttle allows us to. It's fine
// to drop the event and keep whatever's in the buffer -- we
// don't do different things for different events
log.Debugf("Dropping event kind %T due to throttling", nextEvent)
}
}
}
}()
return eventsChanBuffered
}
func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) {
if len(pa.Path) == 0 {
return "", nil
@@ -940,12 +993,12 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
authSecretName := getStringValue(i.Annotations, annotationKubernetesAuthForwardTLSSecret, "")
if len(authSecretName) > 0 {
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
if authSecretCert != "" || authSecretKey != "" {
forwardAuth.TLS = &types.ClientTLS{
Cert: authSecretCert,
Key: authSecretKey,
@@ -953,10 +1006,20 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
}
if forwardAuth.TLS == nil && label.Has(i.Annotations, getAnnotationName(i.Annotations, annotationKubernetesAuthForwardTLSInsecure)) {
forwardAuth.TLS = &types.ClientTLS{
InsecureSkipVerify: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTLSInsecure, false),
}
}
return forwardAuth, nil
}
func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) {
if len(secretName) == 0 {
return "", "", nil
}
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
if err != nil {
return "", "", fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err)

View File

@@ -5,7 +5,7 @@ if [ -z "${VALIDATE_UPSTREAM:-}" ]; then
# are running more than one validate bundlescript
VALIDATE_REPO='https://github.com/containous/traefik.git'
VALIDATE_BRANCH='master'
VALIDATE_BRANCH='v1.7'
# Should not be needed for now O:)
# if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then

View File

@@ -3,9 +3,8 @@ set -e
# List of bundles to create when no argument is passed
DEFAULT_BUNDLES=(
validate-gofmt
validate-govet
generate
validate-gofmt
binary
test-unit

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "$BASH_SOURCE")/.validate"
IFS=$'\n'
files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) )
unset IFS
errors=()
failedErrcheck=$(errcheck .)
if [ "$failedErrcheck" ]; then
errors+=( "$failedErrcheck" )
fi
if [ ${#errors[@]} -eq 0 ]; then
echo 'Congratulations! All Go source files have been errchecked.'
else
{
echo "Errors from errcheck:"
for err in "${errors[@]}"; do
echo "$err"
done
echo
echo 'Please fix the above errors. You can test via "errcheck" and commit the result.'
echo
} >&2
false
fi

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "$BASH_SOURCE")/.validate"
IFS=$'\n'
files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) )
unset IFS
errors=()
for f in "${files[@]}"; do
# we use "git show" here to validate that what's committed passes go vet
failedVet=$(go tool vet -printf=false "$f")
if [ "$failedVet" ]; then
errors+=( "$failedVet" )
fi
done
if [ ${#errors[@]} -eq 0 ]; then
echo 'Congratulations! All Go source files have been vetted.'
else
{
echo "Errors from govet:"
for err in "${errors[@]}"; do
echo "$err"
done
echo
echo 'Please fix the above errors. You can test via "go vet" and commit the result.'
echo
} >&2
false
fi

View File

@@ -85,6 +85,8 @@ func (h *hijackConnectionTracker) Shutdown(ctx context.Context) error {
// Close close all the connections in the tracked connections list
func (h *hijackConnectionTracker) Close() {
h.lock.Lock()
defer h.lock.Unlock()
for conn := range h.conns {
if err := conn.Close(); err != nil {
log.Errorf("Error while closing Hijacked conn: %v", err)

View File

@@ -59,7 +59,9 @@
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -16,36 +16,41 @@ import (
var (
// MinVersion Map of allowed TLS minimum versions
MinVersion = map[string]uint16{
`VersionTLS10`: tls.VersionTLS10,
`VersionTLS11`: tls.VersionTLS11,
`VersionTLS12`: tls.VersionTLS12,
"VersionTLS10": tls.VersionTLS10,
"VersionTLS11": tls.VersionTLS11,
"VersionTLS12": tls.VersionTLS12,
"VersionTLS13": tls.VersionTLS13,
}
// CipherSuites Map of TLS CipherSuites from crypto/tls
// Available CipherSuites defined at https://golang.org/pkg/crypto/tls/#pkg-constants
CipherSuites = map[string]uint16{
`TLS_RSA_WITH_RC4_128_SHA`: tls.TLS_RSA_WITH_RC4_128_SHA,
`TLS_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
`TLS_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_RSA_WITH_AES_128_CBC_SHA,
`TLS_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_RSA_WITH_AES_256_CBC_SHA,
`TLS_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
`TLS_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
`TLS_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_ECDSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
`TLS_ECDHE_RSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
`TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
`TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
`TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
`TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
`TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
`TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
`TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
`TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
}
)

View File

@@ -2,6 +2,7 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"net"
"sort"
"strings"
@@ -47,6 +48,11 @@ func (c CertificateStore) GetAllDomains() []string {
allCerts = append(allCerts, domains)
}
}
// Get Default certificate
if c.DefaultCertificate != nil {
allCerts = append(allCerts, getCertificateDomains(c.DefaultCertificate)...)
}
return allCerts
}
@@ -115,6 +121,27 @@ func (c CertificateStore) ResetCache() {
}
}
func getCertificateDomains(cert *tls.Certificate) []string {
if cert == nil {
return nil
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil
}
var names []string
if len(x509Cert.Subject.CommonName) > 0 {
names = append(names, x509Cert.Subject.CommonName)
}
for _, san := range x509Cert.DNSNames {
names = append(names, san)
}
return names
}
// MatchDomain return true if a domain match the cert domain
func MatchDomain(domain string, certDomain string) bool {
if domain == certDomain {

View File

@@ -13,6 +13,90 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetAllDomains(t *testing.T) {
testCases := []struct {
desc string
staticCert string
dynamicCert string
defaultCert string
expectedDomains []string
}{
{
desc: "Empty Store, returns no domains",
staticCert: "",
dynamicCert: "",
defaultCert: "",
expectedDomains: nil,
},
{
desc: "Static cert domains",
staticCert: "snitest.com",
dynamicCert: "",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Dynamic cert domains",
staticCert: "",
dynamicCert: "snitest.com",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Default cert domains",
staticCert: "",
dynamicCert: "",
defaultCert: "snitest.com",
expectedDomains: []string{"snitest.com"},
},
{
desc: "All domains",
staticCert: "www.snitest.com",
dynamicCert: "*.snitest.com",
defaultCert: "snitest.com",
expectedDomains: []string{"www.snitest.com", "*.snitest.com", "snitest.com"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, false)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, false)
require.NoError(t, err)
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
}
var defaultCert *tls.Certificate
if test.defaultCert != "" {
cert, err := loadTestCert(test.defaultCert, false)
require.NoError(t, err)
defaultCert = cert
}
store := &CertificateStore{
DynamicCerts: safe.New(dynamicMap),
StaticCerts: safe.New(staticMap),
DefaultCertificate: defaultCert,
CertCache: cache.New(1*time.Hour, 10*time.Minute),
}
actual := store.GetAllDomains()
assert.Equal(t, test.expectedDomains, actual)
})
}
}
func TestGetBestCertificate(t *testing.T) {
testCases := []struct {
desc string
@@ -116,15 +200,15 @@ func TestGetBestCertificate(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
dynamicMap := map[string]*tls.Certificate{}
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, test.uppercase)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
require.NoError(t, err)