Allow handling ACME challenges with custom routers

This commit is contained in:
Romain 2024-09-13 15:54:04 +02:00 committed by GitHub
parent d547b943df
commit 0cf2032c15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 142 additions and 24 deletions

View file

@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```)
`--entrypoints.<name>.address`: `--entrypoints.<name>.address`:
Entry point address. Entry point address.
`--entrypoints.<name>.allowacmebypass`:
Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```)
`--entrypoints.<name>.forwardedheaders.insecure`: `--entrypoints.<name>.forwardedheaders.insecure`:
Trust all forwarded headers. (Default: ```false```) Trust all forwarded headers. (Default: ```false```)

View file

@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_ADDRESS`: `TRAEFIK_ENTRYPOINTS_<NAME>_ADDRESS`:
Entry point address. Entry point address.
`TRAEFIK_ENTRYPOINTS_<NAME>_ALLOWACMEBYPASS`:
Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_FORWARDEDHEADERS_INSECURE`: `TRAEFIK_ENTRYPOINTS_<NAME>_FORWARDEDHEADERS_INSECURE`:
Trust all forwarded headers. (Default: ```false```) Trust all forwarded headers. (Default: ```false```)

View file

@ -16,6 +16,7 @@
[entryPoints] [entryPoints]
[entryPoints.EntryPoint0] [entryPoints.EntryPoint0]
address = "foobar" address = "foobar"
allowACMEByPass = true
[entryPoints.EntryPoint0.transport] [entryPoints.EntryPoint0.transport]
keepAliveMaxTime = "42s" keepAliveMaxTime = "42s"
keepAliveMaxRequests = 42 keepAliveMaxRequests = 42

View file

@ -16,6 +16,7 @@ serversTransport:
entryPoints: entryPoints:
EntryPoint0: EntryPoint0:
address: foobar address: foobar
allowACMEByPass: true
transport: transport:
lifeCycle: lifeCycle:
requestAcceptGraceTimeout: 42s requestAcceptGraceTimeout: 42s

View file

@ -233,6 +233,35 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar
Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go.
### AllowACMEByPass
_Optional, Default=false_
`allowACMEByPass` determines whether a user defined router can handle ACME TLS or HTTP challenges instead of the Traefik dedicated one.
This option can be used when a Traefik instance has one or more certificate resolvers configured,
but is also used to route challenges connections/requests to services that could also initiate their own ACME challenges.
??? info "No Certificate Resolvers configured"
It is not necessary to use the `allowACMEByPass' option certificate option if no certificate resolver is defined.
In fact, Traefik will automatically allow ACME TLS or HTTP requests to be handled by custom routers in this case, since there can be no concurrency with its own challenge handlers.
```yaml tab="File (YAML)"
entryPoints:
foo:
allowACMEByPass: true
```
```toml tab="File (TOML)"
[entryPoints.foo]
[entryPoints.foo.allowACMEByPass]
allowACMEByPass = true
```
```bash tab="CLI"
--entryPoints.name.allowACMEByPass=true
```
### HTTP/2 ### HTTP/2
#### `maxConcurrentStreams` #### `maxConcurrentStreams`

View file

@ -12,6 +12,7 @@ import (
// EntryPoint holds the entry point configuration. // EntryPoint holds the entry point configuration.
type EntryPoint struct { type EntryPoint struct {
Address string `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` Address string `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"`
AllowACMEByPass bool `description:"Enables handling of ACME TLS and HTTP challenges with custom routers." json:"allowACMEByPass,omitempty" toml:"allowACMEByPass,omitempty" yaml:"allowACMEByPass,omitempty"`
Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty" export:"true"` Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty" export:"true"`
ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty" export:"true"` ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty" export:"true"`

View file

@ -87,15 +87,27 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati
} }
func (i *Provider) acme(cfg *dynamic.Configuration) { func (i *Provider) acme(cfg *dynamic.Configuration) {
var eps []string allowACMEByPass := map[string]bool{}
for name, ep := range i.staticCfg.EntryPoints {
allowACMEByPass[name] = ep.AllowACMEByPass
}
var eps []string
var epsByPass []string
uniq := map[string]struct{}{} uniq := map[string]struct{}{}
for _, resolver := range i.staticCfg.CertificatesResolvers { for _, resolver := range i.staticCfg.CertificatesResolvers {
if resolver.ACME != nil && resolver.ACME.HTTPChallenge != nil && resolver.ACME.HTTPChallenge.EntryPoint != "" { if resolver.ACME != nil && resolver.ACME.HTTPChallenge != nil && resolver.ACME.HTTPChallenge.EntryPoint != "" {
if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; !ok { if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; ok {
eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint) continue
uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{}
} }
uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{}
if allowByPass, ok := allowACMEByPass[resolver.ACME.HTTPChallenge.EntryPoint]; ok && allowByPass {
epsByPass = append(epsByPass, resolver.ACME.HTTPChallenge.EntryPoint)
continue
}
eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint)
} }
} }
@ -110,6 +122,17 @@ func (i *Provider) acme(cfg *dynamic.Configuration) {
cfg.HTTP.Routers["acme-http"] = rt cfg.HTTP.Routers["acme-http"] = rt
cfg.HTTP.Services["acme-http"] = &dynamic.Service{} cfg.HTTP.Services["acme-http"] = &dynamic.Service{}
} }
if len(epsByPass) > 0 {
rt := &dynamic.Router{
Rule: "PathPrefix(`/.well-known/acme-challenge/`)",
EntryPoints: epsByPass,
Service: "acme-http@internal",
}
cfg.HTTP.Routers["acme-http-bypass"] = rt
cfg.HTTP.Services["acme-http"] = &dynamic.Service{}
}
} }
func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) { func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) {

View file

@ -21,6 +21,8 @@ const defaultBufSize = 4096
// Router is a TCP router. // Router is a TCP router.
type Router struct { type Router struct {
acmeTLSPassthrough bool
// Contains TCP routes. // Contains TCP routes.
muxerTCP tcpmuxer.Muxer muxerTCP tcpmuxer.Muxer
// Contains TCP TLS routes. // Contains TCP TLS routes.
@ -148,7 +150,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
} }
// Handling ACME-TLS/1 challenges. // Handling ACME-TLS/1 challenges.
if slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) { if !r.acmeTLSPassthrough && slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) {
r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked)) r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked))
return return
} }
@ -303,6 +305,10 @@ func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) {
r.httpsTLSConfig = config r.httpsTLSConfig = config
} }
func (r *Router) EnableACMETLSPassthrough() {
r.acmeTLSPassthrough = true
}
// Conn is a connection proxy that handles Peeked bytes. // Conn is a connection proxy that handles Peeked bytes.
type Conn struct { type Conn struct {
// Peeked are the bytes that have been read from Conn for the purposes of route matching, // Peeked are the bytes that have been read from Conn for the purposes of route matching,

View file

@ -212,6 +212,7 @@ func Test_Routing(t *testing.T) {
desc string desc string
routers []applyRouter routers []applyRouter
checks []checkCase checks []checkCase
allowACMETLSPassthrough bool
}{ }{
{ {
desc: "No routers", desc: "No routers",
@ -268,6 +269,18 @@ func Test_Routing(t *testing.T) {
}, },
}, },
}, },
{
desc: "TCP TLS passthrough catches ACME TLS",
allowACMETLSPassthrough: true,
routers: []applyRouter{routerTCPTLSCatchAllPassthrough},
checks: []checkCase{
{
desc: "ACME TLS Challenge",
checkRouter: checkACMETLS,
expectedError: "tls: first record does not look like a TLS handshake",
},
},
},
{ {
desc: "Single TCP CatchAll router", desc: "Single TCP CatchAll router",
routers: []applyRouter{routerTCPCatchAll}, routers: []applyRouter{routerTCPCatchAll},
@ -578,6 +591,10 @@ func Test_Routing(t *testing.T) {
router, err := manager.buildEntryPointHandler(context.Background(), dynConf.TCPRouters, dynConf.Routers, nil, nil) router, err := manager.buildEntryPointHandler(context.Background(), dynConf.TCPRouters, dynConf.Routers, nil, nil)
require.NoError(t, err) require.NoError(t, err)
if test.allowACMETLSPassthrough {
router.EnableACMETLSPassthrough()
}
epListener, err := net.Listen("tcp", "127.0.0.1:0") epListener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err) require.NoError(t, err)
@ -699,7 +716,7 @@ func routerTCPTLSCatchAll(conf *runtime.Configuration) {
} }
} }
// routerTCPTLSCatchAllPassthrough a TCP TLS CatchAll Passthrough - HostSNI(`*`) router with TLS 1.0 config. // routerTCPTLSCatchAllPassthrough a TCP TLS CatchAll Passthrough - HostSNI(`*`) router with TLS 1.2 config.
func routerTCPTLSCatchAllPassthrough(conf *runtime.Configuration) { func routerTCPTLSCatchAllPassthrough(conf *runtime.Configuration) {
conf.TCPRouters["tcp-tls-catchall-passthrough"] = &runtime.TCPRouterInfo{ conf.TCPRouters["tcp-tls-catchall-passthrough"] = &runtime.TCPRouterInfo{
TCPRouter: &dynamic.TCPRouter{ TCPRouter: &dynamic.TCPRouter{

View file

@ -23,12 +23,13 @@ import (
type RouterFactory struct { type RouterFactory struct {
entryPointsTCP []string entryPointsTCP []string
entryPointsUDP []string entryPointsUDP []string
allowACMEByPass map[string]bool
managerFactory *service.ManagerFactory managerFactory *service.ManagerFactory
metricsRegistry metrics.Registry metricsRegistry metrics.Registry
pluginBuilder middleware.PluginsBuilder pluginBuilder middleware.PluginsBuilder
chainBuilder *middleware.ChainBuilder chainBuilder *middleware.ChainBuilder
tlsManager *tls.Manager tlsManager *tls.Manager
} }
@ -37,9 +38,20 @@ type RouterFactory struct {
func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager,
chainBuilder *middleware.ChainBuilder, pluginBuilder middleware.PluginsBuilder, metricsRegistry metrics.Registry, chainBuilder *middleware.ChainBuilder, pluginBuilder middleware.PluginsBuilder, metricsRegistry metrics.Registry,
) *RouterFactory { ) *RouterFactory {
handlesTLSChallenge := false
for _, resolver := range staticConfiguration.CertificatesResolvers {
if resolver.ACME.TLSChallenge != nil {
handlesTLSChallenge = true
break
}
}
allowACMEByPass := map[string]bool{}
var entryPointsTCP, entryPointsUDP []string var entryPointsTCP, entryPointsUDP []string
for name, cfg := range staticConfiguration.EntryPoints { for name, ep := range staticConfiguration.EntryPoints {
protocol, err := cfg.GetProtocol() allowACMEByPass[name] = ep.AllowACMEByPass || !handlesTLSChallenge
protocol, err := ep.GetProtocol()
if err != nil { if err != nil {
// Should never happen because Traefik should not start if protocol is invalid. // Should never happen because Traefik should not start if protocol is invalid.
log.WithoutContext().Errorf("Invalid protocol: %v", err) log.WithoutContext().Errorf("Invalid protocol: %v", err)
@ -60,6 +72,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
tlsManager: tlsManager, tlsManager: tlsManager,
chainBuilder: chainBuilder, chainBuilder: chainBuilder,
pluginBuilder: pluginBuilder, pluginBuilder: pluginBuilder,
allowACMEByPass: allowACMEByPass,
} }
} }
@ -87,6 +100,12 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager) rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP) routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP)
for ep, r := range routersTCP {
if allowACMEByPass, ok := f.allowACMEByPass[ep]; ok && allowACMEByPass {
r.EnableACMETLSPassthrough()
}
}
// UDP // UDP
svcUDPManager := udp.NewManager(rtConf) svcUDPManager := udp.NewManager(rtConf)
rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager) rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager)

View file

@ -172,7 +172,10 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing server: %w", err) return nil, fmt.Errorf("error preparing server: %w", err)
} }
rt := &tcprouter.Router{} rt, err := tcprouter.NewRouter()
if err != nil {
return nil, fmt.Errorf("error preparing tcp router: %w", err)
}
reqDecorator := requestdecorator.New(hostResolverConfig) reqDecorator := requestdecorator.New(hostResolverConfig)

View file

@ -20,7 +20,9 @@ import (
) )
func TestShutdownHijacked(t *testing.T) { func TestShutdownHijacked(t *testing.T) {
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack() conn, _, err := rw.(http.Hijacker).Hijack()
require.NoError(t, err) require.NoError(t, err)
@ -34,7 +36,9 @@ func TestShutdownHijacked(t *testing.T) {
} }
func TestShutdownHTTP(t *testing.T) { func TestShutdownHTTP(t *testing.T) {
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
time.Sleep(time.Second) time.Sleep(time.Second)
@ -167,7 +171,9 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
})) }))
@ -204,7 +210,9 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
})) }))
@ -244,7 +252,9 @@ func TestKeepAliveMaxRequests(t *testing.T) {
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
})) }))
@ -290,7 +300,9 @@ func TestKeepAliveMaxTime(t *testing.T) {
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
router := &tcprouter.Router{} router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
})) }))