Repository: avenga/couper
Pull request #615 (2022-11-03 09:16:57)

Adding (m)TLS Support
Adding options to configure server tls with optional client certificate verification and per backend CA and client certificate configuration.

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/.gitignore b/.gitignore
index 0f6214c9..4ebc2c19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,7 @@
 /docs/website/node_modules
 /docs/website/.vscode/settings.json
 docs/website/.vite
+
+# tls
+/*.crt
+/*.key
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fef7a625..58422a96 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
 * **Added**
   * [`trim()` function](https://docs.couper.io/configuration/functions) ([#605](https://github.com/avenga/couper/pull/605))
   * OAuth2 client authentication methods (`token_endpoint_auth_method` values) `"client_secret_jwt"` and `"private_key_jwt"` including `jwt_signing_profile` block for [`oauth2`](https://docs.couper.io/configuration/block/oauth2_req_auth), [`beta_oauth2`](https://docs.couper.io/configuration/block/oauth2_ac) and [`oidc`](https://docs.couper.io/configuration/block/oidc) blocks ([#599](https://github.com/avenga/couper/pull/599))
+  * **mTLS** Support for [`server`](https://docs.couper.io/configuration/block/server_tls) and [`backend`](https://docs.couper.io/configuration/block/backend_tls) blocks ([#615](https://github.com/avenga/couper/pull/615))
 
 * **Changed**
   * Replaced the JWT library because the former library was no longer maintained ([#612](https://github.com/avenga/couper/pull/612))
diff --git a/Makefile b/Makefile
index f089ebfe..58197e33 100644
--- a/Makefile
+++ b/Makefile
@@ -52,3 +52,7 @@ test-coverage-show:
 	go tool cover -html=logging.coverage
 	go tool cover -html=server.coverage
 	go tool cover -html=main.coverage
+
+.PHONY: mtls-certificates
+mtls-certificates:
+	time go run internal/tls/cli/main.go
diff --git a/command/run.go b/command/run.go
index 639d2c67..8397676a 100644
--- a/command/run.go
+++ b/command/run.go
@@ -114,6 +114,7 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 		return err
 	}
 
+	// TODO: move to config validation
 	if config.Settings.SecureCookies != "" &&
 		config.Settings.SecureCookies != writer.SecureCookiesStrip {
 		return fmt.Errorf("invalid value for the -secure-cookies flag given: '%s' only 'strip' is supported", config.Settings.SecureCookies)
@@ -170,7 +171,10 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 		}
 	}
 
-	serverList, listenCmdShutdown := server.NewServerList(r.context, config.Context, logEntry, config.Settings, &timings, srvConf)
+	servers, listenCmdShutdown, err := server.NewServers(r.context, config.Context, logEntry, config.Settings, &timings, srvConf)
+	if err != nil {
+		return err
+	}
 	var tlsServer []*http.Server
 
 	for mappedListenPort := range tlsDevPorts {
@@ -179,7 +183,7 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 		}
 	}
 
-	for _, srv := range serverList {
+	for _, srv := range servers {
 		if listenErr := srv.Listen(); listenErr != nil {
 			return listenErr
 		}
diff --git a/config/backend.go b/config/backend.go
index 1a71bc95..eff42cab 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -12,21 +12,20 @@ var (
 	_ BackendReference = &Backend{}
 	_ Body             = &Backend{}
 	_ Inline           = &Backend{}
-
-	BackendInlineSchema = Backend{}.Schema(true)
 )
 
 // Backend represents the <Backend> object.
 type Backend struct {
-	DisableCertValidation  bool       `hcl:"disable_certificate_validation,optional" docs:"Disables the peer certificate validation. Must not be used in backend refinement."`
-	DisableConnectionReuse bool       `hcl:"disable_connection_reuse,optional" docs:"Disables reusage of connections to the origin. Must not be used in backend refinement."`
-	Health                 *Health    `hcl:"beta_health,block"`
-	HTTP2                  bool       `hcl:"http2,optional" docs:"Enables the HTTP2 support. Must not be used in backend refinement."`
-	MaxConnections         int        `hcl:"max_connections,optional" docs:"The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin. Must not be used in backend refinement." default:"0"`
-	Name                   string     `hcl:"name,label,optional"`
-	OpenAPI                *OpenAPI   `hcl:"openapi,block"`
-	RateLimits             RateLimits `hcl:"beta_rate_limit,block"`
-	Remain                 hcl.Body   `hcl:",remain"`
+	DisableCertValidation  bool        `hcl:"disable_certificate_validation,optional" docs:"Disables the peer certificate validation. Must not be used in backend refinement."`
+	DisableConnectionReuse bool        `hcl:"disable_connection_reuse,optional" docs:"Disables reusage of connections to the origin. Must not be used in backend refinement."`
+	Health                 *Health     `hcl:"beta_health,block"`
+	HTTP2                  bool        `hcl:"http2,optional" docs:"Enables the HTTP2 support. Must not be used in backend refinement."`
+	MaxConnections         int         `hcl:"max_connections,optional" docs:"The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin. Must not be used in backend refinement." default:"0"`
+	Name                   string      `hcl:"name,label,optional"`
+	OpenAPI                *OpenAPI    `hcl:"openapi,block"`
+	RateLimits             RateLimits  `hcl:"beta_rate_limit,block"`
+	Remain                 hcl.Body    `hcl:",remain"`
+	TLS                    *BackendTLS `hcl:"tls,block"`
 }
 
 // Reference implements the <BackendReference> interface.
diff --git a/config/body/body.go b/config/body/body.go
index f54d3fcc..e84871a6 100644
--- a/config/body/body.go
+++ b/config/body/body.go
@@ -46,6 +46,15 @@ func BlocksOfType(body *hclsyntax.Body, blockType string) []*hclsyntax.Block {
 	return blocks
 }
 
+func AttributeByName(body *hclsyntax.Body, needle string) *hclsyntax.Attribute {
+	for _, attr := range body.Attributes {
+		if attr.Name == needle {
+			return attr
+		}
+	}
+	return nil
+}
+
 func RenameAttribute(body *hclsyntax.Body, old, new string) {
 	if attr, ok := body.Attributes[old]; ok {
 		attr.Name = new
diff --git a/config/certificate.go b/config/certificate.go
new file mode 100644
index 00000000..dcbbc846
--- /dev/null
+++ b/config/certificate.go
@@ -0,0 +1,17 @@
+package config
+
+type ClientCertificate struct {
+	Name     string `hcl:",label,optional"`
+	CA       string `hcl:"ca_certificate,optional" docs:"Public part of the certificate authority in DER or PEM format."`
+	CAFile   string `hcl:"ca_certificate_file,optional" docs:"Public part of the certificate authority file in DER or PEM format."`
+	Leaf     string `hcl:"leaf_certificate,optional" docs:"Public part of the client certificate in DER or PEM format."`
+	LeafFile string `hcl:"leaf_certificate_file,optional" docs:"Public part of the client certificate file in DER or PEM format."`
+}
+
+type ServerCertificate struct {
+	Name           string `hcl:",label,optional"`
+	PublicKey      string `hcl:"public_key,optional" docs:"Public part of the certificate in DER or PEM format."`
+	PublicKeyFile  string `hcl:"public_key_file,optional" docs:"Public part of the certificate file in DER or PEM format."`
+	PrivateKey     string `hcl:"private_key,optional" docs:"Private part of the certificate in DER or PEM format."`
+	PrivateKeyFile string `hcl:"private_key_file,optional" docs:"Private part of the certificate file in DER or PEM format."`
+}
diff --git a/config/configload/load.go b/config/configload/load.go
index ae4131d0..180ffb28 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -42,6 +42,7 @@ const (
 	server          = "server"
 	settings        = "settings"
 	spa             = "spa"
+	tls             = "tls"
 	tokenRequest    = "beta_token_request"
 	// defaultNameLabel maps the hcl label attr 'name'.
 	defaultNameLabel = "default"
diff --git a/config/configload/merge.go b/config/configload/merge.go
index 09551dcd..9f4f5ee4 100644
--- a/config/configload/merge.go
+++ b/config/configload/merge.go
@@ -18,9 +18,10 @@ const (
 	errUniqueLabels     = "All %s blocks must have unique labels."
 )
 
+type namedBlocks map[string]*hclsyntax.Block
+
 func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block) (hclsyntax.Blocks, error) {
 	type (
-		namedBlocks   map[string]*hclsyntax.Block
 		apiDefinition struct {
 			labels          []string
 			typeRange       hcl.Range
@@ -50,9 +51,16 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 			attributes      hclsyntax.Attributes
 			blocks          namedBlocks
 		}
-		namedAPIs        map[string]*apiDefinition
-		namedSPAs        map[string]*spaDefinition
-		namedFiles       map[string]*filesDefinition
+
+		tlsDefinition struct {
+			*hclsyntax.Block
+			blocks namedBlocks
+		}
+
+		namedAPIs  map[string]*apiDefinition
+		namedSPAs  map[string]*spaDefinition
+		namedFiles map[string]*filesDefinition
+
 		serverDefinition struct {
 			labels          []string
 			typeRange       hcl.Range
@@ -60,11 +68,12 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 			openBraceRange  hcl.Range
 			closeBraceRange hcl.Range
 			attributes      hclsyntax.Attributes
+			apis            namedAPIs
 			blocks          namedBlocks
 			endpoints       namedBlocks
-			apis            namedAPIs
-			spas            namedSPAs
 			files           namedFiles
+			spas            namedSPAs
+			tls             *tlsDefinition
 		}
 
 		servers map[string]*serverDefinition
@@ -89,6 +98,9 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 				attributes           = hclsyntax.Attributes
 				blocks[<name>]       = hclsyntax.Block (cors)
 			}
+			tls = {
+				blocks[<name>]       = hclsyntax.Block (server_certificate|client_certificate)
+			}
 		}
 	*/
 
@@ -293,6 +305,25 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 					for _, subBlock := range block.Body.Blocks {
 						results[serverKey].files[filesKey].blocks[subBlock.Type] = subBlock
 					}
+				} else if block.Type == tls {
+					if results[serverKey].tls == nil {
+						results[serverKey].tls = &tlsDefinition{
+							Block:  block,
+							blocks: make(namedBlocks),
+						}
+					}
+
+					for name, attr := range block.Body.Attributes {
+						results[serverKey].tls.Body.Attributes[name] = attr
+					}
+
+					for _, subBlock := range block.Body.Blocks {
+						blockKey := ""
+						if len(subBlock.Labels) > 0 {
+							blockKey = subBlock.Labels[0]
+						}
+						results[serverKey].tls.blocks[blockKey] = subBlock
+					}
 				} else {
 					results[serverKey].blocks[block.Type] = block
 				}
@@ -302,30 +333,30 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 
 	var mergedServers hclsyntax.Blocks
 
-	for _, serverBlock := range results {
+	for _, name := range getSortedMapKeys(results) {
 		var serverBlocks hclsyntax.Blocks
-
-		for _, b := range serverBlock.blocks {
-			serverBlocks = append(serverBlocks, b)
+		serverBlock := results[name]
+		for _, blockName := range getSortedMapKeys(serverBlock.blocks) {
+			serverBlocks = append(serverBlocks, serverBlock.blocks[blockName])
 		}
 
-		for _, b := range serverBlock.endpoints {
-			serverBlocks = append(serverBlocks, b)
+		for _, blockName := range getSortedMapKeys(serverBlock.endpoints) {
+			serverBlocks = append(serverBlocks, serverBlock.endpoints[blockName])
 		}
 
 		for _, apiBlock := range serverBlock.apis {
 			var apiBlocks hclsyntax.Blocks
 
-			for _, b := range apiBlock.blocks {
-				apiBlocks = append(apiBlocks, b)
+			for _, blockName := range getSortedMapKeys(apiBlock.blocks) {
+				apiBlocks = append(apiBlocks, apiBlock.blocks[blockName])
 			}
 
-			for _, b := range apiBlock.endpoints {
-				apiBlocks = append(apiBlocks, b)
+			for _, blockName := range getSortedMapKeys(apiBlock.endpoints) {
+				apiBlocks = append(apiBlocks, apiBlock.endpoints[blockName])
 			}
 
-			for _, b := range apiBlock.errorHandler {
-				apiBlocks = append(apiBlocks, b)
+			for _, blockName := range getSortedMapKeys(apiBlock.errorHandler) {
+				apiBlocks = append(apiBlocks, apiBlock.errorHandler[blockName])
 			}
 
 			mergedAPI := &hclsyntax.Block{
@@ -344,11 +375,12 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 			serverBlocks = append(serverBlocks, mergedAPI)
 		}
 
-		for _, spaBlock := range serverBlock.spas {
+		for _, blockName := range getSortedMapKeys(serverBlock.spas) {
+			spaBlock := serverBlock.spas[blockName]
 			var spaBlocks hclsyntax.Blocks
 
-			for _, b := range spaBlock.blocks {
-				spaBlocks = append(spaBlocks, b)
+			for _, bn := range getSortedMapKeys(spaBlock.blocks) {
+				spaBlocks = append(spaBlocks, spaBlock.blocks[bn])
 			}
 
 			mergedSPA := &hclsyntax.Block{
@@ -367,11 +399,12 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 			serverBlocks = append(serverBlocks, mergedSPA)
 		}
 
-		for _, filesBlock := range serverBlock.files {
+		for _, blockName := range getSortedMapKeys(serverBlock.files) {
+			filesBlock := serverBlock.files[blockName]
 			var filesBlocks hclsyntax.Blocks
 
-			for _, b := range filesBlock.blocks {
-				filesBlocks = append(filesBlocks, b)
+			for _, bn := range getSortedMapKeys(filesBlock.blocks) {
+				filesBlocks = append(filesBlocks, filesBlock.blocks[bn])
 			}
 
 			mergedFiles := &hclsyntax.Block{
@@ -390,6 +423,24 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 			serverBlocks = append(serverBlocks, mergedFiles)
 		}
 
+		if serverBlock.tls != nil {
+			var tlsCertificateBlocks hclsyntax.Blocks
+			for _, blockName := range getSortedMapKeys(serverBlock.tls.blocks) {
+				tlsCertificateBlocks = append(tlsCertificateBlocks, serverBlock.tls.blocks[blockName])
+			}
+			serverBlocks = append(serverBlocks, &hclsyntax.Block{
+				Type: tls,
+				Body: &hclsyntax.Body{
+					Attributes: serverBlock.tls.Body.Attributes,
+					Blocks:     tlsCertificateBlocks,
+				},
+				TypeRange:       serverBlock.tls.TypeRange,
+				LabelRanges:     serverBlock.tls.LabelRanges,
+				OpenBraceRange:  serverBlock.tls.OpenBraceRange,
+				CloseBraceRange: serverBlock.tls.CloseBraceRange,
+			})
+		}
+
 		mergedServer := &hclsyntax.Block{
 			Type:   server,
 			Labels: serverBlock.labels,
@@ -410,18 +461,15 @@ func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block)
 }
 
 func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, map[string]*hclsyntax.Block, error) {
-	type data map[string]*hclsyntax.Block
-	type list map[string]data
-
-	definitionsBlock := make(list)
-	proxiesList := make(data)
+	definitionsBlock := make(map[string]namedBlocks)
+	proxiesList := make(namedBlocks)
 
 	for _, body := range bodies {
 		for _, outerBlock := range body.Blocks {
 			if outerBlock.Type == definitions {
 				for _, innerBlock := range outerBlock.Body.Blocks {
 					if definitionsBlock[innerBlock.Type] == nil {
-						definitionsBlock[innerBlock.Type] = make(data)
+						definitionsBlock[innerBlock.Type] = make(namedBlocks)
 					}
 
 					if innerBlock.Type == backend {
@@ -483,9 +531,9 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, map[string]*h
 
 	var blocks []*hclsyntax.Block
 
-	for _, labels := range definitionsBlock {
-		for _, block := range labels {
-			blocks = append(blocks, block)
+	for _, name := range getSortedMapKeys(definitionsBlock) {
+		for _, label := range getSortedMapKeys(definitionsBlock[name]) {
+			blocks = append(blocks, definitionsBlock[name][label])
 		}
 	}
 
@@ -708,3 +756,12 @@ func newErrorHandlerKey(block *hclsyntax.Block) (key string) {
 
 	return strings.Join(sorted, errorHandlerLabelSep)
 }
+
+func getSortedMapKeys[K string, V any](m map[K]V) []string {
+	var result []string
+	for k := range m {
+		result = append(result, string(k))
+	}
+	sort.Strings(result)
+	return result
+}
diff --git a/config/configload/validate.go b/config/configload/validate.go
index 71afbdb7..fdee659d 100644
--- a/config/configload/validate.go
+++ b/config/configload/validate.go
@@ -10,6 +10,7 @@ import (
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/zclconf/go-cty/cty"
 
+	bodySyntax "github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/utils"
 )
@@ -64,6 +65,11 @@ func validateBody(body *hclsyntax.Body, afterMerge bool) error {
 						}
 						uniqueBackends[label] = struct{}{}
 					}
+
+					if err := validateBackendTLS(innerBlock); err != nil {
+						return err
+					}
+
 				case "basic_auth", "beta_oauth2", "oidc", "saml":
 					err := checkAC(uniqueACs, label, labelRange, afterMerge)
 					if err != nil {
@@ -110,8 +116,8 @@ func validateBody(body *hclsyntax.Body, afterMerge bool) error {
 						return err
 					}
 				} else if innerBlock.Type == api {
-					apiBasePath, err := getBasePath(innerBlock, afterMerge)
-					if err != nil {
+					var apiBasePath string
+					if apiBasePath, err = getBasePath(innerBlock, afterMerge); err != nil {
 						return err
 					}
 
@@ -306,3 +312,21 @@ func validMethods(methods []string, attr *hclsyntax.Attribute) error {
 
 	return nil
 }
+
+// validateBackendTLS determines irrational backend certificate configuration.
+func validateBackendTLS(block *hclsyntax.Block) error {
+	validateCertAttr := bodySyntax.AttributeByName(block.Body, "disable_certificate_validation")
+	if tlsBlocks := bodySyntax.BlocksOfType(block.Body, "tls"); validateCertAttr != nil && len(tlsBlocks) > 0 {
+		var attrName string
+		if caCert := bodySyntax.AttributeByName(tlsBlocks[0].Body, "server_ca_certificate"); caCert != nil {
+			attrName = caCert.Name
+		} else if caCertFile := bodySyntax.AttributeByName(tlsBlocks[0].Body, "server_ca_certificate_file"); caCertFile != nil {
+			attrName = caCertFile.Name
+		}
+		if attrName != "" {
+			r := tlsBlocks[0].Range()
+			return newDiagErr(&r, fmt.Sprintf("configured 'disable_certificate_validation' along with '%s' attribute", attrName))
+		}
+	}
+	return nil
+}
diff --git a/config/generate/main.go b/config/generate/main.go
index 824add62..8dd4dede 100644
--- a/config/generate/main.go
+++ b/config/generate/main.go
@@ -73,6 +73,7 @@ func main() {
 	for _, impl := range []interface{}{
 		&config.API{},
 		&config.Backend{},
+		&config.BackendTLS{},
 		&config.BasicAuth{},
 		&config.CORS{},
 		&config.Defaults{},
@@ -92,6 +93,8 @@ func main() {
 		&config.Response{},
 		&config.SAML{},
 		&config.Server{},
+		&config.ClientCertificate{},
+		&config.ServerCertificate{},
 		&config.Settings{},
 		&config.Spa{},
 		&config.TokenRequest{},
@@ -105,11 +108,13 @@ func main() {
 		if _, exists := blockNamesMap[blockName]; exists {
 			blockName = blockNamesMap[blockName]
 		}
+
 		result := entry{
 			Name: blockName,
 			URL:  strings.ToLower(basePath + blockName),
 			Type: "block",
 		}
+
 		result.ID = result.URL
 
 		var fields []reflect.StructField
diff --git a/config/runtime/backend.go b/config/runtime/backend.go
index c7d5bbcd..00fc3d22 100644
--- a/config/runtime/backend.go
+++ b/config/runtime/backend.go
@@ -58,12 +58,12 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx *hclsyntax.Body, log *logru
 		return nil, diags
 	}
 
+	var err error
 	if beConf.Name == "" {
-		name, err := getBackendName(evalCtx, backendCtx)
+		beConf.Name, err = getBackendName(evalCtx, backendCtx)
 		if err != nil {
 			return nil, err
 		}
-		beConf.Name = name
 	}
 
 	tc := &transport.Config{
@@ -76,17 +76,24 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx *hclsyntax.Body, log *logru
 		MaxConnections:         beConf.MaxConnections,
 	}
 
+	tc.CACertificate, tc.ClientCertificate, err = transport.ReadCertificates(beConf.TLS)
+	if err != nil {
+		return nil, hcl.Diagnostics{&hcl.Diagnostic{
+			Severity: hcl.DiagError,
+			Summary:  err.(errors.GoError).LogError(),
+			Subject:  &backendCtx.SrcRange,
+		}}
+	}
+
 	if len(beConf.RateLimits) > 0 {
 		if strings.HasPrefix(beConf.Name, "anonymous_") {
-			// TODO remove " (%q)"?
-			return nil, fmt.Errorf("anonymous backend (%q) cannot define 'beta_rate_limit' block(s)", beConf.Name)
+			return nil, fmt.Errorf("anonymous backend '%s' cannot define 'beta_rate_limit' block(s)", beConf.Name)
 		}
 
-		rateLimits, err := ratelimit.ConfigureRateLimits(conf.Context, beConf.RateLimits, log)
+		tc.RateLimits, err = ratelimit.ConfigureRateLimits(conf.Context, beConf.RateLimits, log)
 		if err != nil {
 			return nil, err
 		}
-		tc.RateLimits = rateLimits
 	}
 
 	options := &transport.BackendOptions{}
@@ -108,7 +115,6 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx *hclsyntax.Body, log *logru
 			return nil, fmt.Errorf("missing origin for backend %q", beConf.Name)
 		}
 
-		var err error
 		options.HealthCheck, err = config.NewHealthCheck(origin.AsString(), beConf.Health, conf)
 		if err != nil {
 			return nil, err
@@ -121,7 +127,8 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx *hclsyntax.Body, log *logru
 	} {
 		blocks := hclbody.BlocksOfType(backendCtx, blockType)
 		for _, block := range blocks {
-			requestAuthorizer, err := newRequestAuthorizer(evalCtx, block, log, conf, memStore)
+			var requestAuthorizer transport.RequestAuthorizer
+			requestAuthorizer, err = newRequestAuthorizer(evalCtx, block, log, conf, memStore)
 			if err != nil {
 				return nil, err
 			}
diff --git a/config/runtime/server/options.go b/config/runtime/server/options.go
index 7de81482..00207046 100644
--- a/config/runtime/server/options.go
+++ b/config/runtime/server/options.go
@@ -23,6 +23,7 @@ type Options struct {
 	SPABasePaths   []string
 	SrvBasePath    string
 	ServerName     string
+	TLS            *config.ServerTLS
 }
 
 func NewServerOptions(conf *config.Server, logger *logrus.Entry) (*Options, error) {
@@ -34,6 +35,8 @@ func NewServerOptions(conf *config.Server, logger *logrus.Entry) (*Options, erro
 		return options, nil
 	}
 
+	options.TLS = conf.TLS
+
 	options.FilesErrTpls = make([]*errors.Template, len(conf.Files))
 	for i := range conf.Files {
 		options.FilesErrTpls[i] = errors.DefaultHTML
diff --git a/config/server.go b/config/server.go
index 1a6b16cb..a02cd993 100644
--- a/config/server.go
+++ b/config/server.go
@@ -23,6 +23,7 @@ type Server struct {
 	Name                 string      `hcl:"name,label,optional"`
 	Remain               hcl.Body    `hcl:",remain"`
 	SPAs                 SPAs        `hcl:"spa,block"`
+	TLS                  *ServerTLS  `hcl:"tls,block"`
 }
 
 // Servers represents a list of <Server> objects.
diff --git a/config/tls.go b/config/tls.go
new file mode 100644
index 00000000..920e8b8a
--- /dev/null
+++ b/config/tls.go
@@ -0,0 +1,17 @@
+package config
+
+type ServerTLS struct {
+	Ocsp               bool                 `hcl:"ocsp,optional"`
+	OcspTTL            string               `hcl:"ocsp_ttl,optional" type:"duration" default:"12h"`
+	ClientCertificate  []*ClientCertificate `hcl:"client_certificate,block"`
+	ServerCertificates []*ServerCertificate `hcl:"server_certificate,block"`
+}
+
+type BackendTLS struct {
+	ServerCertificate     string `hcl:"server_ca_certificate,optional" docs:"Public part of the certificate authority in DER or PEM format."`
+	ServerCertificateFile string `hcl:"server_ca_certificate_file,optional" docs:"Public part of the certificate authority file in DER or PEM format."`
+	ClientCertificate     string `hcl:"client_certificate,optional" docs:"Public part of the client certificate in DER or PEM format."`
+	ClientCertificateFile string `hcl:"client_certificate_file,optional" docs:"Public part of the client certificate file in DER or PEM format."`
+	ClientPrivateKey      string `hcl:"client_private_key,optional" docs:"Private part of the client certificate in DER or PEM format. Required to complete an mTLS handshake."`
+	ClientPrivateKeyFile  string `hcl:"client_private_key_file,optional" docs:"Private part of the client certificate file in DER or PEM format. Required to complete an mTLS handshake."`
+}
diff --git a/docs/website/content/2.configuration/0.command-line.md b/docs/website/content/2.configuration/0.command-line.md
index f9f999c5..fc57028c 100644
--- a/docs/website/content/2.configuration/0.command-line.md
+++ b/docs/website/content/2.configuration/0.command-line.md
@@ -5,7 +5,7 @@ Couper is build as binary called `couper` with the following commands:
 | Command   | Description                                                                                                                                   |
 |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------|
 | `run`     | Start the server with given configuration file.                                                                                               |
-|           | **Note:** `run` options can also be configured with [settings](block/settings) or related environment variables. |
+|           | **Note:** `run` options can also be configured with [settings](/configuration/block/settings) or related environment variables. |
 | `help`    | Print the usage for the given command: `help run`                                                                                             |
 | `verify`  | Verify the syntax of the given configuration file.                                                                                            |
 | `version` | Print the current version and build information.                                                                                              |
@@ -27,11 +27,11 @@ Couper is build as binary called `couper` with the following commands:
 | `-pprof`             | –            | `COUPER_PPROF`             | Enables profiling. |
 | `-pprof-port`        | `6060`       | `COUPER_PPROF_PORT`        | Port for profiling interface |
 
-> Note: `log-format`, `log-level` and `log-pretty` also map to [settings](block/settings).
+> Note: `log-format`, `log-level` and `log-pretty` also map to [settings](/configuration/block/settings).
 
 > Note: Couper can be started with multiple `-f <file>` and `-d <dir>` arguments.
 
-Files in the `-d <dir>` are loaded in alphabetical order. Blocks and attributes defined in later files may override those defined earlier. See [Merging](multiple-files) for details.
+Files in the `-d <dir>` are loaded in alphabetical order. Blocks and attributes defined in later files may override those defined earlier. See [Merging](/configuration/multiple-files) for details.
 
 ::duration
 ---
@@ -61,7 +61,7 @@ DEBU[0000] loaded files … […/global.hcl …/conf/a.hcl …/conf/b.hcl …/co
 
 | Argument                | Default      | Environment                   | Description                                                                                                                                                                                                                           |
 |:------------------------|:-------------|:------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `-accept-forwarded-url` | `""`         | `COUPER_ACCEPT_FORWARDED_URL` | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](variables#request) `url`, `origin`, `protocol`, `host`, `port`. Comma-separated list of values. Valid values: `proto`, `host`, `port` |
+| `-accept-forwarded-url` | `""`         | `COUPER_ACCEPT_FORWARDED_URL` | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](/configuration/variables#request) `url`, `origin`, `protocol`, `host`, `port`. Comma-separated list of values. Valid values: `proto`, `host`, `port` |
 | `-health-path`          | `/healthz`   | `COUPER_HEALTH_PATH`          | Path for health-check requests for all servers and ports. |
 | `-https-dev-proxy`      | `""`         | `COUPER_HTTPS_DEV_PROXY`      | List of TLS port mappings to define the TLS listen port and the target one. A self-signed certificate will be generated on the fly based on the given hostname.                                                                           |
 | `-beta-metrics`         | -            | `COUPER_BETA_METRICS`         | Option to enable the Prometheus [metrics](/observation/metrics) exporter.                                                                                                                                                                     |
diff --git a/docs/website/content/2.configuration/3.modifiers.md b/docs/website/content/2.configuration/3.modifiers.md
index f4f1c285..00e168a0 100644
--- a/docs/website/content/2.configuration/3.modifiers.md
+++ b/docs/website/content/2.configuration/3.modifiers.md
@@ -8,9 +8,9 @@ executed ordered as follows:
 
 | Modifier                 | Contexts                                                                                                                               | Description                                                       |
 |:-------------------------|:---------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|
-| `remove_request_headers` | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | list of request header to be removed from the upstream request.   |
-| `set_request_headers`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to set request header in the upstream request. |
-| `add_request_headers`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to add request header to the upstream request. |
+| `remove_request_headers` | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | list of request header to be removed from the upstream request.   |
+| `set_request_headers`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to set request header in the upstream request. |
+| `add_request_headers`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to add request header to the upstream request. |
 
 All `*_request_headers` are executed from: `endpoint`, `proxy`, `backend` and `error_handler`.
 
@@ -22,9 +22,9 @@ executed ordered as follows:
 
 | Modifier                  | Contexts                                                                                                                                                                                                                                             | Description                                                       |
 |:--------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|
-| `remove_response_headers` | [Server Block](block/server), [Files Block](block/files), [SPA Block](block/spa), [API Block](block/api), [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | list of response header to be removed from the client response.   |
-| `set_response_headers`    | [Server Block](block/server), [Files Block](block/files), [SPA Block](block/spa), [API Block](block/api), [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to set response header in the client response. |
-| `add_response_headers`    | [Server Block](block/server), [Files Block](block/files), [SPA Block](block/spa), [API Block](block/api), [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to add response header to the client response. |
+| `remove_response_headers` | [Server Block](/configuration/block/server), [Files Block](/configuration/block/files), [SPA Block](/configuration/block/spa), [API Block](/configuration/block/api), [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | list of response header to be removed from the client response.   |
+| `set_response_headers`    | [Server Block](/configuration/block/server), [Files Block](/configuration/block/files), [SPA Block](/configuration/block/spa), [API Block](/configuration/block/api), [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to set response header in the client response. |
+| `add_response_headers`    | [Server Block](/configuration/block/server), [Files Block](/configuration/block/files), [SPA Block](/configuration/block/spa), [API Block](/configuration/block/api), [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to add response header to the client response. |
 
 All `*_response_headers` are executed from: `server`, `files`, `spa`, `api`, `endpoint`, `proxy`, `backend` and `error_handler`.
 
@@ -35,7 +35,7 @@ given value.
 
 | Modifier              | Contexts                                                                                                  | Description                                        |
 |:----------------------|:----------------------------------------------------------------------------------------------------------|:---------------------------------------------------|
-| `set_response_status` | [Endpoint Block](block/endpoint), [Backend Block](block/backend), [Error Handler](block/error_handler) | HTTP status code to be set to the client response. |
+| `set_response_status` | [Endpoint Block](/configuration/block/endpoint), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | HTTP status code to be set to the client response. |
 
 If the HTTP status code ist set to `204`, the response body and the HTTP header
 field `Content-Length` is removed from the client response, and a warning is logged.
@@ -50,9 +50,9 @@ executed ordered as follows:
 
 | Modifier              | Contexts                                                                                                                               | Description                                                             |
 |:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------|
-| `remove_query_params` | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | list of query parameters to be removed from the upstream request URL.   |
-| `set_query_params`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to set query parameters in the upstream request URL. |
-| `add_query_params`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to add query parameters to the upstream request URL. |
+| `remove_query_params` | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | list of query parameters to be removed from the upstream request URL.   |
+| `set_query_params`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to set query parameters in the upstream request URL. |
+| `add_query_params`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to add query parameters to the upstream request URL. |
 
 All `*_query_params` are executed from: `endpoint`, `proxy`, `backend` and `error_handler`.
 
@@ -96,9 +96,9 @@ executed ordered as follows:
 
 | Modifier             | Contexts                                                                                                                               | Description                                                             |
 |:---------------------|:---------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------|
-| `remove_form_params` | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | list of form parameters to be removed from the upstream request body.   |
-| `set_form_params`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to set form parameters in the upstream request body. |
-| `add_form_params`    | [Endpoint Block](block/endpoint), [Proxy Block](block/proxy), [Backend Block](block/backend), [Error Handler](block/error_handler) | Key/value(s) pairs to add form parameters to the upstream request body. |
+| `remove_form_params` | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | list of form parameters to be removed from the upstream request body.   |
+| `set_form_params`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to set form parameters in the upstream request body. |
+| `add_form_params`    | [Endpoint Block](/configuration/block/endpoint), [Proxy Block](/configuration/block/proxy), [Backend Block](/configuration/block/backend), [Error Handler](/configuration/block/error_handler) | Key/value(s) pairs to add form parameters to the upstream request body. |
 
 All `*_form_params` are executed from: `endpoint`, `proxy`, `backend` and `error_handler`.
 
diff --git a/docs/website/content/2.configuration/4.block/api.md b/docs/website/content/2.configuration/4.block/api.md
index c85b5108..5c82c490 100644
--- a/docs/website/content/2.configuration/4.block/api.md
+++ b/docs/website/content/2.configuration/4.block/api.md
@@ -7,12 +7,12 @@ as JSON error with an error body payload. This can be customized via `error_file
 
 | Block name | Context                       | Label    | Nested block(s)                                                                                                 |
 |:-----------|:------------------------------|:---------|:----------------------------------------------------------------------------------------------------------------|
-| `api`      | [Server Block](server) | Optional | [Endpoint Block(s)](endpoint), [CORS Block](cors), [Error Handler Block(s)](error_handler) |
+| `api`      | [Server Block](/configuration/block/server) | Optional | [Endpoint Block(s)](/configuration/block/endpoint), [CORS Block](/configuration/block/cors), [Error Handler Block(s)](/configuration/block/error_handler) |
 
 
 ### Attribute `allowed_methods`
 
-The default value `*` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](cors) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method.
+The default value `*` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](/configuration/block/cors) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method.
 
 **Example:** `allowed_methods = ["GET", "POST"]` or `allowed_methods = ["*", "BREW"]`
 
diff --git a/docs/website/content/2.configuration/4.block/backend.md b/docs/website/content/2.configuration/4.block/backend.md
index 6aef8cbb..8f1a69ae 100644
--- a/docs/website/content/2.configuration/4.block/backend.md
+++ b/docs/website/content/2.configuration/4.block/backend.md
@@ -8,11 +8,11 @@ draft: false
 
 The `backend` block defines the connection to a local/remote backend service.
 
-Backends can be defined in the [Definitions Block](definitions) and referenced by _label_.
+Backends can be defined in the [Definitions Block](/configuration/block/definitions) and referenced by _label_.
 
 | Block name | Context                                                                                                                                                                                                                                   | Label                                                                     | Nested block(s)                                                                                                                       |
 |:-----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
-| `backend`  | [Definitions Block](definitions), [Proxy Block](proxy), [Request Block](request), [OAuth2 CC Block](oauth2req_auth), [JWT Block](jwt), [OAuth2 AC Block (beta)](oauth2), [OIDC Block](oidc)                                               | &#9888; required, when defined in [Definitions Block](definitions)        | [OpenAPI Block](openapi), [OAuth2 CC Block](oauth2req_auth), [Health Block](health), [Token Request (Beta) Block](token_request), [Rate Limit Block (beta)](rate_limit) |
+| `backend`  | [Definitions Block](/configuration/block/definitions), [Proxy Block](/configuration/block/proxy), [Request Block](/configuration/block/request), [OAuth2 CC Block](/configuration/block/oauth2req_auth), [JWT Block](/configuration/block/jwt), [OAuth2 AC Block (beta)](/configuration/block/oauth2), [OIDC Block](/configuration/block/oidc)                                               | &#9888; required, when defined in [Definitions Block](/configuration/block/definitions)        | [OpenAPI Block](/configuration/block/openapi), [OAuth2 CC Block](/configuration/block/oauth2req_auth), [Health Block](/configuration/block/health), [Token Request (Beta) Block](/configuration/block/token_request), [Rate Limit Block (beta)](/configuration/block/rate_limit), [TLS Block](/configuration/block/backend_tls) |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/backend_tls.md b/docs/website/content/2.configuration/4.block/backend_tls.md
new file mode 100644
index 00000000..c5bc6814
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/backend_tls.md
@@ -0,0 +1,64 @@
+---
+title: 'TLS (Backend)'
+description: 'TLS settings for the related backend.'
+draft: false
+---
+
+# TLS (Backend)
+
+| Block name   | Context                                       | Label    |
+|:-------------|:----------------------------------------------|:---------|
+| `tls`        | [Backend Block](/configuration/block/backend) | no       |
+
+Couper has a command-line argument to add a `ca-file` to the backend CA-Pool for all backends.
+However, this `tls` block allows a more specific pool configuration per backend if the `server_ca_certificate` or
+`server_ca_certificate_file` is provided.
+
+### mTLS
+
+Additionally the `client_certificate`(or `client_certificate_file`) and `client_private_key` (or `client_private_key_file`)
+attributes allow the backend to present those ones during a TLS handshake to an origin which requires them due to an mTLS setup.
+
+::attributes
+---
+values: [
+  {
+    "default": "",
+    "description": "Public part of the client certificate in DER or PEM format.",
+    "name": "client_certificate",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the client certificate file in DER or PEM format.",
+    "name": "client_certificate_file",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Private part of the client certificate in DER or PEM format. Required to complete a mTLS handshake.",
+    "name": "client_private_key",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Private part of the client certificate file in DER or PEM format. Required to complete a mTLS handshake.",
+    "name": "client_private_key_file",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the certificate authority in DER or PEM format.",
+    "name": "server_ca_certificate",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the certificate authority file in DER or PEM format.",
+    "name": "server_ca_certificate_file",
+    "type": "string"
+  }
+]
+
+---
+::
diff --git a/docs/website/content/2.configuration/4.block/basic_auth.md b/docs/website/content/2.configuration/4.block/basic_auth.md
index a8393d27..54aa2de3 100644
--- a/docs/website/content/2.configuration/4.block/basic_auth.md
+++ b/docs/website/content/2.configuration/4.block/basic_auth.md
@@ -2,11 +2,11 @@
 
 | Block name   | Context                                 | Label    | Nested block(s)                                |
 |:-------------|:----------------------------------------|:---------|:-----------------------------------------------|
-| `basic_auth` | [Definitions Block](definitions) | required | [Error Handler Block(s)](error_handler) |
+| `basic_auth` | [Definitions Block](/configuration/block/definitions) | required | [Error Handler Block(s)](/configuration/block/error_handler) |
 
 The  `basic_auth` block lets you configure basic auth for your gateway. Like all
-[access control](../access-control) types, the `basic_auth` block is defined in the
-[`definitions` block](definitions) and can be referenced in all configuration
+[access control](/configuration/access-control) types, the `basic_auth` block is defined in the
+[`definitions` block](/configuration/block/definitions) and can be referenced in all configuration
 blocks by its required _label_.
 
 If both `user`/`password` and `htpasswd_file` are configured, the incoming
diff --git a/docs/website/content/2.configuration/4.block/beta_oauth2.md b/docs/website/content/2.configuration/4.block/beta_oauth2.md
index b3e1ac57..a669962a 100644
--- a/docs/website/content/2.configuration/4.block/beta_oauth2.md
+++ b/docs/website/content/2.configuration/4.block/beta_oauth2.md
@@ -1,12 +1,12 @@
 # OAuth2 AC (Beta)
 
-The `beta_oauth2` block lets you configure the [`oauth2_authorization_url()` function](../functions) and an access
+The `beta_oauth2` block lets you configure the [`oauth2_authorization_url()` function](/configuration/functions) and an access
 control for an OAuth2 **Authorization Code Grant Flow** redirect endpoint.
-Like all [access control](../access-control) types, the `beta_oauth2` block is defined in the [`definitions` block](definitions) and can be referenced in all configuration blocks by its required _label_.
+Like all [access control](/configuration/access-control) types, the `beta_oauth2` block is defined in the [`definitions` block](/configuration/block/definitions) and can be referenced in all configuration blocks by its required _label_.
 
 | Block name    | Context                                 | Label            | Nested block(s)                                                                                                  |
 |:--------------|:----------------------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------|
-| `beta_oauth2` | [Definitions Block](definitions)        | &#9888; required | [Backend Block](backend), [Error Handler Block](error_handler), [JWT Signing Profile Block](jwt_signing_profile) |
+| `beta_oauth2` | [Definitions Block](/configuration/block/definitions)        | &#9888; required | [Backend Block](/configuration/block/backend), [Error Handler Block](/configuration/block/error_handler), [JWT Signing Profile Block](jwt_signing_profile) |
 
 ::attributes
 ---
@@ -90,4 +90,4 @@ values: [
 
 If the authorization server supports the `code_challenge_method` `S256` (a.k.a. PKCE, see RFC 7636), we recommend `verifier_method = "ccm_s256"`.
 
-The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](../modifiers#request-header) in a [backend block](backend).
+The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](/configuration/modifiers#request-header) in a [backend block](/configuration/block/backend).
diff --git a/docs/website/content/2.configuration/4.block/client_certificate.md b/docs/website/content/2.configuration/4.block/client_certificate.md
new file mode 100644
index 00000000..d3118f98
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/client_certificate.md
@@ -0,0 +1,69 @@
+---
+title: 'Client Certificate'
+description: 'The `client_certificate` block is part of its parent `tls` block. Enables mTLS configuration.'
+draft: false
+---
+
+# Client Certificate
+
+| Block name   | Context                                              | Label    |
+|:-------------|:-----------------------------------------------------|:---------|
+| `client_certificate` | [tls Block](/configuration/block/server_tls) | optional |
+
+Define an optional `client_certificate` block with its optional _label_ to enable **mTLS**.
+
+> **mTLS:** stands for mutual TLS and will extend the normal handshake process with an additional request (client must present the certificate) and verification for the configured client certificate (CA).
+
+Configuring a `ca_certificate` is the standard way to specify a client certificate. But you can also provide the `leaf_certificate`
+which effectively is the client certificate. The server will verify the given client certificate byte by byte against its own leaf certificate.
+A combination of `ca_certificate`(or `ca_certificate_file`) or/and `leaf_certificate`(or `leaf_certificate_file`) is valid.
+This covers the use-case where the CA has signed multiple client certificates and you want to limit the access to specific ones.
+
+## Example
+
+```hcl
+client_certificate "IOT" {
+  ca_certificate = "base64_der" # PEM or DER encoded
+  # OR
+  ca_certificate_file = "couperIntermediate.crt" # PEM
+
+  # OR/AND
+  # trusted client leaf cert
+
+  leaf_certificate = "base64_der"
+  # OR
+  leaf_certificate_file = "couperClient.crt" # PEM
+}
+```
+
+::attributes
+---
+values: [
+  {
+    "default": "",
+    "description": "Public part of the certificate authority in DER or PEM format.",
+    "name": "ca_certificate",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the certificate authority file in DER or PEM format.",
+    "name": "ca_certificate_file",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the client certificate in DER or PEM format.",
+    "name": "leaf_certificate",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the client certificate file in DER or PEM format.",
+    "name": "leaf_certificate_file",
+    "type": "string"
+  }
+]
+
+---
+::
diff --git a/docs/website/content/2.configuration/4.block/cors.md b/docs/website/content/2.configuration/4.block/cors.md
index f433a9f4..d700c875 100644
--- a/docs/website/content/2.configuration/4.block/cors.md
+++ b/docs/website/content/2.configuration/4.block/cors.md
@@ -4,9 +4,9 @@ The `cors` block configures the CORS (Cross-Origin Resource Sharing) behavior in
 
 | Block name | Context                                                                                                       | Label    | Nested block(s) |
 |:-----------|:--------------------------------------------------------------------------------------------------------------|:---------|:----------------|
-| `cors`     | [Server Block](server), [Files Block](files), [SPA Block](spa), [API Block](api). | no label | -               |
+| `cors`     | [Server Block](/configuration/block/server), [Files Block](/configuration/block/files), [SPA Block](/configuration/block/spa), [API Block](/configuration/block/api). | no label | -               |
 
-**Note:** `Access-Control-Allow-Methods` is only sent in response to a CORS preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method (see the `allowed_method` attribute for [`api`](api) or [`endpoint`](endpoint) blocks).
+**Note:** `Access-Control-Allow-Methods` is only sent in response to a CORS preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method (see the `allowed_method` attribute for [`api`](/configuration/block/api) or [`endpoint`](/configuration/block/endpoint) blocks).
 
 ### Attribute `allowed_origins`
 
diff --git a/docs/website/content/2.configuration/4.block/definitions.md b/docs/website/content/2.configuration/4.block/definitions.md
index 1544a3ef..c93bfaa8 100644
--- a/docs/website/content/2.configuration/4.block/definitions.md
+++ b/docs/website/content/2.configuration/4.block/definitions.md
@@ -2,8 +2,8 @@
 
 Use the `definitions` block to define configurations you want to reuse.
 
-&#9888; [access control](../access-control) is **always** defined in the `definitions` block.
+&#9888; [access control](/configuration/access-control) is **always** defined in the `definitions` block.
 
 | Block name    | Context | Label    | Nested block(s)                                                                                                                                                                                                                                                            |
 |:--------------|:--------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `definitions` | -       | no label | [Backend Block(s)](backend), [Basic Auth Block(s)](basic_auth), [JWT Block(s)](jwt), [JWT Signing Profile Block(s)](jwt_signing_profile), [SAML Block(s)](saml), [OAuth2 AC Block(s)](oauth2), [OIDC Block(s)](oidc), [Proxy Block(s)](proxy) |
+| `definitions` | -       | no label | [Backend Block(s)](/configuration/block/backend), [Basic Auth Block(s)](/configuration/block/basic_auth), [JWT Block(s)](/configuration/block/jwt), [JWT Signing Profile Block(s)](/configuration/block/jwt_signing_profile), [SAML Block(s)](/configuration/block/saml), [OAuth2 AC Block(s)](/configuration/block/oauth2), [OIDC Block(s)](/configuration/block/oidc), [Proxy Block(s)](/configuration/block/proxy) |
diff --git a/docs/website/content/2.configuration/4.block/endpoint.md b/docs/website/content/2.configuration/4.block/endpoint.md
index 3a09dd5b..cefc2e6e 100644
--- a/docs/website/content/2.configuration/4.block/endpoint.md
+++ b/docs/website/content/2.configuration/4.block/endpoint.md
@@ -6,7 +6,7 @@ produce an explicit or implicit client response.
 
 | Block name | Context                                                | Label                                                                  | Nested block(s)                                                                                                                                        |
 |:-----------|:-------------------------------------------------------|:-----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `endpoint` | [Server Block](server), [API Block](api) | &#9888; required, defines the path suffix for incoming client requests | [Proxy Block(s)](proxy),  [Request Block(s)](request), [Response Block](response), [Error Handler Block(s)](error_handler) |
+| `endpoint` | [Server Block](/configuration/block/server), [API Block](/configuration/block/api) | &#9888; required, defines the path suffix for incoming client requests | [Proxy Block(s)](/configuration/block/proxy),  [Request Block(s)](/configuration/block/request), [Response Block](/configuration/block/response), [Error Handler Block(s)](/configuration/block/error_handler) |
 
 ## Endpoint Sequence
 
@@ -15,7 +15,7 @@ at load-time they will be executed sequentially. Unexpected responses can be cau
 
 ### Attribute `allowed_methods`
 
-The default value `"*"` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](cors) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method.
+The default value `"*"` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](/configuration/block/cors) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method.
 
 **Example:**
 
diff --git a/docs/website/content/2.configuration/4.block/environment.md b/docs/website/content/2.configuration/4.block/environment.md
index 086cd82e..85c857db 100644
--- a/docs/website/content/2.configuration/4.block/environment.md
+++ b/docs/website/content/2.configuration/4.block/environment.md
@@ -1,19 +1,19 @@
 # Environment
 
 The `environment` block lets you refine the Couper configuration based on the set
-[environment](../command-line#global-options).
+[environment](/configuration/command-line#global-options).
 
 | Block name    | Context  | Label                                            | Nested block(s)                     |
 | :------------ | :------- | :----------------------------------------------- | :---------------------------------- |
 | `environment` | Overall. | &#9888; required, multiple labels are supported. | All configuration blocks of Couper. |
 
 The `environment` block works like a preprocessor. If the label of an `environment`
-block does not match the set [`COUPER_ENVIRONMENT`](../command-line#global-options) value, the preprocessor
+block does not match the set [`COUPER_ENVIRONMENT`](/configuration/command-line#global-options) value, the preprocessor
 removes this block and its content. Otherwise, the content of the block is added
 to the configuration.
 
 Starting Couper with a configuration containing `environment` blocks will fail, if `COUPER_ENVIRONMENT`
-is missing or empty. A default value can be set using the [`settings` block](settings).
+is missing or empty. A default value can be set using the [`settings` block](/configuration/block/settings).
 
 ## Example
 
diff --git a/docs/website/content/2.configuration/4.block/error_handler.md b/docs/website/content/2.configuration/4.block/error_handler.md
index 7b165df9..3bccf31d 100644
--- a/docs/website/content/2.configuration/4.block/error_handler.md
+++ b/docs/website/content/2.configuration/4.block/error_handler.md
@@ -4,11 +4,11 @@ The `error_handler` block lets you configure the handling of errors thrown in co
 
 The error handler label specifies which [error type](/configuration/error-handling#error-types) should be handled. Multiple labels are allowed. The label can be omitted to catch all relevant errors. This has the same behavior as the error type `*`, that catches all errors explicitly.
 
-Concerning child blocks and attributes, the `error_handler` block is similar to an [Endpoint Block](endpoint).
+Concerning child blocks and attributes, the `error_handler` block is similar to an [Endpoint Block](/configuration/block/endpoint).
 
 | Block name  |Context|Label|Nested block(s)|
 | :-----------| :-----------| :-----------| :-----------|
-| `error_handler` | [API Block](api), [Endpoint Block](endpoint), [Basic Auth Block](basic_auth), [JWT Block](jwt), [OAuth2 AC Block (Beta)](oauth2), [OIDC Block](oidc), [SAML Block](saml) | optional | [Proxy Block(s)](proxy),  [Request Block(s)](request), [Response Block](response), [Error Handler Block(s)](error_handler) |
+| `error_handler` | [API Block](/configuration/block/api), [Endpoint Block](/configuration/block/endpoint), [Basic Auth Block](/configuration/block/basic_auth), [JWT Block](/configuration/block/jwt), [OAuth2 AC Block (Beta)](/configuration/block/oauth2), [OIDC Block](/configuration/block/oidc), [SAML Block](/configuration/block/saml) | optional | [Proxy Block(s)](/configuration/block/proxy),  [Request Block(s)](/configuration/block/request), [Response Block](/configuration/block/response), [Error Handler Block(s)](/configuration/block/error_handler) |
 
 ## Example
 
diff --git a/docs/website/content/2.configuration/4.block/files.md b/docs/website/content/2.configuration/4.block/files.md
index 65d5f562..0e2739de 100644
--- a/docs/website/content/2.configuration/4.block/files.md
+++ b/docs/website/content/2.configuration/4.block/files.md
@@ -4,7 +4,7 @@ The `files` blocks configure the file serving. Can be defined multiple times as
 
 | Block name | Context                       | Label    | Nested block(s)           |
 |:-----------|:------------------------------|:---------|:--------------------------|
-| `files`    | [Server Block](server) | Optional | [CORS Block](cors) |
+| `files`    | [Server Block](/configuration/block/server) | Optional | [CORS Block](/configuration/block/cors) |
 
 
 ::attributes
diff --git a/docs/website/content/2.configuration/4.block/health.md b/docs/website/content/2.configuration/4.block/health.md
index 39ee2981..3a45ccd7 100644
--- a/docs/website/content/2.configuration/4.block/health.md
+++ b/docs/website/content/2.configuration/4.block/health.md
@@ -1,12 +1,12 @@
 # Health
 
-Defines a recurring health check request for its backend. Results can be obtained via the [`backends.<label>.health` variables](../variables#backends).
+Defines a recurring health check request for its backend. Results can be obtained via the [`backends.<label>.health` variables](/configuration/variables#backends).
 Changes in health states and related requests will be logged. Default User-Agent will be `Couper / <version> health-check` if not provided
-via `headers` attribute. An unhealthy backend will return with a [`backend_unhealthy`](../error-handling#api-error-types) error.
+via `headers` attribute. An unhealthy backend will return with a [`backend_unhealthy`](/configuration/error-handling#api-error-types) error.
 
 | Block name    | Context                           | Label | Nested block |
 |:--------------|:----------------------------------|:------|:-------------|
-| `beta_health` | [`backend` block](backend) | –     |              |
+| `beta_health` | [`backend` block](/configuration/block/backend) | –     |              |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/jwt.md b/docs/website/content/2.configuration/4.block/jwt.md
index 294deaea..f9cec66d 100644
--- a/docs/website/content/2.configuration/4.block/jwt.md
+++ b/docs/website/content/2.configuration/4.block/jwt.md
@@ -1,15 +1,15 @@
 # JWT
 
 The `jwt` block lets you configure JSON Web Token access control for your gateway.
-Like all [access control](../access-control) types, the `jwt` block is defined in
-the [`definitions` Block](definitions) and can be referenced in all configuration blocks by its
+Like all [access control](/configuration/access-control) types, the `jwt` block is defined in
+the [`definitions` Block](/configuration/block/definitions) and can be referenced in all configuration blocks by its
 required _label_.
 
 Since responses from endpoints protected by JWT access controls are not publicly cacheable, a `Cache-Control: private` header field is added to the response, unless this feature is disabled with `disable_private_caching = true`.
 
 | Block name | Context                                 | Label            | Nested block(s)                                                                  |
 |:-----------|:----------------------------------------|:-----------------|:---------------------------------------------------------------------------------|
-| `jwt`      | [Definitions Block](definitions) | &#9888; required | [JWKS `backend`](backend), [Error Handler Block(s)](error_handler) |
+| `jwt`      | [Definitions Block](/configuration/block/definitions) | &#9888; required | [JWKS `backend`](/configuration/block/backend), [Error Handler Block(s)](/configuration/block/error_handler) |
 
 ::attributes
 ---
@@ -156,7 +156,7 @@ A JWT access control configured by this block can extract permissions from
 - the value of the claim specified by `beta_permissions_claim` and
 - the result of mapping the value of the claim specified by `beta_roles_claim` using the `beta_roles_map`.
 
-The `jwt` block may also be referenced by the [`jwt_sign()` function](../functions), if it has a `signing_ttl` defined. For `HS*` algorithms the signing key is taken from `key`/`key_file`, for `RS*` and `ES*` algorithms, `signing_key` or `signing_key_file` have to be specified.
+The `jwt` block may also be referenced by the [`jwt_sign()` function](/configuration/functions), if it has a `signing_ttl` defined. For `HS*` algorithms the signing key is taken from `key`/`key_file`, for `RS*` and `ES*` algorithms, `signing_key` or `signing_key_file` have to be specified.
 
 > **Note:** A `jwt` block with `signing_ttl` cannot have the same label as a `jwt_signing_profile` block.
 
diff --git a/docs/website/content/2.configuration/4.block/jwt_signing_profile.md b/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
index 37abda3c..cb96d902 100644
--- a/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
+++ b/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
@@ -1,7 +1,7 @@
 # JWT Signing Profile
 
 The `jwt_signing_profile` block lets you configure a JSON Web Token signing
-profile for your gateway. It is referenced in the [`jwt_sign()` function](../functions)
+profile for your gateway. It is referenced in the [`jwt_sign()` function](/configuration/functions)
 by its required _label_.
 
 It can also be used (without _label_) in [`oauth2`](oauth2), [`oidc`](oidc) or
@@ -10,7 +10,7 @@ or `"private_key_jwt"`.
 
 | Block name            | Context                                                                                                             | Label                              | Nested block(s) |
 |:----------------------|:--------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:----------------|
-| `jwt_signing_profile` | [Definitions Block](definitions), [OAuth2 Block](oauth2), [OAuth2 AC (Beta) Block](beta_oauth2), [OIDC Block](oidc) | required if defined in defititions | -               |
+| `jwt_signing_profile` | [Definitions Block](/configuration/block/definitions), [OAuth2 Block](oauth2), [OAuth2 AC (Beta) Block](beta_oauth2), [OIDC Block](oidc) | required if defined in defititions | -               |
 
 
 ::attributes
diff --git a/docs/website/content/2.configuration/4.block/oauth2.md b/docs/website/content/2.configuration/4.block/oauth2.md
index 9ac5cc16..e00baa0d 100644
--- a/docs/website/content/2.configuration/4.block/oauth2.md
+++ b/docs/website/content/2.configuration/4.block/oauth2.md
@@ -1,12 +1,12 @@
 # OAuth2
 
-The `oauth2` block in the [Backend Block](backend) context configures an OAuth2 flow to request a bearer token for the backend request.
+The `oauth2` block in the [Backend Block](/configuration/block/backend) context configures an OAuth2 flow to request a bearer token for the backend request.
 
 **Note:** The token received from the authorization server's token endpoint is stored **per backend**. So even with flows where a user's account characteristics like username/password or email address are involved, there is no way to "switch" from one user to another depending on the client request.
 
 | Block name | Context                  | Label    | Nested block(s)                                                            |
 |:-----------|:-------------------------|:---------|:---------------------------------------------------------------------------|
-| `oauth2`   | [Backend Block](backend) | no label | [Backend Block](backend), [JWT Signing Profile Block](jwt_signing_profile) |
+| `oauth2`   | [Backend Block](/configuration/block/backend) | no label | [Backend Block](/configuration/block/backend), [JWT Signing Profile Block](jwt_signing_profile) |
 
 ::attributes
 ---
@@ -82,4 +82,4 @@ values: [
 ---
 ::
 
-The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](../modifiers#request-header) in a [backend block](backend).
+The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](/configuration/modifiers#request-header) in a [backend block](/configuration/block/backend).
diff --git a/docs/website/content/2.configuration/4.block/oidc.md b/docs/website/content/2.configuration/4.block/oidc.md
index 5fb13293..bd5a9bdb 100644
--- a/docs/website/content/2.configuration/4.block/oidc.md
+++ b/docs/website/content/2.configuration/4.block/oidc.md
@@ -2,11 +2,11 @@
 
 The `oidc` block lets you configure the [`oauth2_authorization_url()` function](/configuration/functions) and an access
 control for an OIDC **Authorization Code Grant Flow** redirect endpoint.
-Like all [access control](../access-control) types, the `oidc` block is defined in the [`definitions` Block](definitions) and can be referenced in all configuration blocks by its required _label_.
+Like all [access control](/configuration/access-control) types, the `oidc` block is defined in the [`definitions` Block](/configuration/block/definitions) and can be referenced in all configuration blocks by its required _label_.
 
 | Block name | Context                                 | Label            | Nested block(s)                                                                                                  |
 |:-----------|:----------------------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------|
-| `oidc`     | [Definitions Block](definitions)        | &#9888; required | [Backend Block](backend), [Error Handler Block](error_handler), [JWT Signing Profile Block](jwt_signing_profile) |
+| `oidc`     | [Definitions Block](/configuration/block/definitions)        | &#9888; required | [Backend Block](/configuration/block/backend), [Error Handler Block](/configuration/block/error_handler), [JWT Signing Profile Block](jwt_signing_profile) |
 
 > any `backend` attributes: Do not disable the peer certificate validation with `disable_certificate_validation = true`.
 
@@ -131,7 +131,7 @@ You should only use `configuration_backend`, `jwks_uri_backend`, `token_backend`
 
 If the OpenID server supports the `code_challenge_method` `S256` the default value for `verifier_method`is `"ccm_s256"`, `"nonce"` otherwise.
 
-The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](../modifiers#request-header) in a [backend block](backend).
+The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](/configuration/modifiers#request-header) in a [backend block](/configuration/block/backend).
 
 
 ::duration
diff --git a/docs/website/content/2.configuration/4.block/openapi.md b/docs/website/content/2.configuration/4.block/openapi.md
index 60033c9a..e420721b 100644
--- a/docs/website/content/2.configuration/4.block/openapi.md
+++ b/docs/website/content/2.configuration/4.block/openapi.md
@@ -12,7 +12,7 @@ In this case the response validation will fail if not ignored, too.
 
 |Block name|Context|Label|Nested block(s)|
 | :-----------| :-----------| :-----------| :-----------|
-|`openapi`| [Backend Block](backend)|-|-|
+|`openapi`| [Backend Block](/configuration/block/backend)|-|-|
 
 
 ::attributes
diff --git a/docs/website/content/2.configuration/4.block/proxy.md b/docs/website/content/2.configuration/4.block/proxy.md
index 687c06a5..d2939ab2 100644
--- a/docs/website/content/2.configuration/4.block/proxy.md
+++ b/docs/website/content/2.configuration/4.block/proxy.md
@@ -2,13 +2,13 @@
 
 The `proxy` block creates and executes a proxy request to a backend service.
 
-> 📝 Multiple `proxy` and [`request`](request) blocks are executed in parallel.
+> 📝 Multiple `proxy` and [`request`](/configuration/block/request) blocks are executed in parallel.
 
 | Block name | Context                           | Label                                                                                                                                                                                                                                          | Nested block(s)                                                                                                                                                                                                                                |
 |:-----------|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `proxy`    | [Endpoint Block](endpoint) | See `Label` description below. | [Backend Block](backend) (&#9888; required, if no [Backend Block](backend) reference is defined or no `url` attribute is set.), [Websockets Block](websockets) (&#9888; Either websockets attribute or block is allowed.) |
+| `proxy`    | [Endpoint Block](/configuration/block/endpoint) | See `Label` description below. | [Backend Block](/configuration/block/backend) (&#9888; required, if no [Backend Block](/configuration/block/backend) reference is defined or no `url` attribute is set.), [Websockets Block](/configuration/block/websockets) (&#9888; Either websockets attribute or block is allowed.) |
 
-**Label:** If defined in an [Endpoint Block](endpoint), a `proxy` block or [Request Block](request) w/o a label has an implicit name `"default"`. If defined in the [Definitions Block](definitions), the label of `proxy` is used as reference in [Endpoint Blocks](endpoint) and the name can be defined via `name` attribute. Only **one** `proxy` block or [Request Block](request) w/ label `"default"` per [Endpoint Block](endpoint) is allowed. 
+**Label:** If defined in an [Endpoint Block](/configuration/block/endpoint), a `proxy` block or [Request Block](/configuration/block/request) w/o a label has an implicit name `"default"`. If defined in the [Definitions Block](/configuration/block/definitions), the label of `proxy` is used as reference in [Endpoint Blocks](/configuration/block/endpoint) and the name can be defined via `name` attribute. Only **one** `proxy` block or [Request Block](/configuration/block/request) w/ label `"default"` per [Endpoint Block](/configuration/block/endpoint) is allowed. 
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/rate_limit.md b/docs/website/content/2.configuration/4.block/rate_limit.md
index e1a88d4a..e3c4e1d2 100644
--- a/docs/website/content/2.configuration/4.block/rate_limit.md
+++ b/docs/website/content/2.configuration/4.block/rate_limit.md
@@ -4,7 +4,7 @@ Rate limiting protects backend services. It implements quota management used to
 
 | Block name        | Context                         | Label | Nested block(s) |
 | :---------------- | :------------------------------ | :---- | :-------------- |
-| `beta_rate_limit` | named [`backend` block](backend)| -     | -               |
+| `beta_rate_limit` | named [`backend` block](/configuration/block/backend)| -     | -               |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/request.md b/docs/website/content/2.configuration/4.block/request.md
index 3ceb6019..5a77c4f6 100644
--- a/docs/website/content/2.configuration/4.block/request.md
+++ b/docs/website/content/2.configuration/4.block/request.md
@@ -2,11 +2,11 @@
 
 The `request` block creates and executes a request to a backend service.
 
-> 📝 Multiple [`proxy`](proxy) and `request` blocks are executed in parallel.
+> 📝 Multiple [`proxy`](/configuration/block/proxy) and `request` blocks are executed in parallel.
 
 | Block name | Context                           | Label                                                                                                                                                                                                                                                                      | Nested block(s)                                                                                                             |
 |:-----------|:----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------|
-| `request`  | [Endpoint Block](endpoint) | &#9888; A [Proxy Block](proxy) or [Request Block](request) w/o a label has an implicit label `"default"`. Only **one** [Proxy Block](proxy) or [Request Block](request) w/ label `"default"` per [Endpoint Block](endpoint) is allowed. | [Backend Block](backend) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
+| `request`  | [Endpoint Block](/configuration/block/endpoint) | &#9888; A [Proxy Block](/configuration/block/proxy) or [Request Block](/configuration/block/request) w/o a label has an implicit label `"default"`. Only **one** [Proxy Block](/configuration/block/proxy) or [Request Block](/configuration/block/request) w/ label `"default"` per [Endpoint Block](/configuration/block/endpoint) is allowed. | [Backend Block](/configuration/block/backend) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
 <!-- TODO: add available http methods -->
 
 
diff --git a/docs/website/content/2.configuration/4.block/response.md b/docs/website/content/2.configuration/4.block/response.md
index 0437f92b..0228c5c9 100644
--- a/docs/website/content/2.configuration/4.block/response.md
+++ b/docs/website/content/2.configuration/4.block/response.md
@@ -4,7 +4,7 @@ The `response` block creates and sends a client response.
 
 | Block name | Context                           | Label    | Nested block(s) |
 |:-----------|:----------------------------------|:---------|:----------------|
-| `response` | [Endpoint Block](endpoint) | no label | -               |
+| `response` | [Endpoint Block](/configuration/block/endpoint) | no label | -               |
 
 The response body can be omitted or must be one of `body` or `json_body`.
 
diff --git a/docs/website/content/2.configuration/4.block/saml.md b/docs/website/content/2.configuration/4.block/saml.md
index 33ac576a..3fc184f9 100644
--- a/docs/website/content/2.configuration/4.block/saml.md
+++ b/docs/website/content/2.configuration/4.block/saml.md
@@ -1,14 +1,14 @@
 # SAML
 
-The `saml` block lets you configure the [`saml_sso_url()` function](../functions) and an access
+The `saml` block lets you configure the [`saml_sso_url()` function](/configuration/functions) and an access
 control for a SAML Assertion Consumer Service (ACS) endpoint.
-Like all [access control](../access-control) types, the `saml` block is defined in
-the [`definitions` block](definitions) and can be referenced in all configuration blocks by its
+Like all [access control](/configuration/access-control) types, the `saml` block is defined in
+the [`definitions` block](/configuration/block/definitions) and can be referenced in all configuration blocks by its
 required _label_.
 
 | Block name | Context                                 | Label            | Nested block(s)                             |
 |:-----------|:----------------------------------------|:-----------------|:--------------------------------------------|
-| `saml`     | [Definitions Block](definitions) | &#9888; required | [Error Handler Block](error_handler) |
+| `saml`     | [Definitions Block](/configuration/block/definitions) | &#9888; required | [Error Handler Block](/configuration/block/error_handler) |
 
 ## Example
 
diff --git a/docs/website/content/2.configuration/4.block/server.md b/docs/website/content/2.configuration/4.block/server.md
index ad87a147..599bb25b 100644
--- a/docs/website/content/2.configuration/4.block/server.md
+++ b/docs/website/content/2.configuration/4.block/server.md
@@ -4,7 +4,7 @@ The `server` block is one of the root configuration blocks of Couper's configura
 
 | Block name | Context | Label    | Nested block(s)                                                                                                                                       |
 |:-----------|:--------|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `server`   | -       | optional | [CORS Block](cors), [Files Block](files), [SPA Block(s)](spa) , [API Block(s)](api), [Endpoint Block(s)](endpoint) |
+| `server`   | -       | optional | [CORS Block](/configuration/block/cors), [Files Block](/configuration/block/files), [SPA Block(s)](/configuration/block/spa) , [API Block(s)](/configuration/block/api), [Endpoint Block(s)](/configuration/block/endpoint) |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/server_certificate.md b/docs/website/content/2.configuration/4.block/server_certificate.md
new file mode 100644
index 00000000..26b27a3b
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/server_certificate.md
@@ -0,0 +1,59 @@
+---
+title: 'Server Certificate'
+description: 'The `server_certificate` block is part of its parent `tls` block. Enables TLS configuration.'
+draft: false
+---
+
+# Server Certificate
+
+| Block name   | Context                                              | Label    |
+|:-------------|:-----------------------------------------------------|:---------|
+| `server_certificate` | [tls Block](/configuration/block/server_tls) | optional |
+
+A configured `server_certificate` pub/key pair will be loaded once at startup and served on the related `server` port configured with the `hosts` attribute.
+
+## Example
+
+```hcl
+server_certificate "api.example.com" { #optional label
+  public_key = "base64_DER" # kube secret ...
+  # OR
+  public_key_file = "couperServer.crt" # PEM
+
+  private_key = "base64_DER"
+  # OR
+  private_key_file = "couperServer.key" # PEM
+}
+```
+
+::attributes
+---
+values: [
+  {
+    "default": "",
+    "description": "Private part of the certificate in DER or PEM format.",
+    "name": "private_key",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Private part of the certificate file in DER or PEM format.",
+    "name": "private_key_file",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the certificate in DER or PEM format.",
+    "name": "public_key",
+    "type": "string"
+  },
+  {
+    "default": "",
+    "description": "Public part of the certificate file in DER or PEM format.",
+    "name": "public_key_file",
+    "type": "string"
+  }
+]
+
+---
+::
diff --git a/docs/website/content/2.configuration/4.block/server_tls.md b/docs/website/content/2.configuration/4.block/server_tls.md
new file mode 100644
index 00000000..ab4e7df1
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/server_tls.md
@@ -0,0 +1,45 @@
+---
+title: 'TLS (Server)'
+description: 'TLS settings for the related server.'
+draft: false
+---
+
+# TLS (Server)
+
+| Block name   | Context                                     | Label    | Nested block(s)                              |
+|:-------------|:--------------------------------------------|:---------|:---------------------------------------------|
+| `tls`        | [Server Block](/configuration/block/server) | no       | [Server Certificate(s)](/configuration/block/server_certificate), [Client Certificate(s)](/configuration/block/client_certificate) |
+
+The `tls` block allows to configure one or more server certificates in the first place.
+The certificates will be served on all ports within the `hosts` list. Enabling `tls` also enables the upgrade option to the `HTTP2` protocol.
+
+ > The simplest configuration is an empty `tls {}` block which will serve a self signed certificate
+for local development.
+
+Multiple [`server_certificate`](server_certificate) or [`client_certificate`](client_certificate) blocks are allowed.
+
+## mTLS
+
+Once a [`client_certificate`](client_certificate) block is defined the server automatically requests and verify a certificate from the client.
+
+## Example
+
+```hcl
+server "couper" {
+  hosts = ["*:443"]
+
+  tls {
+    server_certificate "api.example.com" {
+      public_key_file = "couperServer.crt" # PEM
+      private_key_file = "couperServer.key" # PEM
+    }
+
+    # mTLS
+
+    client_certificate "IOT" {
+      ca_certificate_file = "couperIntermediate.crt" # PEM
+      # OR(AND!)
+      leaf_certificate_file = "couperClient.crt" # PEM
+    }
+  }
+```
diff --git a/docs/website/content/2.configuration/4.block/spa.md b/docs/website/content/2.configuration/4.block/spa.md
index 0ccea848..76e7914c 100644
--- a/docs/website/content/2.configuration/4.block/spa.md
+++ b/docs/website/content/2.configuration/4.block/spa.md
@@ -4,7 +4,7 @@ The `spa` blocks configure the Web serving for SPA assets. Can be defined multip
 
 | Block name | Context                       | Label    | Nested block(s)           |
 |:-----------|:------------------------------|:---------|:--------------------------|
-| `spa`      | [Server Block](server) | Optional | [CORS Block](cors) |
+| `spa`      | [Server Block](/configuration/block/server) | Optional | [CORS Block](/configuration/block/cors) |
 
 ```hcl
 spa {
diff --git a/docs/website/content/2.configuration/4.block/token_request.md b/docs/website/content/2.configuration/4.block/token_request.md
index 82051a48..b818606c 100644
--- a/docs/website/content/2.configuration/4.block/token_request.md
+++ b/docs/website/content/2.configuration/4.block/token_request.md
@@ -1,10 +1,10 @@
 # Token Request (Beta)
 
-The `beta_token_request` block in the [Backend Block](backend) context configures a request to get a token used to authorize backend requests.
+The `beta_token_request` block in the [Backend Block](/configuration/block/backend) context configures a request to get a token used to authorize backend requests.
 
 | Block name            | Context                           | Label                                                                                                                                                                                                                       | Nested block(s)                                                                                                      |
 |:----------------------|:----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------|
-| `beta_token_request`  | [Backend Block](backend)          | &#9888; A [Token Request (Beta) Block](token_request) w/o a label has an implicit label `"default"`. Only **one** [Token Request (Beta) Block](token_request) w/ label `"default"` per [Backend Block](backend) is allowed. | [Backend Block](backend) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
+| `beta_token_request`  | [Backend Block](/configuration/block/backend)          | &#9888; A [Token Request (Beta) Block](/configuration/block/token_request) w/o a label has an implicit label `"default"`. Only **one** [Token Request (Beta) Block](/configuration/block/token_request) w/ label `"default"` per [Backend Block](/configuration/block/backend) is allowed. | [Backend Block](/configuration/block/backend) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
 <!-- TODO: add available http methods -->
 
 ::attributes
diff --git a/docs/website/content/2.configuration/4.block/websockets.md b/docs/website/content/2.configuration/4.block/websockets.md
index 9fb53cf7..c7bbed8c 100644
--- a/docs/website/content/2.configuration/4.block/websockets.md
+++ b/docs/website/content/2.configuration/4.block/websockets.md
@@ -4,7 +4,7 @@ The `websockets` block activates support for WebSocket connections in Couper.
 
 | Block name   | Context                     | Label    | Nested block(s) |
 |:-------------|:----------------------------|:---------|:----------------|
-| `websockets` | [`proxy`](proxy)            | –        | –               |
+| `websockets` | [`proxy`](/configuration/block/proxy)            | –        | –               |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/5.variables.md b/docs/website/content/2.configuration/5.variables.md
index a34166aa..1bdcd6db 100644
--- a/docs/website/content/2.configuration/5.variables.md
+++ b/docs/website/content/2.configuration/5.variables.md
@@ -14,14 +14,14 @@ The second evaluation will happen during the request/response handling.
 | Variable                         | Type   | Description                                                                | Example   |
 | :------------------------------- | :----- | :------------------------------------------------------------------------- | :-------- |
 | `version`                        | string | Couper's version number                                                    | `"1.9.2"` |
-| `environment`                    | string | The [environment](command-line#global-options) Couper currently runs in    | `"prod"`  |
+| `environment`                    | string | The [environment](/configuration/command-line#global-options) Couper currently runs in    | `"prod"`  |
 
 ## `env`
 
 Environment variables can be accessed everywhere within the configuration file
 since these references get evaluated at start.
 
-You may provide default values by means of `environment_variables` in the [`defaults` block](block/defaults):
+You may provide default values by means of `environment_variables` in the [`defaults` block](/configuration/block/defaults):
 
 ```hcl
 // ...
@@ -48,9 +48,9 @@ defaults {
 | `body`                             | string        | Request message body                                                                                                                                                        |                                               |
 | `form_body.<name>`                 | list (string) | Parameter in a `application/x-www-form-urlencoded` body                                                                                                                     |                                               |
 | `json_body`                        | various       | Access JSON decoded message body. Media type must be `application/json` or `application/*+json`.                                                                            |                                               |
-| `context.beta_granted_permissions` | list (string) | Permissions granted to the requester as yielded by access controls (see e.g. `beta_permissions_claim`, `beta_roles_claim` in the [`jwt` block](block/jwt)).                | `["perm1", "perm2"]`                          |
-| `context.beta_required_permission` | string        | Permission required to perform the requested operation (value of the `beta_required_permission` attribute of [`endpoint`](#endpoint-block) (or [`api`](block/api)) block). |                                               |
-| `context.<name>.<property_name>`   | various       | Request context containing information from the [access control](access-control).                                                                                          |                                               |
+| `context.beta_granted_permissions` | list (string) | Permissions granted to the requester as yielded by access controls (see e.g. `beta_permissions_claim`, `beta_roles_claim` in the [`jwt` block](/configuration/block/jwt)).                | `["perm1", "perm2"]`                          |
+| `context.beta_required_permission` | string        | Permission required to perform the requested operation (value of the `beta_required_permission` attribute of [`endpoint`](#endpoint-block) (or [`api`](/configuration/block/api)) block). |                                               |
+| `context.<name>.<property_name>`   | various       | Request context containing information from the [access control](/configuration/access-control).                                                                                          |                                               |
 | `url`                              | string        | Request URL                                                                                                                                                                 | `"https://www.example.com/path/to?q=val&a=1"` |
 | `origin`                           | string        | Origin of the request URL                                                                                                                                                   | `"https://www.example.com"`                   |
 | `protocol`                         | string        | Request protocol                                                                                                                                                            | `"https"`                                     |
@@ -60,24 +60,24 @@ defaults {
 
 The value of `context.<name>` depends on the type of block referenced by `<name>`.
 
-For a [`basic_auth` block](block/basic_auth) and successfully authenticated request the variable contains the `user` name.
+For a [`basic_auth` block](/configuration/block/basic_auth) and successfully authenticated request the variable contains the `user` name.
 
-For a [`jwt` block](block/jwt) the variable contains claims from the JWT used for [access control](access-control).
+For a [`jwt` block](/configuration/block/jwt) the variable contains claims from the JWT used for [access control](/configuration/access-control).
 
-For a [`saml` block](block/saml) the variable contains
+For a [`saml` block](/configuration/block/saml) the variable contains
 
 - `sub`: the `NameID` of the SAML assertion
 - `exp`: optional expiration date (value of `SessionNotOnOrAfter` of the SAML assertion)
 - `attributes`: a map of attributes from the SAML assertion
 
-For an [`beta_oauth2` block](block/oauth2), the variable contains the response from the token endpoint, e.g.
+For an [`beta_oauth2` block](/configuration/block/oauth2), the variable contains the response from the token endpoint, e.g.
 
 - `access_token`: the access token retrieved from the token endpoint
 - `token_type`: the token type
 - `expires_in`: the token lifetime
 - `scope`: the granted scope (if different from the requested scope)
 
-and for an [`oidc` block](block/oidc) additionally:
+and for an [`oidc` block](/configuration/block/oidc) additionally:
 
 - `id_token`: the ID token
 - `id_token_claims`: a map of claims from the ID token
@@ -85,7 +85,7 @@ and for an [`oidc` block](block/oidc) additionally:
 
 ## `beta_token_response`
 
-Only available in the [`beta_token_request` block](block/token_request) context, `beta_token_response` allows access to the current token response (see [`backend_responses`](#backend_responses) for available properties).
+Only available in the [`beta_token_request` block](/configuration/block/token_request) context, `beta_token_response` allows access to the current token response (see [`backend_responses`](#backend_responses) for available properties).
 
 ## `backends`
 
@@ -93,9 +93,9 @@ Only available in the [`beta_token_request` block](block/token_request) context,
 
 | Variable                           | Type   | Description                                                                                         | Example                                              |
 |:-----------------------------------|:-------|:----------------------------------------------------------------------------------------------------|:-----------------------------------------------------|
-| `health`                           | object | current [health state](block/health)                                                                | `{"error": "", "healthy": true, "state": "healthy"}` |
-| `beta_tokens.<token_request_name>` | string | The token obtained by the [token request](block/token_request) with name `<token_request_name>`     |                                                      |
-| `beta_token`                       | string | The token obtained by the [token request](block/token_request) with name `"default"`, if configured |                                                      |
+| `health`                           | object | current [health state](/configuration/block/health)                                                                | `{"error": "", "healthy": true, "state": "healthy"}` |
+| `beta_tokens.<token_request_name>` | string | The token obtained by the [token request](/configuration/block/token_request) with name `<token_request_name>`     |                                                      |
+| `beta_token`                       | string | The token obtained by the [token request](/configuration/block/token_request) with name `"default"`, if configured |                                                      |
 
 ## `backend`
 
@@ -104,13 +104,13 @@ Only available in the `backend` block context, `backend` allows access to the cu
 ## `backend_request`
 
 `backend_request` holds information about the current backend request. It is only
-available in a [`backend` block](block/backend), and has the same attributes as a backend request in `backend_requests.<label>` (see [backend_requests](#backend_requests) below).
+available in a [`backend` block](/configuration/block/backend), and has the same attributes as a backend request in `backend_requests.<label>` (see [backend_requests](#backend_requests) below).
 
 ## `backend_requests`
 
 `backend_requests` is an object with all backend requests and their attributes.
-To access a specific request use the related label. [`request`](block/request) and
-[`proxy`](block/proxy) blocks without a label will be available as `default`.
+To access a specific request use the related label. [`request`](/configuration/block/request) and
+[`proxy`](/configuration/block/proxy) blocks without a label will be available as `default`.
 To access the HTTP method of the `default` request use `backend_requests.default.method` .
 
 | Variable                         | Type          | Description                                                                                                                                                                                                                                                                         | Example                                       |
@@ -123,7 +123,7 @@ To access the HTTP method of the `default` request use `backend_requests.default
 | `body`                           | string        | Backend request message body                                                                                                                                                                                                                                                        |                                               |
 | `form_body.<name>`               | list (string) | Parameter in a `application/x-www-form-urlencoded` body                                                                                                                                                                                                                             |                                               |
 | `json_body`                      | various       | Access JSON decoded message body. Media type must be `application/json` or `application/*+json`.                                                                                                                                                                                    |                                               |
-| `context.<name>.<property_name>` | various       | Request context containing claims from JWT used for [access control](access-control) or information from a SAML assertion, `<name>` being the [`jwt` block's](block/jwt) or [`saml` block's](block/saml) label and `property_name` being the claim's or assertion information's name |                                               |
+| `context.<name>.<property_name>` | various       | Request context containing claims from JWT used for [access control](/configuration/access-control) or information from a SAML assertion, `<name>` being the [`jwt` block's](/configuration/block/jwt) or [`saml` block's](/configuration/block/saml) label and `property_name` being the claim's or assertion information's name |                                               |
 | `url`                            | string        | Backend request URL                                                                                                                                                                                                                                                                 | `"https://www.example.com/path/to?q=val&a=1"` |
 | `origin`                         | string        | Origin of the backend request URL                                                                                                                                                                                                                                                   | `"https://www.example.com"`                   |
 | `protocol`                       | string        | Backend request protocol                                                                                                                                                                                                                                                            | `"https"`                                     |
@@ -134,13 +134,13 @@ To access the HTTP method of the `default` request use `backend_requests.default
 ## `backend_response`
 
 `backend_response` represents the current backend response.  It is only
-available in a [`backend` block](block/backend), and has the same attributes as a backend response in `backend_responses.<label>` (see [backend_responses](#backend_responses) below).
+available in a [`backend` block](/configuration/block/backend), and has the same attributes as a backend response in `backend_responses.<label>` (see [backend_responses](#backend_responses) below).
 
 ## `backend_responses`
 
 `backend_responses` is an object with all backend responses and their attributes.
 Use the related label to access a specific response.
-[`request`](block/request) and [`proxy`](block/proxy) blocks without a label will be available as `default`.
+[`request`](/configuration/block/request) and [`proxy`](/configuration/block/proxy) blocks without a label will be available as `default`.
 To access the HTTP status code of the `default` response use `backend_responses.default.status` .
 
 | Variable         | Type    | Description                                                                                      | Example |
diff --git a/docs/website/content/2.configuration/6.functions.md b/docs/website/content/2.configuration/6.functions.md
index 50a73856..16fa2baf 100644
--- a/docs/website/content/2.configuration/6.functions.md
+++ b/docs/website/content/2.configuration/6.functions.md
@@ -9,15 +9,15 @@
 | `join`                     | string          | Concatenates together the string elements of one or more lists with a given separator.                                                                                                                                                                                                            | `sep` (string), `lists...` (tuples or lists)                    | `join("-", [0,1,2,3])`                                                                              |
 | `json_decode`              | various         | Parses the given JSON string and, if it is valid, returns the value it represents.                                                                                                                                                                                                                | `encoded` (string)                                              | `json_decode("{\"foo\": 1}")`                                                                       |
 | `json_encode`              | string          | Returns a JSON serialization of the given value.                                                                                                                                                                                                                                                  | `val` (various)                                                 | `json_encode(request.context.myJWT)`                                                                |
-| `jwt_sign`                 | string          | jwt_sign creates and signs a JSON Web Token (JWT) from information from a referenced [JWT Signing Profile Block](block/jwt_signing_profile) (or [JWT Block](block/jwt) with `signing_ttl`) and additional claims provided as a function parameter.                                                | `label` (string), `claims` (object)                             | `jwt_sign("myJWT")`                                                                                 |
+| `jwt_sign`                 | string          | jwt_sign creates and signs a JSON Web Token (JWT) from information from a referenced [JWT Signing Profile Block](/configuration/block/jwt_signing_profile) (or [JWT Block](/configuration/block/jwt) with `signing_ttl`) and additional claims provided as a function parameter.                                                | `label` (string), `claims` (object)                             | `jwt_sign("myJWT")`                                                                                 |
 | `keys`                     | list            | Takes a map and returns a sorted list of the map keys.                                                                                                                                                                                                                                            | `inputMap` (object or map)                                      | `keys(request.headers)`                                                                             |
 | `length`                   | integer         | Returns the number of elements in the given collection.                                                                                                                                                                                                                                           | `collection` (tuple, list or map; **no object**)                | `length([0,1,2,3])`                                                                                 |
 | `lookup`                   | various         | Performs a dynamic lookup into a map. The default (third argument) is returned if the key (second argument) is not found in the inputMap (first argument).                                                                                                                                        | `inputMap` (object or map), `key` (string), `default` (various) | `lookup({a = 1}, "b", "def")`                                                                       |
 | `merge`                    | object or tuple | Deep-merges two or more of either objects or tuples. `null` arguments are ignored. An attribute value with a different type than the current value is set as the new value. `merge()` with no parameters returns `null`.                                                                          | `arg...` (object or tuple)                                      | `merge(request.headers, { x-additional = "myval" })`                                                |
-| `oauth2_authorization_url` | string          | Creates an OAuth2 authorization URL from a referenced [OAuth2 AC Block](block/oauth2) or [OIDC Block](block/oidc).                                                                                                                                                                                | `label` (string)                                                | `oauth2_authorization_url("myOAuth2")`                                                              |
+| `oauth2_authorization_url` | string          | Creates an OAuth2 authorization URL from a referenced [OAuth2 AC Block](/configuration/block/oauth2) or [OIDC Block](/configuration/block/oidc).                                                                                                                                                                                | `label` (string)                                                | `oauth2_authorization_url("myOAuth2")`                                                              |
 | `oauth2_verifier`          | string          | Creates a cryptographically random key as specified in RFC 7636, applicable for all verifier methods; e.g. to be set as a cookie and read into `verifier_value`. Multiple calls of this function in the same client request context return the same value.                                        |                                                                 | `oauth2_verifier()`                                                                                 |
 | `relative_url`             | string          | Returns a relative URL by retaining `path`, `query` and `fragment` components.  The input URL `s` must begin with `/<path>`, `//<authority>`, `http://` or `https://`, otherwise an error is thrown.                                                                                              | s (string)                                                      | `relative_url("https://httpbin.org/anything?query#fragment") // returns "/anything?query#fragment"` |
-| `saml_sso_url`             | string          | Creates a SAML SingleSignOn URL (including the `SAMLRequest` parameter) from a referenced [SAML Block](block/saml).                                                                                                                                                                               | `label` (string)                                                | `saml_sso_url("mySAML")`                                                                            |
+| `saml_sso_url`             | string          | Creates a SAML SingleSignOn URL (including the `SAMLRequest` parameter) from a referenced [SAML Block](/configuration/block/saml).                                                                                                                                                                               | `label` (string)                                                | `saml_sso_url("mySAML")`                                                                            |
 | `set_intersection`         | list or tuple   | Returns a new set containing the elements that exist in all of the given sets.                                                                                                                                                                                                                    | `sets...` (tuple or list)                                       | `set_intersection(["A", "B", "C"], ["B", D"])`                                                      |
 | `split`                    | tuple           | Divides a given string by a given separator, returning a list of strings containing the characters between the separator sequences.                                                                                                                                                               | `sep` (string), `str` (string)                                  | `split(" ", "foo bar qux")`                                                                         |
 | `substr`                   | string          | Extracts a sequence of characters from another string and creates a new string. The "`offset`" index may be negative, in which case it is relative to the end of the given string. The "`length`" may be `-1`, in which case the remainder of the string after the given offset will be returned. | `str` (string), `offset` (integer), `length` (integer)          | `substr("abcdef", 3, -1)`                                                                           |
diff --git a/docs/website/content/2.configuration/7.error-handling.md b/docs/website/content/2.configuration/7.error-handling.md
index ae01f654..e5a778fe 100644
--- a/docs/website/content/2.configuration/7.error-handling.md
+++ b/docs/website/content/2.configuration/7.error-handling.md
@@ -13,7 +13,7 @@ Detailed information is provided via log message. This way, all information can
 ## Access control `error_handler`
 
 Access control errors in particular require special handling, e.g. sending a specific response for missing login credentials.
-For this purpose every access control definition of `basic_auth`, `jwt`, `oidc` or `saml2` can define one or multiple [`error_handler` blocks](block/error_handler) with one or more defined error type labels listed below.
+For this purpose every access control definition of `basic_auth`, `jwt`, `oidc` or `saml2` can define one or multiple [`error_handler` blocks](/configuration/block/error_handler) with one or more defined error type labels listed below.
 
 ## Permissions related `error_handler`
 
@@ -21,8 +21,8 @@ Required permissions are configured for `api` or `endpoint` blocks. So errors ca
 
 ## Endpoint related `error_handler`
 
-A [sequence](block/endpoint#endpoint-sequence), a simple `request` or `proxy` error can be handled in combination with the `expected_status` attribute for `request`
-and `proxy` block definitions and an [`error_handler` block](block/error_handler) with the [related](#endpoint-error-types) type label.
+A [sequence](/configuration/block/endpoint#endpoint-sequence), a simple `request` or `proxy` error can be handled in combination with the `expected_status` attribute for `request`
+and `proxy` block definitions and an [`error_handler` block](/configuration/block/error_handler) with the [related](#endpoint-error-types) type label.
 
 ## Error types
 
diff --git a/docs/website/content/2.configuration/8.multiple-files.md b/docs/website/content/2.configuration/8.multiple-files.md
index 664d9ca6..04eff7de 100644
--- a/docs/website/content/2.configuration/8.multiple-files.md
+++ b/docs/website/content/2.configuration/8.multiple-files.md
@@ -1,7 +1,7 @@
 # Merging
 
 Couper supports merging of blocks and attributes from multiple configuration files
-(see [Global Options of Command Line Interface](command-line#global-options)).
+(see [Global Options of Command Line Interface](/configuration/command-line#global-options)).
 
 * [Merging](#merging)
 * [General Rules of Merging](#general-rules-of-merging)
diff --git a/docs/website/content/2.configuration/9.access-control.md b/docs/website/content/2.configuration/9.access-control.md
index d591caaa..7b1747eb 100644
--- a/docs/website/content/2.configuration/9.access-control.md
+++ b/docs/website/content/2.configuration/9.access-control.md
@@ -1,10 +1,10 @@
 # Access Control
 
 The configuration of access controls is twofold in Couper: You define the particular
-type such as `jwt` or `basic_auth` in [`definitions`](block/definitions), each with a distinct label
+type such as `jwt` or `basic_auth` in [`definitions`](/configuration/block/definitions), each with a distinct label
 (must not be one of the reserved names: `beta_granted_permissions`, `beta_required_permission`).
 
-Anywhere in the [`server` block](block/server) those labels can be used in the `access_control`
+Anywhere in the [`server` block](/configuration/block/server) those labels can be used in the `access_control`
 list to protect the respective block.
 
 > 📝 Access permissions are inherited by nested blocks.
@@ -12,12 +12,12 @@ list to protect the respective block.
 You can also disable access control for blocks with `disable_access_control`: With `disable_access_control = ["bar"]`,
 the `access_control` named `bar` will be disabled for the corresponding block context.
 
-All access controls have an option to handle [related errors](error-handling#access-control-error_handler).
+All access controls have an option to handle [related errors](/configuration/error-handling#access-control-error_handler).
 
 ### Blocks
 
-* [`basic_auth`](block/basic_auth)
-* [`beta_oauth2`](block/beta_oauth2)
-* [`jwt`](block/jwt)
-* [`oidc`](block/oidc)
-* [`saml`](block/saml)
+* [`basic_auth`](/configuration/block/basic_auth)
+* [`beta_oauth2`](/configuration/block/beta_oauth2)
+* [`jwt`](/configuration/block/jwt)
+* [`oidc`](/configuration/block/oidc)
+* [`saml`](/configuration/block/saml)
diff --git a/docs/website/content/3.observation/logging.md b/docs/website/content/3.observation/logging.md
index 45960273..fdf736ea 100644
--- a/docs/website/content/3.observation/logging.md
+++ b/docs/website/content/3.observation/logging.md
@@ -121,8 +121,8 @@ These fields are found in the [Log Type](#log-types) `couper_daemon` in addition
 
 | Name         |                 | Description                                                                                                                |
 |:-------------|:----------------|:---------------------------------------------------------------------------------------------------------------------------|
-| `"deadline"` |                 | shutdown parameter, see [Health Check](health)                                                        |
-| `"delay"`    |                 | shutdown parameter, see [Health Check](health)                                                        |
+| `"deadline"` |                 | shutdown parameter, see [Health Check](/observation/health)                                                        |
+| `"delay"`    |                 | shutdown parameter, see [Health Check](/observation/health)                                                        |
 | `"watch":`   |                 | field watching configuration file changes, logs with this field only appear if `watch=true`, more in [Settings](/configuration/block/settings) |
 |              | `{`             |                                                                                                                            |
 |              | `"max-retries"` | maximum retry count, see [Global Options](/configuration/command-line#global-options)                                                         |
diff --git a/errors/template.go b/errors/template.go
index d657d159..12b4cedc 100644
--- a/errors/template.go
+++ b/errors/template.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"sync"
 	"text/template"
 
 	"github.com/sirupsen/logrus"
@@ -61,8 +62,12 @@ func NewTemplateFromFile(path string, logger *logrus.Entry) (*Template, error) {
 	return NewTemplate(mime, fileName, tplFile, logger)
 }
 
+var setLoggerMu sync.Mutex // required for testing purposes
+
 // SetLogger updates the default templates with the configured "daemon" logger.
 func SetLogger(log *logrus.Entry) {
+	setLoggerMu.Lock()
+	defer setLoggerMu.Unlock()
 	DefaultJSON.log = log
 	DefaultHTML.log = log
 }
diff --git a/fuzz/server/http.go b/fuzz/server/http.go
index 94c9abbc..49cc1b80 100644
--- a/fuzz/server/http.go
+++ b/fuzz/server/http.go
@@ -82,7 +82,11 @@ settings {
 		panic("init error: " + err.Error())
 	}
 
-	servers, _ := server.NewServerList(cmdCtx, configFile.Context, log, configFile.Settings, &couperruntime.DefaultTimings, config)
+	servers, _, err := server.NewServers(cmdCtx, configFile.Context, log, configFile.Settings, &couperruntime.DefaultTimings, config)
+	if err != nil {
+		panic("init error: " + err.Error())
+	}
+
 	var addr string
 	for _, s := range servers {
 		if err = s.Listen(); err != nil {
diff --git a/handler/transport/tls.go b/handler/transport/tls.go
new file mode 100644
index 00000000..63e3a800
--- /dev/null
+++ b/handler/transport/tls.go
@@ -0,0 +1,64 @@
+package transport
+
+import (
+	"crypto/tls"
+
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/errors"
+	coupertls "github.com/avenga/couper/internal/tls"
+)
+
+// ReadCertificates parses an optional CA certificate or a client certificate / key pair.
+// It is valid to have just the client pair without the CA certificate since the system
+// Root CAs or the related Couper cli option MAY configure the related transport too.
+func ReadCertificates(conf *config.BackendTLS) (tls.Certificate, tls.Certificate, error) {
+	fail := func(err error) (tls.Certificate, tls.Certificate, error) {
+		return tls.Certificate{}, tls.Certificate{}, err
+	}
+
+	if conf == nil {
+		return fail(nil)
+	}
+
+	hasCA := conf.ServerCertificate != "" || conf.ServerCertificateFile != ""
+	hasClient := conf.ClientCertificate != "" || conf.ClientCertificateFile != ""
+	hasClientKey := conf.ClientPrivateKey != "" || conf.ClientPrivateKeyFile != ""
+
+	if !hasCA && !hasClient {
+		return fail(nil)
+	}
+
+	if hasClient && !hasClientKey {
+		return fail(errors.Configuration.Message("tls: missing client private key"))
+	}
+
+	var caCertificate, clientCertificate tls.Certificate
+
+	caCert, err := coupertls.ReadValueOrFile(conf.ServerCertificate, conf.ServerCertificateFile)
+	if err != nil && hasCA {
+		return fail(err)
+	}
+
+	clientCert, err := coupertls.ReadValueOrFile(conf.ClientCertificate, conf.ClientCertificateFile)
+	if err != nil && hasClient {
+		return fail(err)
+	}
+
+	clientKey, err := coupertls.ReadValueOrFile(conf.ClientPrivateKey, conf.ClientPrivateKeyFile)
+	if err != nil && (conf.ClientPrivateKey != "" || conf.ClientPrivateKeyFile != "") {
+		return fail(err)
+	}
+
+	if len(caCert) > 0 {
+		caCertificate, err = coupertls.ParseCertificate(caCert, nil)
+		if err != nil {
+			return fail(err)
+		}
+	}
+
+	if len(clientCert) > 0 {
+		clientCertificate, err = coupertls.ParseCertificate(clientCert, clientKey)
+	}
+
+	return caCertificate, clientCertificate, err
+}
diff --git a/handler/transport/transport.go b/handler/transport/transport.go
index c5b82b9a..f8750a61 100644
--- a/handler/transport/transport.go
+++ b/handler/transport/transport.go
@@ -3,7 +3,6 @@ package transport
 import (
 	"context"
 	"crypto/tls"
-	"crypto/x509"
 	"fmt"
 	"net"
 	"net/http"
@@ -17,6 +16,7 @@ import (
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/handler/ratelimit"
+	coupertls "github.com/avenga/couper/internal/tls"
 	"github.com/avenga/couper/telemetry"
 	"golang.org/x/net/http/httpproxy"
 )
@@ -24,7 +24,6 @@ import (
 // Config represents the transport <Config> object.
 type Config struct {
 	BackendName            string
-	Certificate            []byte
 	DisableCertValidation  bool
 	DisableConnectionReuse bool
 	HTTP2                  bool
@@ -37,6 +36,14 @@ type Config struct {
 	TTFBTimeout    time.Duration
 	Timeout        time.Duration
 
+	// TLS settings
+	// Certificate is passed to all backends from the related cli option.
+	Certificate []byte
+	// CACertificate contains a per backend configured one.
+	CACertificate tls.Certificate
+	// ClientCertificate holds the one the backend will send during tls handshake if required.
+	ClientCertificate tls.Certificate
+
 	// Dynamic values
 	Hostname string
 	Origin   string
@@ -45,14 +52,21 @@ type Config struct {
 
 // NewTransport creates a new <*http.Transport> object by the given <*Config>.
 func NewTransport(conf *Config, log *logrus.Entry) *http.Transport {
-	certPool, err := x509.SystemCertPool()
-	if err == nil {
-		certPool.AppendCertsFromPEM(conf.Certificate)
+	tlsConf := coupertls.DefaultTLSConfig()
+	if len(conf.Certificate) > 0 {
+		tlsConf.RootCAs.AppendCertsFromPEM(conf.Certificate)
+	}
+	if conf.CACertificate.Leaf == nil {
+		tlsConf.InsecureSkipVerify = conf.DisableCertValidation
+	} else {
+		tlsConf.RootCAs.AddCert(conf.CACertificate.Leaf)
 	}
 
-	tlsConf := &tls.Config{
-		InsecureSkipVerify: conf.DisableCertValidation,
-		RootCAs:            certPool,
+	if conf.ClientCertificate.Leaf != nil {
+		clientCert := &conf.ClientCertificate
+		tlsConf.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+			return clientCert, nil
+		}
 	}
 
 	if conf.Origin != conf.Hostname {
@@ -83,6 +97,7 @@ func NewTransport(conf *Config, log *logrus.Entry) *http.Transport {
 	var nextProto map[string]func(authority string, c *tls.Conn) http.RoundTripper
 	if !conf.HTTP2 {
 		nextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
+		tlsConf.NextProtos = nil
 	}
 
 	logEntry := log.WithField("type", "couper_connection")
diff --git a/internal/tls/certificate.go b/internal/tls/certificate.go
new file mode 100644
index 00000000..f6f0a104
--- /dev/null
+++ b/internal/tls/certificate.go
@@ -0,0 +1,95 @@
+package tls
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"strings"
+
+	"github.com/avenga/couper/errors"
+)
+
+// ParseCertificate reads a certificate from the given bytes.
+// Either as PEM format where chained ones are considered or just plain DER format.
+func ParseCertificate(cert, key []byte) (certificate tls.Certificate, err error) {
+	certBytes := cert[:]
+	// try PEM format
+	var keyDERBlock *pem.Block
+	for {
+		var certDERBlock *pem.Block
+		certDERBlock, certBytes = pem.Decode(certBytes)
+		if certDERBlock == nil {
+			if len(certificate.Certificate) > 0 {
+				certificate.Leaf, err = x509.ParseCertificate(certificate.Certificate[0])
+				if err != nil {
+					return certificate, err
+				}
+
+				// Option to combine both into one file.
+				if keyDERBlock != nil {
+					certificate.PrivateKey, err = parsePrivateKey(keyDERBlock.Bytes)
+					if err != nil {
+						return certificate, err
+					}
+				}
+			}
+			break
+		}
+		if certDERBlock.Type == "CERTIFICATE" {
+			certificate.Certificate = append(certificate.Certificate, certDERBlock.Bytes)
+		} else if strings.HasSuffix(certDERBlock.Type, " PRIVATE KEY") {
+			keyDERBlock = certDERBlock
+		}
+	}
+
+	// assume DER format
+	if len(certificate.Certificate) == 0 {
+		var x509Certificate *x509.Certificate
+		x509Certificate, err = x509.ParseCertificate(cert)
+		if err != nil {
+			return certificate, err
+		}
+
+		certificate = tls.Certificate{
+			Certificate: [][]byte{cert},
+			Leaf:        x509Certificate,
+		}
+	}
+
+	if certificate.PrivateKey == nil && len(key) > 0 {
+		keyBytes := key[:]
+		keyDERBlock, _ = pem.Decode(key)
+		if keyDERBlock != nil {
+			keyBytes = keyDERBlock.Bytes
+		}
+		certificate.PrivateKey, err = parsePrivateKey(keyBytes)
+	}
+
+	return certificate, err
+}
+
+// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
+// PKCS #1 private keys by default, while OpenSSL 1.0.0 generates PKCS #8 keys.
+// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
+func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
+	if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
+		return key, nil
+	}
+	if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
+		switch key := key.(type) {
+		case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
+			return key, nil
+		default:
+			return nil, errors.Configuration.Message("tls: found unknown private key type in PKCS#8 wrapping")
+		}
+	}
+	if key, err := x509.ParseECPrivateKey(der); err == nil {
+		return key, nil
+	}
+
+	return nil, errors.Configuration.Message("tls: failed to parse private key")
+}
diff --git a/internal/tls/cli/main.go b/internal/tls/cli/main.go
new file mode 100644
index 00000000..8d8507a1
--- /dev/null
+++ b/internal/tls/cli/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"github.com/avenga/couper/server"
+)
+
+func main() {
+	selfSigned, err := server.NewCertificate(time.Hour*12, nil, nil)
+	if err != nil {
+		panic(err)
+	}
+
+	_ = os.WriteFile("couperRootCA.crt", selfSigned.CACertificate.Certificate, 0644)
+	_ = os.WriteFile("couperRootCA.key", selfSigned.CACertificate.PrivateKey, 0644)
+	_ = os.WriteFile("couperServer.crt", selfSigned.ServerCertificate.Certificate, 0644)
+	_ = os.WriteFile("couperServer.key", selfSigned.ServerCertificate.PrivateKey, 0644)
+	_ = os.WriteFile("couperClient.crt", selfSigned.ClientCertificate.Certificate, 0644)
+	_ = os.WriteFile("couperClient.key", selfSigned.ClientCertificate.PrivateKey, 0644)
+	_ = os.WriteFile("couperIntermediate.crt",
+		append(selfSigned.ClientIntermediateCertificate.Certificate, selfSigned.CACertificate.Certificate...), 0644)
+
+	println("certificates generated...")
+}
diff --git a/internal/tls/config.go b/internal/tls/config.go
new file mode 100644
index 00000000..53747b5e
--- /dev/null
+++ b/internal/tls/config.go
@@ -0,0 +1,30 @@
+package tls
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+)
+
+func DefaultTLSConfig() *tls.Config {
+	cfg := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+		CipherSuites: []uint16{
+			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+		},
+		// Include h2 to allow connect http servers to automatically support http2.
+		// See: https://github.com/golang/go/blob/917c33fe8672116b04848cf11545296789cafd3b/src/net/http/server.go#L2724-L2731
+		NextProtos: []string{"h2"},
+	}
+
+	certPool, err := x509.SystemCertPool()
+	if err == nil {
+		cfg.RootCAs = certPool
+	}
+
+	return cfg
+}
diff --git a/internal/tls/read.go b/internal/tls/read.go
new file mode 100644
index 00000000..b1d33694
--- /dev/null
+++ b/internal/tls/read.go
@@ -0,0 +1,18 @@
+package tls
+
+import (
+	"os"
+
+	"github.com/avenga/couper/errors"
+)
+
+func ReadValueOrFile(value, filePath string) ([]byte, error) {
+	if value != "" && filePath != "" {
+		return nil, errors.Configuration.Message("both attributes provided: value and file")
+	} else if value != "" {
+		return []byte(value), nil
+	} else if filePath != "" {
+		return os.ReadFile(filePath)
+	}
+	return nil, errors.Configuration.Message("no attributes provided")
+}
diff --git a/logging/access_log.go b/logging/access_log.go
index 83f6e3ff..3f7accf6 100644
--- a/logging/access_log.go
+++ b/logging/access_log.go
@@ -129,10 +129,16 @@ func (log *AccessLog) Do(writer http.ResponseWriter, req *http.Request) {
 	}
 
 	requestFields["tls"] = req.TLS != nil
-	if req.URL.Scheme != "" {
-		requestFields["proto"] = req.URL.Scheme
-	} else if req.TLS != nil && req.TLS.HandshakeComplete {
+	if req.Proto == "HTTP/2.0" {
+		// TODO: any way to obtain the StreamID?
+		// https://github.com/golang/net/blob/master/http2/writesched.go
+		requestFields["h2"] = true
+	}
+
+	if req.TLS != nil && req.TLS.HandshakeComplete {
 		requestFields["proto"] = "https"
+	} else if req.URL.Scheme != "" {
+		requestFields["proto"] = req.URL.Scheme
 	}
 
 	if fields["port"] == "" {
diff --git a/profile.cov b/profile.cov
deleted file mode 100644
index 5f02b111..00000000
--- a/profile.cov
+++ /dev/null
@@ -1,1 +0,0 @@
-mode: set
diff --git a/server/http.go b/server/http.go
index ff0383b2..2e64639c 100644
--- a/server/http.go
+++ b/server/http.go
@@ -3,6 +3,7 @@ package server
 import (
 	"context"
 	"fmt"
+	"log"
 	"net"
 	"net/http"
 	"strings"
@@ -41,14 +42,18 @@ type HTTPServer struct {
 	timings    *runtime.HTTPTimings
 }
 
-// NewServerList creates a list of all configured HTTP server.
-func NewServerList(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, settings *config.Settings,
-	timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
+// NewServers returns a list of the created and configured HTTP(s) servers.
+func NewServers(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, settings *config.Settings,
+	timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func(), error) {
 
 	var list []*HTTPServer
 
 	for port, hosts := range srvConf {
-		list = append(list, New(cmdCtx, evalCtx, log, settings, timings, port, hosts))
+		srv, err := New(cmdCtx, evalCtx, log, settings, timings, port, hosts)
+		if err != nil {
+			return nil, nil, err
+		}
+		list = append(list, srv)
 	}
 
 	handleShutdownFn := func() {
@@ -56,12 +61,12 @@ func NewServerList(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, sett
 		time.Sleep(timings.ShutdownDelay + timings.ShutdownTimeout) // wait for max amount, TODO: feedback per server
 	}
 
-	return list, handleShutdownFn
+	return list, handleShutdownFn, nil
 }
 
-// New creates a configured HTTP server.
+// New creates an HTTP(S) server with configured router and middlewares.
 func New(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, settings *config.Settings,
-	timings *runtime.HTTPTimings, p runtime.Port, hosts runtime.Hosts) *HTTPServer {
+	timings *runtime.HTTPTimings, p runtime.Port, hosts runtime.Hosts) (*HTTPServer, error) {
 
 	logConf := *logging.DefaultConfig
 	logConf.TypeFieldKey = "couper_access"
@@ -70,11 +75,17 @@ func New(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, settings *conf
 	shutdownCh := make(chan struct{})
 
 	muxersList := make(muxers)
+	var serverTLS *config.ServerTLS
 	for host, muxOpts := range hosts {
 		mux := NewMux(muxOpts)
 		mux.registerHandler(mux.endpointRoot, []string{http.MethodGet}, settings.HealthPath, handler.NewHealthCheck(settings.HealthPath, shutdownCh))
-
 		muxersList[host] = mux
+
+		// TODO: refactor (hosts,muxOpts, etc) format type and usage
+		// serverOpts are all the same, pick first
+		if serverTLS == nil && muxOpts.ServerOptions != nil && muxOpts.ServerOptions.TLS != nil {
+			serverTLS = muxOpts.ServerOptions.TLS
+		}
 	}
 
 	httpSrv := &HTTPServer{
@@ -106,14 +117,23 @@ func New(cmdCtx, evalCtx context.Context, log logrus.FieldLogger, settings *conf
 	srv := &http.Server{
 		Addr:              ":" + p.String(),
 		ConnState:         httpSrv.onConnState,
+		ErrorLog:          newErrorLogWrapper(log),
 		Handler:           startTimeHandler,
 		IdleTimeout:       timings.IdleTimeout,
 		ReadHeaderTimeout: timings.ReadHeaderTimeout,
 	}
 
+	if serverTLS != nil {
+		tlsConfig, err := newTLSConfig(serverTLS, log)
+		if err != nil {
+			return nil, err
+		}
+		srv.TLSConfig = tlsConfig
+	}
+
 	httpSrv.srv = srv
 
-	return httpSrv
+	return httpSrv, nil
 }
 
 // Addr returns the listener address.
@@ -128,7 +148,11 @@ func (s *HTTPServer) Addr() string {
 func (s *HTTPServer) Listen() error {
 	if s.srv.Addr == "" {
 		s.srv.Addr = ":http"
+		if s.srv.TLSConfig != nil {
+			s.srv.Addr += "s"
+		}
 	}
+
 	ln, err := net.Listen("tcp4", s.srv.Addr)
 	if err != nil {
 		return err
@@ -140,7 +164,14 @@ func (s *HTTPServer) Listen() error {
 	go s.listenForCtx()
 
 	go func() {
-		if serveErr := s.srv.Serve(ln); serveErr != nil {
+		var serveErr error
+		if s.srv.TLSConfig != nil {
+			serveErr = s.srv.ServeTLS(s.listener, "", "")
+		} else {
+			serveErr = s.srv.Serve(ln)
+		}
+
+		if serveErr != nil {
 			if serveErr == http.ErrServerClosed {
 				s.log.Infof("%v: %s", serveErr, ln.Addr().String())
 			} else {
@@ -312,3 +343,19 @@ func getChildHandler(handler http.Handler) http.Handler {
 	}
 	return outer
 }
+
+// ErrorWrapper logs incoming Write bytes with the context filled logrus.FieldLogger.
+type ErrorWrapper struct{ l logrus.FieldLogger }
+
+func (e *ErrorWrapper) Write(p []byte) (n int, err error) {
+	msg := string(p)
+	if strings.HasSuffix(msg, " tls: unknown certificate") {
+		return len(p), nil // triggered on first browser connect for self signed certs; skip
+	}
+
+	e.l.Error(strings.TrimSpace(msg))
+	return len(p), nil
+}
+func newErrorLogWrapper(logger logrus.FieldLogger) *log.Logger {
+	return log.New(&ErrorWrapper{logger}, "", log.Lmsgprefix)
+}
diff --git a/server/tls.go b/server/tls.go
index c569df79..f13dc323 100644
--- a/server/tls.go
+++ b/server/tls.go
@@ -2,320 +2,244 @@ package server
 
 import (
 	"bytes"
-	"context"
-	"crypto/rand"
-	"crypto/rsa"
+	"crypto/md5"
 	"crypto/tls"
 	"crypto/x509"
-	"crypto/x509/pkix"
-	"encoding/pem"
+	"encoding/hex"
 	"fmt"
-	"log"
-	"math/big"
-	"net"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
-	"strconv"
-	"strings"
-	"sync"
 	"time"
 
-	"github.com/rs/xid"
 	"github.com/sirupsen/logrus"
 
 	"github.com/avenga/couper/config"
-	"github.com/avenga/couper/config/request"
-	"github.com/avenga/couper/config/runtime"
 	"github.com/avenga/couper/errors"
-	"github.com/avenga/couper/handler/middleware"
-	"github.com/avenga/couper/logging"
-	"github.com/avenga/couper/server/writer"
+	coupertls "github.com/avenga/couper/internal/tls"
 )
 
-type ListenPort string
-type Ports []string
-type TLSDevPorts map[ListenPort]Ports
-
-const TLSProxyOption = "https_dev_proxy"
+func requireClientAuth(config *config.ServerTLS) tls.ClientAuthType {
+	if config != nil && len(config.ClientCertificate) > 0 {
+		return tls.RequireAndVerifyClientCert
+	}
+	return tls.NoClientCert
+}
 
-var httpsDevProxyIDField = "x-" + xid.New().String()
+func newTLSConfig(config *config.ServerTLS, log logrus.FieldLogger) (*tls.Config, error) {
+	cfg := coupertls.DefaultTLSConfig()
+	cfg.RootCAs = x509.NewCertPool() // no system CA's
+	var leafOnlyCerts [][]byte
 
-func (tdp TLSDevPorts) Add(pair string) error {
-	ports := strings.Split(pair, ":")
-	if len(ports) != 2 {
-		return errors.Configuration.Messagef("%s: invalid port mapping: %s", TLSProxyOption, pair)
+	cfg.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
+		log.WithField("ClientHelloInfo", logrus.Fields{
+			"connection": logrus.Fields{
+				"client_ip": info.Conn.RemoteAddr().String(),
+				"server_ip": info.Conn.LocalAddr().String(),
+			},
+			"server_name":      info.ServerName,
+			"supported_protos": info.SupportedProtos,
+		}).Debug()
+		return nil, nil
 	}
-	for _, p := range ports {
-		if _, err := strconv.Atoi(p); err != nil {
-			return errors.Configuration.Messagef("%s: invalid format: %s", TLSProxyOption, pair).With(err)
+
+	cfg.ClientAuth = requireClientAuth(config)
+
+	for _, certConfig := range config.ServerCertificates {
+		cert, err := LoadServerCertificate(certConfig)
+		if err != nil {
+			return nil, err
 		}
-	}
 
-	if dp, exist := tdp[ListenPort(ports[1])]; exist && dp.Contains(ports[0]) {
-		return errors.Configuration.Messagef("https_dev_proxy: tls port already defined: %s", pair)
+		cfg.Certificates = append(cfg.Certificates, cert)
+	}
+
+	var leafCerts map[string][]byte
+	if cfg.ClientAuth == tls.RequireAndVerifyClientCert {
+		cfg.ClientCAs = x509.NewCertPool()
+		for _, certConfig := range config.ClientCertificate {
+			cert, clientCrt, err := LoadClientCertificate(certConfig)
+			if err != nil {
+				return nil, err
+			}
+
+			if cert.Leaf != nil {
+				cfg.ClientCAs.AddCert(cert.Leaf)
+			}
+
+			if clientCrt.Leaf != nil {
+				if cert.Leaf == nil {
+					leafOnlyCerts = append(leafOnlyCerts, clientCrt.Leaf.Raw)
+				} else {
+					if leafCerts == nil {
+						leafCerts = make(map[string][]byte)
+					}
+					leafCerts[checksum(cert.Leaf.Raw)] = clientCrt.Leaf.Raw
+				}
+			}
+		}
 	}
 
-	tdp[ListenPort(ports[1])] = append(tdp[ListenPort(ports[1])], ports[0])
-	return nil
-}
+	if len(leafCerts)+len(leafOnlyCerts) > 0 {
+		cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			// TODO: check if chains can be passed to prevent double verify
+			return verifyClientCertificate(cfg.ClientCAs, leafCerts, leafOnlyCerts, rawCerts)
+		}
 
-func (tdp TLSDevPorts) Get(p string) []string {
-	if result, exist := tdp[ListenPort(p)]; exist {
-		return result
+		// If one of the certificate blocks has no CA certificate but a leaf one then downgrade
+		// the clientCert option to prevent the tls module to call verify on its own.
+		if len(leafOnlyCerts) > 0 {
+			cfg.ClientAuth = tls.RequestClientCert
+		}
 	}
-	return nil
-}
 
-func (tp Ports) Contains(needle string) bool {
-	for _, p := range tp {
-		if p == needle {
-			return true
-		}
+	// fallback to the self-signed server certificate
+	if len(cfg.Certificates) == 0 {
+		selfSigned, _ := NewCertificate(time.Hour*12, nil, nil)
+		cfg.Certificates = append(cfg.Certificates, *selfSigned.Server)
 	}
-	return false
-}
 
-func (lp ListenPort) Port() runtime.Port {
-	i, _ := strconv.Atoi(string(lp))
-	return runtime.Port(i)
+	return cfg, nil
 }
 
-func NewTLSProxy(addr, port string, logger logrus.FieldLogger, settings *config.Settings) (*http.Server, error) {
-	origin, err := url.Parse(fmt.Sprintf("http://%s/", addr))
-	if err != nil {
-		return nil, err
+func LoadServerCertificate(config *config.ServerCertificate) (tls.Certificate, error) {
+	if config == nil {
+		return tls.Certificate{}, nil // currently triggers self signed fallback
 	}
-
-	logEntry := logger.WithField("type", "couper_access_tls")
-
-	httpProxy := httputil.NewSingleHostReverseProxy(origin)
-	httpProxy.Transport = &http.Transport{ // http.DefaultTransport /wo Proxy
-		DialContext: (&net.Dialer{
-			Timeout:   30 * time.Second,
-			KeepAlive: 30 * time.Second,
-		}).DialContext,
-		ForceAttemptHTTP2:     true,
-		MaxIdleConns:          100,
-		IdleConnTimeout:       90 * time.Second,
-		TLSHandshakeTimeout:   10 * time.Second,
-		ExpectContinueTimeout: 1 * time.Second,
+	cert, err := coupertls.ReadValueOrFile(config.PublicKey, config.PublicKeyFile)
+	if err != nil {
+		return tls.Certificate{}, err
 	}
 
-	headers := []string{"Connection", "Upgrade"}
-	accessLog := logging.NewAccessLog(&logging.Config{
-		RequestHeaders:  append(logging.DefaultConfig.RequestHeaders, headers...),
-		ResponseHeaders: append(logging.DefaultConfig.ResponseHeaders, headers...),
-	}, logEntry)
-
-	initialConfig, err := getTLSConfig(&tls.ClientHelloInfo{})
+	privateKey, err := coupertls.ReadValueOrFile(config.PrivateKey, config.PrivateKeyFile)
 	if err != nil {
-		return nil, err
+		return tls.Certificate{}, err
 	}
 
-	listener, err := net.Listen("tcp4", ":"+port)
+	// Try PEM encoded
+	certificate, err := tls.X509KeyPair(cert, privateKey)
+	if err == nil {
+		certificate.Leaf, err = x509.ParseCertificate(certificate.Certificate[0])
+		return certificate, err
+	} // otherwise assume DER
+	x509certificate, err := x509.ParseCertificate(cert)
 	if err != nil {
-		return nil, err
+		return tls.Certificate{}, err
 	}
-
-	uidFn := middleware.NewUIDFunc(settings.RequestIDFormat)
-
-	tlsServer := &http.Server{
-		Addr:     ":" + port,
-		ErrorLog: newErrorLogWrapper(logEntry),
-		Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-			uid := uidFn()
-			req.Header.Set(httpsDevProxyIDField, uid)
-
-			ctx := context.WithValue(req.Context(), request.ServerName, "couper_tls")
-			ctx = context.WithValue(ctx, request.UID, uid)
-			ctx = context.WithValue(ctx, request.StartTime, time.Now())
-
-			req.Header.Set("Forwarded", fmt.Sprintf("for=%s;proto=https;host=%s;by=%s", req.RemoteAddr, req.Host, listener.Addr().String()))
-			req.Header.Set("Via", "couper-https-dev-proxy")
-			req.Header.Set("X-Forwarded-For", req.RemoteAddr+", "+listener.Addr().String())
-			req.Header.Set("X-Forwarded-Host", req.Host)
-			req.Header.Set("X-Forwarded-Proto", "https")
-
-			req.URL.Host = req.Host
-
-			respW := writer.NewResponseWriter(rw, "")
-			outreq := req.WithContext(ctx)
-			httpProxy.ServeHTTP(respW, outreq)
-			accessLog.Do(respW, outreq)
-		}),
-		TLSConfig: initialConfig,
+	key, err := x509.ParsePKCS8PrivateKey(privateKey)
+	if err != nil {
+		return tls.Certificate{}, err
 	}
 
-	go tlsServer.ServeTLS(listener, "", "")
-	return tlsServer, err
+	// TODO: Check for public <> private key type match as seen in tls.X509KeyPair() method.
+	return tls.Certificate{
+		Certificate: [][]byte{x509certificate.Raw},
+		Leaf:        x509certificate,
+		PrivateKey:  key,
+	}, nil
 }
 
-var tlsConfigurations = map[string]*tls.Config{}
-var tlsLock = sync.RWMutex{}
-
-// getTLSConfig returns a clone from created or memorized tls configuration due to
-// transport protocol upgrades / clones later on which would result in data races.
-func getTLSConfig(info *tls.ClientHelloInfo) (*tls.Config, error) {
-	var hosts []string
-	key := "localhost"
-	if info.ServerName != "" {
-		hosts = append(hosts, info.ServerName)
-		key = info.ServerName
+func LoadClientCertificate(config *config.ClientCertificate) (tls.Certificate, tls.Certificate, error) {
+	fail := func(err error) (tls.Certificate, tls.Certificate, error) {
+		return tls.Certificate{}, tls.Certificate{}, errors.Configuration.With(err).Message("client_certificate:")
 	}
 
-	// Global lock to prevent recreate loop for new connections.
-	tlsLock.Lock()
-	defer tlsLock.Unlock()
-
-	tlsConfig, ok := tlsConfigurations[key]
-	if !ok || tlsConfig.Certificates[0].Leaf.NotAfter.Before(time.Now()) {
-		selfSigned, err := NewCertificate(time.Hour*24, hosts, nil)
-		if err != nil {
-			return nil, err
-		}
-		tlsConf := &tls.Config{
-			Certificates:       []tls.Certificate{*selfSigned.Server},
-			GetConfigForClient: getTLSConfig,
-		}
-
-		tlsConfigurations[key] = tlsConf
-		return tlsConf.Clone(), nil
+	if config == nil {
+		return tls.Certificate{}, tls.Certificate{}, nil
 	}
 
-	return tlsConfig.Clone(), nil
-}
-
-type SelfSignedCertificate struct {
-	CA     []byte // PEM encoded
-	Server *tls.Certificate
-}
+	hasLeaf := config.Leaf != "" || config.LeafFile != ""
 
-// NewCertificate creates a certificate with given hosts and duration.
-// If no hosts are provided all localhost variants will be used.
-func NewCertificate(duration time.Duration, hosts []string, notBefore *time.Time) (*SelfSignedCertificate, error) {
-	caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
-	if err != nil {
-		return nil, err
+	caCert, err := coupertls.ReadValueOrFile(config.CA, config.CAFile)
+	if err != nil && !hasLeaf {
+		return fail(err)
 	}
 
-	if len(hosts) == 0 {
-		hosts = []string{"127.0.0.1", "::1", "localhost", "0.0.0.0", "::0"}
+	leafCert, err := coupertls.ReadValueOrFile(config.Leaf, config.LeafFile)
+	if err != nil && hasLeaf { // since its optional, if given
+		return fail(err)
 	}
 
-	if notBefore == nil {
-		n := time.Now()
-		notBefore = &n
+	// try PEM encoded
+	var leafCertificate tls.Certificate
+	if len(leafCert) > 0 {
+		leafCertificate, err = coupertls.ParseCertificate(leafCert, nil)
+		if err != nil {
+			return fail(err)
+		}
 	}
-	notAfter := notBefore.Add(duration)
 
-	serialNumber, err := newSerialNumber()
-	if err != nil {
-		log.Fatalf("failed to generate serial number: %s", err)
+	if len(caCert) == 0 && leafCertificate.Leaf != nil {
+		return tls.Certificate{}, leafCertificate, nil
 	}
 
-	template := x509.Certificate{
-		Subject: pkix.Name{
-			Country:            []string{"DE"},
-			Organization:       []string{"Couper"},
-			OrganizationalUnit: []string{"Development"},
-		},
-		NotBefore: *notBefore,
-		NotAfter:  notAfter,
-
-		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
-		BasicConstraintsValid: true,
-	}
+	caCertificate, err := coupertls.ParseCertificate(caCert, nil)
 
-	// self CA
-	caTemplate := template
-	caTemplate.SerialNumber = serialNumber
-	caTemplate.IsCA = true
-	caTemplate.KeyUsage |= x509.KeyUsageCertSign
+	return caCertificate, leafCertificate, err
+}
 
-	caDER, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caPrivateKey.PublicKey, caPrivateKey)
-	if err != nil {
-		log.Fatalf("Failed to create certificate: %s", err)
+// verifyClientCertificate will be called as soon as a client_certificate block contains just one leaf certificate
+// without a CA certificate. Other possible client_certificate blocks with CA certificate must still be validated.
+// If there is no "leaf-only" client_certificate block the tls package will do the verification.
+func verifyClientCertificate(caPool *x509.CertPool, leafs map[string][]byte, leafsOnly [][]byte, rawCerts [][]byte) error {
+	for _, leaf := range leafsOnly {
+		for _, cert := range rawCerts {
+			if bytes.Equal(cert, leaf) {
+				return nil
+			}
+		}
 	}
 
-	caCert := &bytes.Buffer{}
-	err = pem.Encode(caCert, &pem.Block{Type: "CERTIFICATE", Bytes: caDER})
-	if err != nil {
-		return nil, err
+	// unfortunately parsing happens twice, already before calling this callback
+	certs := make([]*x509.Certificate, len(rawCerts))
+	var err error
+	for i, asn1Data := range rawCerts {
+		if certs[i], err = x509.ParseCertificate(asn1Data); err != nil {
+			return fmt.Errorf("tls: failed to parse client certificate: " + err.Error())
+		}
 	}
 
-	caKey := &bytes.Buffer{}
-	err = pem.Encode(caKey, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivateKey)})
+	chains, err := verify(caPool, certs)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	// server certificate
-	srvPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
-	if err != nil {
-		return nil, err
+	// Determine the requirement to verify leaf equality with CA <> leaf mapping.
+	var checkLeaf bool
+	var leaf []byte
+	if leafs != nil && len(chains) > 0 && len(chains[0]) > 1 {
+		leaf, checkLeaf = leafs[checksum(chains[0][1].Raw)]
 	}
 
-	srvTemplate := template
-	srvTemplate.SerialNumber, err = newSerialNumber()
-	if err != nil {
-		return nil, err
+	if !checkLeaf {
+		return nil
 	}
 
-	for _, h := range hosts {
-		if ip := net.ParseIP(h); ip != nil {
-			srvTemplate.IPAddresses = append(srvTemplate.IPAddresses, ip)
-		} else {
-			srvTemplate.DNSNames = append(srvTemplate.DNSNames, h)
+	for _, cert := range rawCerts {
+		if bytes.Equal(cert, leaf) {
+			return nil
 		}
 	}
+	return fmt.Errorf("tls: client leaf certificate mismatch")
+}
 
-	srvDER, err := x509.CreateCertificate(rand.Reader, &srvTemplate, &caTemplate, &srvPrivateKey.PublicKey, caPrivateKey)
-	if err != nil {
-		log.Fatalf("Failed to create certificate: %s", err)
+func verify(caPool *x509.CertPool, certs []*x509.Certificate) ([][]*x509.Certificate, error) {
+	opts := x509.VerifyOptions{
+		Roots:         caPool,
+		CurrentTime:   time.Now(),
+		Intermediates: x509.NewCertPool(),
+		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
 	}
 
-	srvCert := &bytes.Buffer{}
-	err = pem.Encode(srvCert, &pem.Block{Type: "CERTIFICATE", Bytes: srvDER})
-	if err != nil {
-		return nil, err
+	for _, cert := range certs[1:] {
+		opts.Intermediates.AddCert(cert)
 	}
 
-	srvKey := &bytes.Buffer{}
-	err = pem.Encode(srvKey, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(srvPrivateKey)})
+	chains, err := certs[0].Verify(opts)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("tls: failed to verify client certificate: " + err.Error())
 	}
-
-	cert, err := tls.X509KeyPair(srvCert.Bytes(), srvKey.Bytes())
-	if err != nil {
-		return nil, err
-	}
-	cert.Leaf, err = x509.ParseCertificate(srvDER)
-	return &SelfSignedCertificate{
-		CA:     caCert.Bytes(),
-		Server: &cert,
-	}, err
-}
-
-func newSerialNumber() (*big.Int, error) {
-	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
-	return rand.Int(rand.Reader, serialNumberLimit)
+	return chains, nil
 }
 
-// ErrorWrapper logs incoming Write bytes with the context filled logrus.FieldLogger.
-type ErrorWrapper struct{ l logrus.FieldLogger }
-
-func (e *ErrorWrapper) Write(p []byte) (n int, err error) {
-	msg := string(p)
-	if strings.HasSuffix(msg, " tls: unknown certificate") ||
-		strings.HasPrefix(msg, "http: TLS handshake error") {
-		return len(p), nil // triggered on first browser connect for self signed certs; skip
-	}
-	e.l.Error(msg)
-	return len(p), nil
-}
-func newErrorLogWrapper(logger logrus.FieldLogger) *log.Logger {
-	return log.New(&ErrorWrapper{logger}, "", log.Lmsgprefix)
+func checksum(b []byte) string {
+	sum := md5.Sum(b)
+	return hex.EncodeToString(sum[:])
 }
diff --git a/server/tls_certificate.go b/server/tls_certificate.go
new file mode 100644
index 00000000..7e4babf0
--- /dev/null
+++ b/server/tls_certificate.go
@@ -0,0 +1,198 @@
+package server
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"log"
+	"math/big"
+	"net"
+	"strconv"
+	"sync/atomic"
+	"time"
+)
+
+type SelfSignedCertificate struct {
+	CA                            *tls.Certificate
+	CACertificate                 PEM
+	Server                        *tls.Certificate
+	ServerCertificate             PEM
+	ServerPrivateKey              []byte
+	Client                        *tls.Certificate
+	ClientCertificate             PEM
+	ClientPrivateKey              []byte
+	ClientIntermediateCertificate PEM
+	ClientIntermediate            *tls.Certificate
+}
+
+type PEM struct {
+	Certificate []byte
+	PrivateKey  []byte
+}
+
+var caCount uint32 = 1
+
+// NewCertificate creates a certificate with given hosts and duration.
+// If no hosts are provided all localhost variants will be used.
+func NewCertificate(duration time.Duration, hosts []string, notBefore *time.Time) (*SelfSignedCertificate, error) {
+	parentName := "rootCA_" + strconv.Itoa(int(atomic.LoadUint32(&caCount)))
+	defer atomic.AddUint32(&caCount, 1)
+
+	rootCA, rootPEM, err := newCertificateAuthority(parentName, "", nil, nil)
+	if err != nil {
+		log.Fatalf("Failed to create certificate: %s", err)
+	}
+
+	// server
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+
+	template := defaultTemplate()
+
+	if notBefore == nil {
+		n := time.Now()
+		notBefore = &n
+	}
+	template.NotAfter = notBefore.Add(duration)
+
+	if len(hosts) == 0 {
+		hosts = []string{"127.0.0.1", "::1", "localhost", "0.0.0.0", "::0"}
+	}
+
+	for _, h := range hosts {
+		if ip := net.ParseIP(h); ip != nil {
+			template.IPAddresses = append(template.IPAddresses, ip)
+		} else {
+			template.DNSNames = append(template.DNSNames, h)
+		}
+	}
+
+	srvDER, err := x509.CreateCertificate(rand.Reader, &template, rootCA.Leaf, &privateKey.PublicKey, rootCA.PrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	srvCrt, srvKeyBytes, srvPEM, err := newCertificateFromDER(srvDER, privateKey)
+
+	// intermediate
+	intermediateName := "intermediateCA_" + strconv.Itoa(int(atomic.LoadUint32(&caCount)))
+	interCA, interPEM, err := newCertificateAuthority(intermediateName, parentName, rootCA.Leaf.PublicKey, rootCA.PrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// client
+	privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+
+	template = defaultTemplate()
+	template.NotAfter = notBefore.Add(duration)
+	template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
+	template.MaxPathLenZero = true
+
+	clientDER, err := x509.CreateCertificate(rand.Reader, &template, interCA.Leaf, &privateKey.PublicKey, interCA.PrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	clientCrt, ClientKeyBytes, clientPEM, err := newCertificateFromDER(clientDER, privateKey)
+
+	return &SelfSignedCertificate{
+		CA:                            rootCA,
+		CACertificate:                 *rootPEM,
+		Server:                        srvCrt,
+		ServerCertificate:             *srvPEM,
+		ServerPrivateKey:              srvKeyBytes,
+		Client:                        clientCrt,
+		ClientCertificate:             *clientPEM,
+		ClientPrivateKey:              ClientKeyBytes,
+		ClientIntermediate:            interCA,
+		ClientIntermediateCertificate: *interPEM,
+	}, err
+}
+
+func newCertificateAuthority(name, parentName string, publicKey any, privateKey crypto.PrivateKey) (*tls.Certificate, *PEM, error) {
+	pubKey := publicKey
+	pathLen := 2
+	if _, ok := privateKey.(*ecdsa.PrivateKey); !ok {
+		key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+		if err != nil {
+			return nil, nil, err
+		}
+		pubKey = &key.PublicKey
+		privateKey = key
+		pathLen = 1
+	}
+
+	template := defaultTemplate()
+	template.IsCA = true
+	template.KeyUsage |= x509.KeyUsageCertSign | x509.KeyUsageCRLSign
+	template.NotAfter = template.NotBefore.Add(time.Hour * 24)
+	template.Subject = pkix.Name{
+		CommonName:         name,
+		Country:            template.Subject.Country,
+		Organization:       template.Subject.Organization,
+		OrganizationalUnit: template.Subject.OrganizationalUnit,
+	}
+	template.BasicConstraintsValid = true
+	template.MaxPathLen = pathLen
+
+	if parentName != "" {
+		template.Issuer = template.Subject
+		template.Issuer.CommonName = parentName
+	}
+
+	caDER, err := x509.CreateCertificate(rand.Reader, &template, &template, pubKey, privateKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	cert, _, certPEM, err := newCertificateFromDER(caDER, privateKey)
+	return cert, certPEM, err
+}
+
+func newCertificateFromDER(caDER []byte, key any) (*tls.Certificate, []byte, *PEM, error) {
+	caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})
+
+	privBytes, err := x509.MarshalPKCS8PrivateKey(key)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
+
+	certificate, err := tls.X509KeyPair(caPEM, keyPEM)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	certificate.Leaf, err = x509.ParseCertificate(caDER)
+	return &certificate, privBytes, &PEM{caPEM, keyPEM}, err
+}
+
+func newSerialNumber() *big.Int {
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	i, _ := rand.Int(rand.Reader, serialNumberLimit)
+	return i
+}
+
+func defaultTemplate() x509.Certificate {
+	return x509.Certificate{
+		Subject: pkix.Name{
+			Country:            []string{"DE"},
+			Organization:       []string{"Couper"},
+			OrganizationalUnit: []string{"Development"},
+		},
+		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+		Issuer:       pkix.Name{CommonName: "github/avenga/couper/server"},
+		KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		NotBefore:    time.Now(),
+		SerialNumber: newSerialNumber(),
+	}
+}
diff --git a/server/tls_dev_proxy.go b/server/tls_dev_proxy.go
new file mode 100644
index 00000000..d229f106
--- /dev/null
+++ b/server/tls_dev_proxy.go
@@ -0,0 +1,179 @@
+package server
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/rs/xid"
+	"github.com/sirupsen/logrus"
+
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/handler/middleware"
+	"github.com/avenga/couper/logging"
+	"github.com/avenga/couper/server/writer"
+)
+
+type ListenPort string
+type Ports []string
+type TLSDevPorts map[ListenPort]Ports
+
+const TLSProxyOption = "https_dev_proxy"
+
+var httpsDevProxyIDField = "x-" + xid.New().String()
+
+func (tdp TLSDevPorts) Add(pair string) error {
+	ports := strings.Split(pair, ":")
+	if len(ports) != 2 {
+		return errors.Configuration.Messagef("%s: invalid port mapping: %s", TLSProxyOption, pair)
+	}
+	for _, p := range ports {
+		if _, err := strconv.Atoi(p); err != nil {
+			return errors.Configuration.Messagef("%s: invalid format: %s", TLSProxyOption, pair).With(err)
+		}
+	}
+
+	if dp, exist := tdp[ListenPort(ports[1])]; exist && dp.Contains(ports[0]) {
+		return errors.Configuration.Messagef("https_dev_proxy: tls port already defined: %s", pair)
+	}
+
+	tdp[ListenPort(ports[1])] = append(tdp[ListenPort(ports[1])], ports[0])
+	return nil
+}
+
+func (tdp TLSDevPorts) Get(p string) []string {
+	if result, exist := tdp[ListenPort(p)]; exist {
+		return result
+	}
+	return nil
+}
+
+func (tp Ports) Contains(needle string) bool {
+	for _, p := range tp {
+		if p == needle {
+			return true
+		}
+	}
+	return false
+}
+
+func (lp ListenPort) Port() runtime.Port {
+	i, _ := strconv.Atoi(string(lp))
+	return runtime.Port(i)
+}
+
+func NewTLSProxy(addr, port string, logger logrus.FieldLogger, settings *config.Settings) (*http.Server, error) {
+	origin, err := url.Parse(fmt.Sprintf("http://%s/", addr))
+	if err != nil {
+		return nil, err
+	}
+
+	logEntry := logger.WithField("type", "couper_access_tls")
+
+	httpProxy := httputil.NewSingleHostReverseProxy(origin)
+	httpProxy.Transport = &http.Transport{ // http.DefaultTransport /wo Proxy
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).DialContext,
+		ForceAttemptHTTP2:     true,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+	}
+
+	headers := []string{"Connection", "Upgrade"}
+	accessLog := logging.NewAccessLog(&logging.Config{
+		RequestHeaders:  append(logging.DefaultConfig.RequestHeaders, headers...),
+		ResponseHeaders: append(logging.DefaultConfig.ResponseHeaders, headers...),
+	}, logEntry)
+
+	initialConfig, err := getTLSConfig(&tls.ClientHelloInfo{})
+	if err != nil {
+		return nil, err
+	}
+
+	listener, err := net.Listen("tcp4", ":"+port)
+	if err != nil {
+		return nil, err
+	}
+
+	uidFn := middleware.NewUIDFunc(settings.RequestIDFormat)
+
+	tlsServer := &http.Server{
+		Addr:     ":" + port,
+		ErrorLog: newErrorLogWrapper(logEntry),
+		Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+			uid := uidFn()
+			req.Header.Set(httpsDevProxyIDField, uid)
+
+			ctx := context.WithValue(req.Context(), request.ServerName, "couper_tls")
+			ctx = context.WithValue(ctx, request.UID, uid)
+			ctx = context.WithValue(ctx, request.StartTime, time.Now())
+
+			req.Header.Set("Forwarded", fmt.Sprintf("for=%s;proto=https;host=%s;by=%s", req.RemoteAddr, req.Host, listener.Addr().String()))
+			req.Header.Set("Via", "couper-https-dev-proxy")
+			req.Header.Set("X-Forwarded-For", req.RemoteAddr+", "+listener.Addr().String())
+			req.Header.Set("X-Forwarded-Host", req.Host)
+			req.Header.Set("X-Forwarded-Proto", "https")
+
+			req.URL.Host = req.Host
+
+			respW := writer.NewResponseWriter(rw, "")
+			outreq := req.WithContext(ctx)
+			httpProxy.ServeHTTP(respW, outreq)
+			accessLog.Do(respW, outreq)
+		}),
+		TLSConfig: initialConfig,
+	}
+
+	go tlsServer.ServeTLS(listener, "", "")
+	return tlsServer, err
+}
+
+var tlsConfigurations = map[string]*tls.Config{}
+var tlsLock = sync.RWMutex{}
+
+// getTLSConfig returns a clone from created or memorized tls configuration due to
+// transport protocol upgrades / clones later on which would result in data races.
+func getTLSConfig(info *tls.ClientHelloInfo) (*tls.Config, error) {
+	var hosts []string
+	key := "localhost"
+	if info.ServerName != "" {
+		hosts = append(hosts, info.ServerName)
+		key = info.ServerName
+	}
+
+	// Global lock to prevent recreate loop for new connections.
+	tlsLock.Lock()
+	defer tlsLock.Unlock()
+
+	tlsConfig, ok := tlsConfigurations[key]
+	if !ok || tlsConfig.Certificates[0].Leaf.NotAfter.Before(time.Now()) {
+		selfSigned, err := NewCertificate(time.Hour*24, hosts, nil)
+		if err != nil {
+			return nil, err
+		}
+		tlsConf := &tls.Config{
+			Certificates:       []tls.Certificate{*selfSigned.Server},
+			GetConfigForClient: getTLSConfig,
+		}
+
+		tlsConfigurations[key] = tlsConf
+		return tlsConf.Clone(), nil
+	}
+
+	return tlsConfig.Clone(), nil
+}


Test patch:
diff --git a/command/run_test.go b/command/run_test.go
index 05e3a960..aeefe0c5 100644
--- a/command/run_test.go
+++ b/command/run_test.go
@@ -3,7 +3,6 @@ package command
 import (
 	"context"
 	"crypto/tls"
-	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -259,7 +258,7 @@ func TestArgs_CAFile(t *testing.T) {
 
 	tmpFile, err := os.CreateTemp("", "ca.cert")
 	helper.Must(err)
-	_, err = tmpFile.Write(selfSigned.CA)
+	_, err = tmpFile.Write(selfSigned.CACertificate.Certificate)
 	helper.Must(err)
 	helper.Must(tmpFile.Close())
 	defer os.Remove(tmpFile.Name())
@@ -311,7 +310,6 @@ definitions {
 
 	port := couperFile.Settings.DefaultPort
 
-	fmt.Println(">>>>> START 2", time.Now())
 	// ensure the previous tests aren't listening
 	test.WaitForClosedPort(port)
 	go func() {
@@ -406,7 +404,7 @@ func TestReadCAFile(t *testing.T) {
 	ssc, err := server.NewCertificate(time.Minute, nil, nil)
 	helper.Must(err)
 
-	_, err = malformedFile.Write(ssc.CA[:100]) // incomplete
+	_, err = malformedFile.Write(ssc.CACertificate.Certificate[:100]) // incomplete
 	helper.Must(err)
 
 	_, err = readCertificateFile(malformedFile.Name())
diff --git a/config/configload/merge_internal_test.go b/config/configload/merge_internal_test.go
new file mode 100644
index 00000000..749ae269
--- /dev/null
+++ b/config/configload/merge_internal_test.go
@@ -0,0 +1,241 @@
+package configload
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/hashicorp/hcl/v2/hclwrite"
+
+	"github.com/avenga/couper/config/parser"
+	"github.com/avenga/couper/internal/test"
+)
+
+func Test_mergeServers_ServerTLS(t *testing.T) {
+	tests := []struct {
+		name    string
+		content []string
+		want    string
+		wantErr bool
+	}{
+		{"normal, one file", []string{`server {
+  tls {
+    server_certificate {
+      attr1 = "val1"
+    }
+  }
+}`}, `server {
+  tls {
+    server_certificate {
+      attr1 = "val1"
+    }
+  }
+}
+`, false},
+		{"two files, override", []string{`server {
+  tls {
+    server_certificate {
+      attr1 = "val1"
+    }
+  }
+}`, `server {
+  tls {
+    server_certificate {
+      attr2 = "val2"
+    }
+  }
+}`}, `server {
+  tls {
+    server_certificate {
+      attr2 = "val2"
+    }
+  }
+}
+`, false},
+		{"two files, merge", []string{`server {
+  tls {
+    server_certificate "example1.com" {
+      attr1 = "val1"
+    }
+  }
+}`, `server {
+  tls {
+    server_certificate "example2.com" {
+      attr2 = "val2"
+    }
+  }
+}`}, `server {
+  tls {
+    server_certificate "example1.com" {
+      attr1 = "val1"
+    }
+    server_certificate "example2.com" {
+      attr2 = "val2"
+    }
+  }
+}
+`, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(st *testing.T) {
+			hlp := test.New(st)
+
+			parsedBodies, err := parseBodies(tt.content)
+			hlp.Must(err)
+
+			blocks, err := mergeServers(parsedBodies, nil)
+			hlp.Must(err)
+
+			result := writeBlocks(blocks)
+
+			if (err != nil) != tt.wantErr {
+				t.Errorf("bodiesToConfig() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if diff := cmp.Diff(result, tt.want); diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}
+
+func Test_mergeDefinitions_BackendTLS(t *testing.T) {
+	tests := []struct {
+		name    string
+		content []string
+		want    string
+		wantErr bool
+	}{
+		{"normal, one file", []string{`definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "same.crt"
+    }
+  }
+}`}, `definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "same.crt"
+    }
+  }
+}
+`, false},
+		{"two files, replace", []string{`definitions {
+  backend "one" {
+    origin = "https://localhost"
+    tls {
+      server_ca_certificate_file = "one.crt"
+    }
+  }
+}`, `definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "two.crt"
+    }
+  }
+}`}, `definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "two.crt"
+    }
+  }
+}
+`, false},
+		{"two files, append", []string{`definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "one.crt"
+    }
+  }
+}`, `definitions {
+  backend "two" {
+    tls {
+      server_ca_certificate_file = "two.crt"
+    }
+  }
+}`}, `definitions {
+  backend "one" {
+    tls {
+      server_ca_certificate_file = "one.crt"
+    }
+  }
+  backend "two" {
+    tls {
+      server_ca_certificate_file = "two.crt"
+    }
+  }
+}
+`, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(st *testing.T) {
+			hlp := test.New(st)
+
+			parsedBodies, err := parseBodies(tt.content)
+			hlp.Must(err)
+
+			block, _, err := mergeDefinitions(parsedBodies)
+			hlp.Must(err)
+
+			result := writeBlocks(hclsyntax.Blocks{block})
+
+			if (err != nil) != tt.wantErr {
+				t.Errorf("bodiesToConfig() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if diff := cmp.Diff(result, tt.want); diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}
+
+func writeBlocks(blocks hclsyntax.Blocks) string {
+	f := hclwrite.NewEmptyFile()
+	root := f.Body()
+
+	for _, block := range blocks {
+		appendBlock(root, block)
+	}
+
+	b := &bytes.Buffer{}
+	_, _ = f.WriteTo(b)
+	return b.String()
+}
+
+func parseBodies(bodies []string) ([]*hclsyntax.Body, error) {
+	var parsedBodies []*hclsyntax.Body
+	for _, bodyStr := range bodies {
+		body, err := parser.Load([]byte(bodyStr), "")
+		if err != nil {
+			return nil, err
+		}
+		parsedBodies = append(parsedBodies, body)
+	}
+	return parsedBodies, nil
+}
+
+func appendBlock(parent *hclwrite.Body, block *hclsyntax.Block) {
+	writeBlock := gohcl.EncodeAsBlock(block, block.Type)
+	writeBlock.SetLabels(block.Labels)
+
+	for _, subBlock := range block.Body.Blocks {
+		appendBlock(writeBlock.Body(), subBlock)
+	}
+
+	appendAttrs(writeBlock.Body(), block.Body.Attributes)
+
+	parent.AppendBlock(writeBlock)
+}
+
+func appendAttrs(parent *hclwrite.Body, attributes hclsyntax.Attributes) {
+	for _, attr := range attributes {
+		v, _ := attr.Expr.Value(&hcl.EvalContext{})
+		parent.SetAttributeValue(attr.Name, v)
+	}
+}
diff --git a/config/configload/validate_test.go b/config/configload/validate_test.go
index ecbebde2..2099a16a 100644
--- a/config/configload/validate_test.go
+++ b/config/configload/validate_test.go
@@ -288,6 +288,43 @@ func Test_validateBody(t *testing.T) {
 			 }`,
 			"couper.hcl:5,15-20: backend labels must be unique; ",
 		},
+		{
+			"backend disable cert validation with tls block and server_ca_certificate",
+			`server {}
+			 definitions {
+			   backend "foo" {
+					disable_certificate_validation = false # value does not matter
+					tls {
+						server_ca_certificate = "asdf"
+					}
+			   }
+			 }`,
+			"couper.hcl:5,6-7,7: configured 'disable_certificate_validation' along with 'server_ca_certificate' attribute; ",
+		},
+		{
+			"backend disable cert validation with tls block and server_ca_certificate_file",
+			`server {}
+			 definitions {
+			   backend "foo" {
+					disable_certificate_validation = true # value does not matter
+					tls {
+						server_ca_certificate_file = "asdf.crt"
+					}
+			   }
+			 }`,
+			"couper.hcl:5,6-7,7: configured 'disable_certificate_validation' along with 'server_ca_certificate_file' attribute; ",
+		},
+		{
+			"backend disable cert validation with tls block and w/o server_ca_certificate*",
+			`server {}
+			 definitions {
+			   backend "foo" {
+					disable_certificate_validation = true # value does not matter
+					tls {}
+			   }
+			 }`,
+			"",
+		},
 		{
 			"duplicate proxy labels",
 			`server {}
diff --git a/handler/transport/tls_test.go b/handler/transport/tls_test.go
new file mode 100644
index 00000000..3960fa4a
--- /dev/null
+++ b/handler/transport/tls_test.go
@@ -0,0 +1,97 @@
+package transport_test
+
+import (
+	"crypto/tls"
+	"os"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/handler/transport"
+	"github.com/avenga/couper/internal/test"
+	"github.com/avenga/couper/server"
+)
+
+func TestReadCertificates(t *testing.T) {
+	helper := test.New(t)
+
+	now := time.Now()
+	selfSignedCert, serr := server.NewCertificate(time.Hour, nil, &now)
+	helper.Must(serr)
+	t.Logf("generated certificates in %s", time.Since(now).String())
+
+	// obsolete for this test
+	selfSignedCert.CA.PrivateKey = nil
+	clientCertOnly := *selfSignedCert.Client
+	clientCertOnly.PrivateKey = nil
+
+	for _, format := range []string{"DER", "PEM"} {
+		var caCertBytes, certBytes []byte
+		switch format {
+		case "DER":
+			caCertBytes = selfSignedCert.CA.Certificate[0]
+			certBytes = selfSignedCert.Client.Certificate[0]
+		case "PEM":
+			caCertBytes = selfSignedCert.CACertificate.Certificate
+			certBytes = selfSignedCert.ClientCertificate.Certificate
+		}
+
+		pattern := "couper_test_tls_read_certs_" + format
+		tmpCaCertFile, ferr := os.CreateTemp("", pattern)
+		helper.Must(ferr)
+		_, ferr = tmpCaCertFile.Write(caCertBytes)
+		helper.Must(ferr)
+		helper.Must(tmpCaCertFile.Close())
+		defer os.Remove(tmpCaCertFile.Name())
+
+		tmpCertFile, ferr := os.CreateTemp("", pattern)
+		helper.Must(ferr)
+		_, ferr = tmpCertFile.Write(certBytes)
+		helper.Must(ferr)
+		helper.Must(tmpCertFile.Close())
+		defer os.Remove(tmpCertFile.Name())
+
+		tests := []struct {
+			name       string
+			conf       config.BackendTLS
+			wantSrv    tls.Certificate
+			wantClient tls.Certificate
+			wantErr    bool
+		}{
+			{"empty attributes", config.BackendTLS{}, tls.Certificate{}, tls.Certificate{}, false},
+			{"server ca file", config.BackendTLS{ServerCertificateFile: tmpCaCertFile.Name()}, *selfSignedCert.CA, tls.Certificate{}, false},
+			{"server ca value", config.BackendTLS{ServerCertificate: string(caCertBytes)}, *selfSignedCert.CA, tls.Certificate{}, false},
+			{"server ca file + value", config.BackendTLS{ServerCertificateFile: tmpCaCertFile.Name(), ServerCertificate: string(caCertBytes)}, tls.Certificate{}, tls.Certificate{}, true},
+			// TODO: testCase with combined crt+key PEM file
+			{"client ca file /w malformed key", config.BackendTLS{ClientCertificateFile: tmpCertFile.Name(), ClientPrivateKey: "malformed"}, tls.Certificate{}, clientCertOnly, true},
+			{"client ca file /w key", config.BackendTLS{ClientCertificateFile: tmpCertFile.Name(), ClientPrivateKey: string(selfSignedCert.ClientPrivateKey)}, tls.Certificate{}, *selfSignedCert.Client, false},
+			{"client ca value /w key", config.BackendTLS{ClientCertificate: string(certBytes), ClientPrivateKey: string(selfSignedCert.ClientPrivateKey)}, tls.Certificate{}, *selfSignedCert.Client, false},
+			{"client ca file /wo key", config.BackendTLS{ClientCertificateFile: tmpCertFile.Name()}, tls.Certificate{}, tls.Certificate{}, true},
+			{"client ca value /wo key", config.BackendTLS{ClientCertificate: string(certBytes)}, tls.Certificate{}, tls.Certificate{}, true},
+			{"client ca file + value", config.BackendTLS{ClientCertificateFile: tmpCertFile.Name(), ClientCertificate: string(certBytes)}, tls.Certificate{}, tls.Certificate{}, true},
+		}
+		for _, tt := range tests {
+			t.Run(format+"/"+tt.name, func(t *testing.T) {
+				gotSrv, gotClient, err := transport.ReadCertificates(&tt.conf)
+				if (err != nil) != tt.wantErr {
+					msg := "<nil>"
+					if lerr, ok := err.(errors.GoError); ok {
+						msg = lerr.LogError()
+					} else if err != nil {
+						msg = err.Error()
+					}
+					t.Errorf("ReadCertificates() error = %v, wantErr %v", msg, tt.wantErr)
+					return
+				}
+				if !reflect.DeepEqual(gotSrv, tt.wantSrv) {
+					t.Errorf("ReadCertificates():\n\tgotSrv  %v\n\t\twant %v", gotSrv, tt.wantSrv)
+				}
+				if !reflect.DeepEqual(gotClient, tt.wantClient) {
+					t.Errorf("ReadCertificates():\n\tgotClient %v\n\t\t want %v", gotClient, tt.wantClient)
+				}
+			})
+		}
+	}
+}
diff --git a/internal/test/client.go b/internal/test/client.go
index d9bc528c..ee6172ee 100644
--- a/internal/test/client.go
+++ b/internal/test/client.go
@@ -2,6 +2,7 @@ package test
 
 import (
 	"context"
+	"crypto/tls"
 	"net"
 	"net/http"
 	"time"
@@ -25,3 +26,11 @@ func NewHTTPClient() *http.Client {
 		},
 	}
 }
+
+func NewHTTPSClient(tlsConf *tls.Config) *http.Client {
+	client := NewHTTPClient()
+	transport := client.Transport.(*http.Transport)
+	transport.TLSClientConfig = tlsConf
+	client.Transport = transport
+	return client
+}
diff --git a/main_test.go b/main_test.go
index 11420979..7f04acca 100644
--- a/main_test.go
+++ b/main_test.go
@@ -51,7 +51,7 @@ func Test_realmain(t *testing.T) {
 		{"empty string in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/13_couper.hcl"}, nil, `level=error msg="%s/13_couper.hcl:3,5-27: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="%s/14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="%s/15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
-		{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="configuration error: anonymous_3_11: anonymous backend (\"anonymous_3_11\") cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
+		{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="configuration error: anonymous_3_11: anonymous backend 'anonymous_3_11' cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
 		{"non-string proxy reference", []string{"couper", "run", "-f", base + "/19_couper.hcl"}, nil, `level=error msg="%s/19_couper.hcl:3,13-14: proxy must evaluate to string; " build=dev`, 1},
 		{"proxy reference does not exist", []string{"couper", "run", "-f", base + "/20_couper.hcl"}, nil, `level=error msg="%s/20_couper.hcl:3,14-17: proxy reference is not defined; " build=dev`, 1},
 		{"circular backend references", []string{"couper", "run", "-f", base + "/21_couper.hcl"}, nil, `level=error msg="configuration error: <nil>: configuration error; circular reference:`, 1},
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 6995dd70..b898c02f 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3104,7 +3104,7 @@ func TestConfigBodyContent(t *testing.T) {
 	expiredCert, err := os.CreateTemp(os.TempDir(), "expired.pem")
 	helper.Must(err)
 
-	_, err = expiredCert.Write(selfSigned.CA)
+	_, err = expiredCert.Write(selfSigned.CACertificate.Certificate)
 	helper.Must(err)
 	helper.Must(expiredCert.Close())
 
diff --git a/server/http_mtls_test.go b/server/http_mtls_test.go
new file mode 100644
index 00000000..79be2ce2
--- /dev/null
+++ b/server/http_mtls_test.go
@@ -0,0 +1,298 @@
+package server_test
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/avenga/couper/internal/test"
+	"github.com/avenga/couper/server"
+)
+
+func TestHTTPSServer_TLS_SelfSigned(t *testing.T) {
+	helper := test.New(t)
+
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs: x509.NewCertPool(),
+	})
+
+	shutdown, _ := newCouper("testdata/mtls/01_couper.hcl", helper)
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	_, err = client.Do(outreq)
+	if err == nil {
+		t.Fatal("tls error expected, got nil")
+	}
+
+	if err.Error() != `Get "https://localhost:4443/": x509: certificate signed by unknown authority` {
+		t.Errorf("Want unknown authority error, got: %v", err)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerCertificate(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs: pool,
+	})
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/02_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate), // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),  // PEM
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(outreq)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("Expected statusOK, got: %d", res.StatusCode)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerClientCertificate(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs:      pool,
+		Certificates: []tls.Certificate{*selfSigned.Client},
+	})
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/03_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate),             // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),              // PEM
+		"clientCA":   string(selfSigned.ClientIntermediateCertificate.Certificate), // PEM
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(outreq)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("Expected statusOK, got: %d", res.StatusCode)
+	}
+
+	// without presenting the client certificate
+	client = test.NewHTTPSClient(&tls.Config{
+		RootCAs: pool,
+	})
+
+	res, err = client.Do(outreq)
+	if err == nil {
+		t.Fatal("expected a remote tls error")
+	}
+
+	if err.Error() != `Get "https://localhost:4443/": remote error: tls: bad certificate` {
+		t.Errorf("Expected a tls handshake error, got: %v", err)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerClientCertificateLeaf(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs:      pool,
+		Certificates: []tls.Certificate{*selfSigned.Client},
+	})
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/04_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate),             // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),              // PEM
+		"clientCA":   string(selfSigned.ClientIntermediateCertificate.Certificate), // PEM
+		"clientLeaf": string(selfSigned.ClientCertificate.Certificate),             // PEM
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(outreq)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("Expected statusOK, got: %d", res.StatusCode)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerClientCertificateLeafNoMatch(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs:      pool,
+		Certificates: []tls.Certificate{*selfSigned.Client},
+	})
+
+	shutdown, hook := newCouperWithTemplate("testdata/mtls/04_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate),             // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),              // PEM
+		"clientCA":   string(selfSigned.ClientIntermediateCertificate.Certificate), // PEM
+		"clientLeaf": string(selfSigned.CACertificate.Certificate),                 // PEM / just a non-matching one
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	hook.Reset()
+
+	_, err = client.Do(outreq)
+	if err == nil {
+		t.Error("expected a tls handshake error")
+	}
+
+	entries := hook.AllEntries()
+	if len(entries) == 0 {
+		t.Fatal("expected log entries")
+	}
+
+	if !strings.HasSuffix(entries[0].Message, "tls: client leaf certificate mismatch") {
+		t.Errorf("expected leaf mismatch err, got: %v", entries[0].Message)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerClientLeafOnly(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs:      pool,
+		Certificates: []tls.Certificate{*selfSigned.Client},
+	})
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/06_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate), // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),  // PEM
+		"clientLeaf": string(selfSigned.ClientCertificate.Certificate), // PEM
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(outreq)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("Expected statusOK, got: %d", res.StatusCode)
+	}
+}
+
+func TestHTTPSServer_TLS_ServerClientCertificateLeafMixed(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned1, err := server.NewCertificate(time.Hour, nil, nil)
+	helper.Must(err)
+
+	selfSigned2, err := server.NewCertificate(time.Hour, nil, nil)
+	helper.Must(err)
+
+	selfSigned3, err := server.NewCertificate(time.Hour, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned1.CA.Leaf)
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/07_couper.hcl", helper, map[string]interface{}{
+		"publicKey":    string(selfSigned1.ServerCertificate.Certificate),             // PEM
+		"privateKey":   string(selfSigned1.ServerCertificate.PrivateKey),              // PEM
+		"client1_Leaf": string(selfSigned1.ClientCertificate.Certificate),             // PEM
+		"client2_CA":   string(selfSigned2.ClientIntermediateCertificate.Certificate), // PEM
+		"client2_Leaf": string(selfSigned2.ClientCertificate.Certificate),             // PEM
+		"client3_CA":   string(selfSigned3.ClientIntermediateCertificate.Certificate), // PEM
+	})
+	defer shutdown()
+
+	for i, clientCert := range []*tls.Certificate{selfSigned1.Client, selfSigned2.Client, selfSigned3.Client} {
+		t.Run(fmt.Sprintf("client%d", i+1), func(st *testing.T) {
+			h := test.New(st)
+			c := clientCert
+			client := test.NewHTTPSClient(&tls.Config{
+				RootCAs: pool,
+				GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+					return c, nil
+				},
+			})
+
+			outreq, e := http.NewRequest(http.MethodGet, "https://localhost:4443/", nil)
+			h.Must(e)
+
+			res, e := client.Do(outreq)
+			h.Must(e)
+
+			if res.StatusCode != http.StatusOK {
+				st.Errorf("Expected statusOK, got: %d", res.StatusCode)
+			}
+		})
+	}
+}
+
+func TestHTTPSServer_TLS_ServerBackendClient(t *testing.T) {
+	helper := test.New(t)
+
+	selfSigned, err := server.NewCertificate(time.Minute, nil, nil)
+	helper.Must(err)
+
+	pool := x509.NewCertPool()
+	pool.AddCert(selfSigned.CA.Leaf)
+	client := test.NewHTTPSClient(&tls.Config{
+		RootCAs:      pool,
+		Certificates: []tls.Certificate{*selfSigned.Client},
+	})
+
+	shutdown, _ := newCouperWithTemplate("testdata/mtls/05_couper.hcl", helper, map[string]interface{}{
+		"publicKey":  string(selfSigned.ServerCertificate.Certificate),             // PEM
+		"privateKey": string(selfSigned.ServerCertificate.PrivateKey),              // PEM
+		"clientCA":   string(selfSigned.ClientIntermediateCertificate.Certificate), // PEM
+		"clientLeaf": string(selfSigned.ClientCertificate.Certificate),             // PEM
+		"clientKey":  string(selfSigned.ClientCertificate.PrivateKey),              // PEM
+		"rootCA":     string(selfSigned.CACertificate.Certificate),                 // PEM
+	})
+	defer shutdown()
+
+	outreq, err := http.NewRequest(http.MethodGet, "https://localhost:4443/inception", nil)
+	helper.Must(err)
+
+	res, err := client.Do(outreq)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("Expected statusOK, got: %d", res.StatusCode)
+	}
+}
diff --git a/server/http_test.go b/server/http_test.go
index c6035faa..cd860215 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -88,7 +88,9 @@ func TestHTTPServer_ServeHTTP_Files(t *testing.T) {
 	helper.Must(err)
 
 	port := runtime.Port(conf.Settings.DefaultPort)
-	gw := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, port, srvConf[port])
+	gw, err := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, port, srvConf[port])
+	helper.Must(err)
+
 	gw.Listen()
 	defer gw.Close()
 
@@ -190,7 +192,9 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
 	srvConf, err := runtime.NewServerConfiguration(conf, logger, tmpMemStore)
 	helper.Must(err)
 
-	couper := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, runtime.Port(0), srvConf[0])
+	couper, err := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, runtime.Port(0), srvConf[0])
+	helper.Must(err)
+
 	couper.Listen()
 	defer couper.Close()
 
diff --git a/server/testdata/mtls/01_couper.hcl b/server/testdata/mtls/01_couper.hcl
new file mode 100644
index 00000000..b02d7487
--- /dev/null
+++ b/server/testdata/mtls/01_couper.hcl
@@ -0,0 +1,7 @@
+server {
+  hosts = ["*:4443"]
+
+  tls {
+    # self signed fallback
+  }
+}
diff --git a/server/testdata/mtls/02_couper.hcl b/server/testdata/mtls/02_couper.hcl
new file mode 100644
index 00000000..b3d3dea9
--- /dev/null
+++ b/server/testdata/mtls/02_couper.hcl
@@ -0,0 +1,18 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+  }
+}
diff --git a/server/testdata/mtls/03_couper.hcl b/server/testdata/mtls/03_couper.hcl
new file mode 100644
index 00000000..2deae6ca
--- /dev/null
+++ b/server/testdata/mtls/03_couper.hcl
@@ -0,0 +1,24 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+
+    client_certificate {
+      ca_certificate = <<-EOC
+{{ .clientCA }}
+EOC
+    }
+  }
+}
diff --git a/server/testdata/mtls/04_couper.hcl b/server/testdata/mtls/04_couper.hcl
new file mode 100644
index 00000000..41563355
--- /dev/null
+++ b/server/testdata/mtls/04_couper.hcl
@@ -0,0 +1,27 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+
+    client_certificate {
+      ca_certificate = <<-EOC
+{{ .clientCA }}
+EOC
+      leaf_certificate = <<-EOC
+{{ .clientLeaf }}
+EOC
+    }
+  }
+}
diff --git a/server/testdata/mtls/05_couper.hcl b/server/testdata/mtls/05_couper.hcl
new file mode 100644
index 00000000..e2d038e8
--- /dev/null
+++ b/server/testdata/mtls/05_couper.hcl
@@ -0,0 +1,52 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  endpoint "/inception" {
+    proxy {
+      backend = "secured"
+    }
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+
+    client_certificate {
+      ca_certificate = <<-EOC
+{{ .clientCA }}
+EOC
+      leaf_certificate = <<-EOC
+{{ .clientLeaf }}
+EOC
+    }
+  }
+}
+
+definitions {
+  backend "secured" {
+    origin = "https://localhost:4443"
+    path = "/"
+
+    tls {
+      server_ca_certificate = <<-EOC
+{{ .rootCA }}
+EOC
+      client_certificate = <<-EOC
+{{ .clientLeaf }}
+EOC
+      client_private_key = <<-EOC
+{{ .clientKey }}
+EOC
+    }
+  }
+}
diff --git a/server/testdata/mtls/06_couper.hcl b/server/testdata/mtls/06_couper.hcl
new file mode 100644
index 00000000..ba67a2e5
--- /dev/null
+++ b/server/testdata/mtls/06_couper.hcl
@@ -0,0 +1,24 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+
+    client_certificate {
+      leaf_certificate = <<-EOC
+{{ .clientLeaf }}
+EOC
+    }
+  }
+}
diff --git a/server/testdata/mtls/07_couper.hcl b/server/testdata/mtls/07_couper.hcl
new file mode 100644
index 00000000..40330485
--- /dev/null
+++ b/server/testdata/mtls/07_couper.hcl
@@ -0,0 +1,39 @@
+server {
+  hosts = ["*:4443"]
+
+  endpoint "/" {
+    response {}
+  }
+
+  tls {
+    server_certificate {
+      public_key = <<-EOC
+{{ .publicKey }}
+EOC
+      private_key = <<-EOC
+{{ .privateKey }}
+EOC
+    }
+
+    client_certificate "one" {
+      leaf_certificate = <<-EOC
+{{ .client1_Leaf }}
+EOC
+    }
+#
+    client_certificate "two" {
+      leaf_certificate = <<-EOC
+{{ .client2_Leaf }}
+EOC
+      ca_certificate = <<-EOC
+{{ .client2_CA }}
+EOC
+    }
+
+    client_certificate "three" {
+      ca_certificate = <<-EOC
+{{ .client3_CA }}
+EOC
+    }
+  }
+}
diff --git a/server/tls_dev_proxy_test.go b/server/tls_dev_proxy_test.go
new file mode 100644
index 00000000..2e349616
--- /dev/null
+++ b/server/tls_dev_proxy_test.go
@@ -0,0 +1,67 @@
+package server_test
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/avenga/couper/internal/test"
+)
+
+func TestNewTLSProxy(t *testing.T) {
+	couperFile := []byte(`server "tls-dev" {
+  endpoint "/" {
+    response {
+      status = 204
+    }
+  }
+}
+
+settings {
+  https_dev_proxy = ["8443:8080", "9443:8080"]
+}`)
+
+	helper := test.New(t)
+	shutdown, hook := newCouperWithBytes(couperFile, helper)
+	defer func() {
+		if t.Failed() {
+			for _, e := range hook.AllEntries() {
+				t.Log(e.String())
+			}
+		}
+		shutdown()
+	}()
+
+	client := newClient()
+	transport := client.Transport.(*http.Transport)
+	transport.TLSClientConfig = &tls.Config{
+		InsecureSkipVerify: true,
+		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			cert, err := x509.ParseCertificate(rawCerts[0])
+			helper.Must(err)
+			if len(cert.DNSNames) == 0 || (cert.DNSNames[0] != "localhost" && cert.DNSNames[0] != "couper.dev") {
+				t.Errorf("Expect dns name to be one of localhost,couper.dev; got: %v", cert.DNSNames)
+			}
+			return nil
+		},
+	}
+
+	time.Sleep(2 * time.Second) // tls server needs some time.
+
+	for _, p := range []string{"8443", "9443"} {
+		for _, host := range []string{"127.0.0.1", "localhost", "couper.dev"} {
+			req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s:%s/", host, p), nil)
+			helper.Must(err)
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			if res.StatusCode != http.StatusNoContent {
+				t.Errorf("Expected 204, got: %d - %s", res.StatusCode, res.Status)
+			}
+		}
+	}
+}
diff --git a/server/tls_internal_test.go b/server/tls_internal_test.go
new file mode 100644
index 00000000..3b2071f9
--- /dev/null
+++ b/server/tls_internal_test.go
@@ -0,0 +1,26 @@
+package server
+
+import (
+	"crypto/tls"
+	"testing"
+
+	"github.com/avenga/couper/config"
+)
+
+func Test_requireClientAuth(t *testing.T) {
+	tests := []struct {
+		name   string
+		config *config.ServerTLS
+		want   tls.ClientAuthType
+	}{
+		{"NoClientCert without ClientCertificates", &config.ServerTLS{}, tls.NoClientCert},
+		{"RequireAndVerifyClientCert with ClientCertificates", &config.ServerTLS{ClientCertificate: make([]*config.ClientCertificate, 1)}, tls.RequireAndVerifyClientCert},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := requireClientAuth(tt.config); got != tt.want {
+				t.Errorf("requireClientAuth() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/server/tls_test.go b/server/tls_test.go
index 2e349616..c212218c 100644
--- a/server/tls_test.go
+++ b/server/tls_test.go
@@ -2,66 +2,160 @@ package server_test
 
 import (
 	"crypto/tls"
-	"crypto/x509"
-	"fmt"
-	"net/http"
+	"os"
+	"reflect"
 	"testing"
 	"time"
 
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/internal/test"
+	"github.com/avenga/couper/server"
 )
 
-func TestNewTLSProxy(t *testing.T) {
-	couperFile := []byte(`server "tls-dev" {
-  endpoint "/" {
-    response {
-      status = 204
-    }
-  }
-}
+func Test_LoadClientCertificate(t *testing.T) {
+	helper := test.New(t)
 
-settings {
-  https_dev_proxy = ["8443:8080", "9443:8080"]
-}`)
+	now := time.Now()
+	selfSignedCert, serr := server.NewCertificate(time.Hour, nil, &now)
+	helper.Must(serr)
+	t.Logf("generated certificates in %s", time.Since(now).String())
 
-	helper := test.New(t)
-	shutdown, hook := newCouperWithBytes(couperFile, helper)
-	defer func() {
-		if t.Failed() {
-			for _, e := range hook.AllEntries() {
-				t.Log(e.String())
-			}
+	// obsolete for this test
+	selfSignedCert.ClientIntermediate.PrivateKey = nil
+	selfSignedCert.Client.PrivateKey = nil
+
+	for _, format := range []string{"DER", "PEM"} {
+		var caCertBytes, certBytes []byte
+		switch format {
+		case "DER":
+			caCertBytes = selfSignedCert.ClientIntermediate.Certificate[0]
+			certBytes = selfSignedCert.Client.Certificate[0]
+		case "PEM":
+			caCertBytes = selfSignedCert.ClientIntermediateCertificate.Certificate
+			certBytes = selfSignedCert.ClientCertificate.Certificate
 		}
-		shutdown()
-	}()
 
-	client := newClient()
-	transport := client.Transport.(*http.Transport)
-	transport.TLSClientConfig = &tls.Config{
-		InsecureSkipVerify: true,
-		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
-			cert, err := x509.ParseCertificate(rawCerts[0])
-			helper.Must(err)
-			if len(cert.DNSNames) == 0 || (cert.DNSNames[0] != "localhost" && cert.DNSNames[0] != "couper.dev") {
-				t.Errorf("Expect dns name to be one of localhost,couper.dev; got: %v", cert.DNSNames)
-			}
-			return nil
-		},
+		pattern := "couper_test_tls_" + format
+		tmpCertFile, err := os.CreateTemp("", pattern)
+		helper.Must(err)
+		_, err = tmpCertFile.Write(caCertBytes)
+		helper.Must(err)
+		helper.Must(tmpCertFile.Close())
+		defer os.Remove(tmpCertFile.Name())
+
+		tmpLeafCertFile, err := os.CreateTemp("", pattern)
+		helper.Must(err)
+		_, err = tmpLeafCertFile.Write(certBytes)
+		helper.Must(err)
+		helper.Must(tmpLeafCertFile.Close())
+		defer os.Remove(tmpLeafCertFile.Name())
+
+		tests := []struct {
+			name         string
+			config       *config.ClientCertificate
+			wantCaCert   tls.Certificate
+			wantLeafCert tls.Certificate
+			wantErr      bool
+		}{
+			{"nil clientCertificate", nil, tls.Certificate{}, tls.Certificate{}, false},
+			{"empty clientCertificate", &config.ClientCertificate{}, tls.Certificate{}, tls.Certificate{}, true},
+			{"malformed clientCertificate value", &config.ClientCertificate{CA: "asdf"}, tls.Certificate{}, tls.Certificate{}, true},
+			{"clientCertificate CA value", &config.ClientCertificate{
+				CA: string(caCertBytes),
+			}, *selfSignedCert.ClientIntermediate, tls.Certificate{}, false},
+			{"clientCertificate CA /w Leaf value", &config.ClientCertificate{
+				CA:   string(caCertBytes),
+				Leaf: string(certBytes),
+			}, *selfSignedCert.ClientIntermediate, *selfSignedCert.Client, false},
+			{"clientCertificate /w Leaf value", &config.ClientCertificate{
+				Leaf: string(certBytes),
+			}, tls.Certificate{}, *selfSignedCert.Client, false},
+		}
+		for _, tt := range tests {
+			t.Run(format+"/"+tt.name, func(t *testing.T) {
+				gotCaCert, gotLeafCert, err := server.LoadClientCertificate(tt.config)
+				if (err != nil) != tt.wantErr {
+					msg := err.Error()
+					if cerr, ok := err.(errors.GoError); ok {
+						msg = cerr.LogError()
+					}
+					t.Errorf("LoadClientCertificate() error = %v, wantErr %v", msg, tt.wantErr)
+					return
+				}
+				if !reflect.DeepEqual(gotCaCert, tt.wantCaCert) {
+					t.Errorf("LoadClientCertificate() CA\n\tgot:\t%v\n\twant:\t%v\n", gotCaCert, tt.wantCaCert)
+				}
+				if !reflect.DeepEqual(gotLeafCert, tt.wantLeafCert) {
+					t.Errorf("LoadClientCertificate() Leaf\n\tgot:\t%v\n\twant:\t%v\n", gotLeafCert, tt.wantLeafCert)
+				}
+			})
+		}
 	}
+}
 
-	time.Sleep(2 * time.Second) // tls server needs some time.
+func Test_LoadServerCertificate(t *testing.T) {
+	helper := test.New(t)
 
-	for _, p := range []string{"8443", "9443"} {
-		for _, host := range []string{"127.0.0.1", "localhost", "couper.dev"} {
-			req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s:%s/", host, p), nil)
-			helper.Must(err)
+	now := time.Now()
+	selfSignedCert, serr := server.NewCertificate(time.Hour, nil, &now)
+	helper.Must(serr)
+	t.Logf("generated certificates in %s", time.Since(now).String())
 
-			res, err := client.Do(req)
-			helper.Must(err)
+	for _, format := range []string{"DER", "PEM"} {
+		var certBytes, privateKeyBytes []byte
+		switch format {
+		case "DER":
+			certBytes = selfSignedCert.Server.Certificate[0]
+			privateKeyBytes = selfSignedCert.ServerPrivateKey
+		case "PEM":
+			certBytes = selfSignedCert.ServerCertificate.Certificate
+			privateKeyBytes = selfSignedCert.ServerCertificate.PrivateKey
+		}
 
-			if res.StatusCode != http.StatusNoContent {
-				t.Errorf("Expected 204, got: %d - %s", res.StatusCode, res.Status)
-			}
+		pattern := "couper_test_tls_" + format
+		tmpCertFile, err := os.CreateTemp("", pattern)
+		helper.Must(err)
+		_, err = tmpCertFile.Write(certBytes)
+		helper.Must(err)
+		helper.Must(tmpCertFile.Close())
+		defer os.Remove(tmpCertFile.Name())
+
+		tmpKeyFile, err := os.CreateTemp("", pattern)
+		helper.Must(err)
+		_, err = tmpKeyFile.Write(privateKeyBytes)
+		helper.Must(err)
+		helper.Must(tmpKeyFile.Close())
+		defer os.Remove(tmpKeyFile.Name())
+
+		tests := []struct {
+			name    string
+			config  *config.ServerCertificate
+			want    tls.Certificate
+			wantErr bool
+		}{
+			{"nil serverCertificate", nil, tls.Certificate{}, false},
+			{"empty serverCertificate", &config.ServerCertificate{}, tls.Certificate{}, true},
+			{"with serverCertificateValue", &config.ServerCertificate{
+				PublicKey:  string(certBytes),
+				PrivateKey: string(privateKeyBytes),
+			}, *selfSignedCert.Server, false},
+			{"with serverCertificateFile", &config.ServerCertificate{
+				PublicKeyFile:  tmpCertFile.Name(),
+				PrivateKeyFile: tmpKeyFile.Name(),
+			}, *selfSignedCert.Server, false},
+		}
+		for _, tt := range tests {
+			t.Run(format+"/"+tt.name, func(t *testing.T) {
+				got, terr := server.LoadServerCertificate(tt.config)
+				if (terr != nil) != tt.wantErr {
+					t.Errorf("LoadServerCertificate() error = %v, wantErr %v", terr, tt.wantErr)
+					return
+				}
+				if !reflect.DeepEqual(got, tt.want) {
+					t.Errorf("LoadServerCertificate()\n\tgot:\t%v\n\twant:\t%v\n", got, tt.want)
+				}
+			})
 		}
 	}
 }