diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index f98e1a177..362146ac0 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```) `--entrypoints..address`: Entry point address. +`--entrypoints..allowacmebypass`: +Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```) + `--entrypoints..forwardedheaders.insecure`: Trust all forwarded headers. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index d71ffe6c7..6ca27db89 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__ADDRESS`: Entry point address. +`TRAEFIK_ENTRYPOINTS__ALLOWACMEBYPASS`: +Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```) + `TRAEFIK_ENTRYPOINTS__FORWARDEDHEADERS_INSECURE`: Trust all forwarded headers. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 42c4e6fdf..8f6f9510e 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -16,6 +16,7 @@ [entryPoints] [entryPoints.EntryPoint0] address = "foobar" + allowACMEByPass = true [entryPoints.EntryPoint0.transport] keepAliveMaxTime = "42s" keepAliveMaxRequests = 42 diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index abb8e05d8..09e66b545 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -16,6 +16,7 @@ serversTransport: entryPoints: EntryPoint0: address: foobar + allowACMEByPass: true transport: lifeCycle: requestAcceptGraceTimeout: 42s diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index e874764bc..ca34161d2 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -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. +### 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 #### `maxConcurrentStreams` diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 905d74961..8bfbc59f1 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -12,6 +12,7 @@ import ( // EntryPoint holds the entry point configuration. type EntryPoint struct { 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"` 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"` diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 83bfee8bc..da44f8ff2 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -87,15 +87,27 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati } 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{}{} for _, resolver := range i.staticCfg.CertificatesResolvers { if resolver.ACME != nil && resolver.ACME.HTTPChallenge != nil && resolver.ACME.HTTPChallenge.EntryPoint != "" { - if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; !ok { - eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint) - uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{} + if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; ok { + continue } + 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.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) { diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index ccb8c06bf..0da33e10a 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -21,6 +21,8 @@ const defaultBufSize = 4096 // Router is a TCP router. type Router struct { + acmeTLSPassthrough bool + // Contains TCP routes. muxerTCP tcpmuxer.Muxer // Contains TCP TLS routes. @@ -148,7 +150,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } // 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)) return } @@ -303,6 +305,10 @@ func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) { r.httpsTLSConfig = config } +func (r *Router) EnableACMETLSPassthrough() { + r.acmeTLSPassthrough = true +} + // Conn is a connection proxy that handles Peeked bytes. type Conn struct { // Peeked are the bytes that have been read from Conn for the purposes of route matching, diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 8f6ae37c1..2f997c7f1 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -209,9 +209,10 @@ func Test_Routing(t *testing.T) { } testCases := []struct { - desc string - routers []applyRouter - checks []checkCase + desc string + routers []applyRouter + checks []checkCase + allowACMETLSPassthrough bool }{ { 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", 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) require.NoError(t, err) + if test.allowACMETLSPassthrough { + router.EnableACMETLSPassthrough() + } + epListener, err := net.Listen("tcp", "127.0.0.1:0") 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) { conf.TCPRouters["tcp-tls-catchall-passthrough"] = &runtime.TCPRouterInfo{ TCPRouter: &dynamic.TCPRouter{ diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 6b7b80ff4..c7534ef26 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -21,25 +21,37 @@ import ( // RouterFactory the factory of TCP/UDP routers. type RouterFactory struct { - entryPointsTCP []string - entryPointsUDP []string + entryPointsTCP []string + entryPointsUDP []string + allowACMEByPass map[string]bool + + managerFactory *service.ManagerFactory - managerFactory *service.ManagerFactory metricsRegistry metrics.Registry pluginBuilder middleware.PluginsBuilder - - chainBuilder *middleware.ChainBuilder - tlsManager *tls.Manager + chainBuilder *middleware.ChainBuilder + tlsManager *tls.Manager } // NewRouterFactory creates a new RouterFactory. func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder, pluginBuilder middleware.PluginsBuilder, metricsRegistry metrics.Registry, ) *RouterFactory { + handlesTLSChallenge := false + for _, resolver := range staticConfiguration.CertificatesResolvers { + if resolver.ACME.TLSChallenge != nil { + handlesTLSChallenge = true + break + } + } + + allowACMEByPass := map[string]bool{} var entryPointsTCP, entryPointsUDP []string - for name, cfg := range staticConfiguration.EntryPoints { - protocol, err := cfg.GetProtocol() + for name, ep := range staticConfiguration.EntryPoints { + allowACMEByPass[name] = ep.AllowACMEByPass || !handlesTLSChallenge + + protocol, err := ep.GetProtocol() if err != nil { // Should never happen because Traefik should not start if protocol is invalid. log.WithoutContext().Errorf("Invalid protocol: %v", err) @@ -60,6 +72,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * tlsManager: tlsManager, chainBuilder: chainBuilder, 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) routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP) + for ep, r := range routersTCP { + if allowACMEByPass, ok := f.allowACMEByPass[ep]; ok && allowACMEByPass { + r.EnableACMETLSPassthrough() + } + } + // UDP svcUDPManager := udp.NewManager(rtConf) rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager) diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 1fb371d08..6e30de331 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -172,7 +172,10 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos 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) diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index f0b12c8dd..4dc9ee428 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -20,7 +20,9 @@ import ( ) 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) { conn, _, err := rw.(http.Hijacker).Hijack() require.NoError(t, err) @@ -34,7 +36,9 @@ func TestShutdownHijacked(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) { rw.WriteHeader(http.StatusOK) time.Sleep(time.Second) @@ -167,7 +171,9 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) { }, nil) 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) { rw.WriteHeader(http.StatusOK) })) @@ -204,7 +210,9 @@ func TestReadTimeoutWithFirstByte(t *testing.T) { }, nil) 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) { rw.WriteHeader(http.StatusOK) })) @@ -244,7 +252,9 @@ func TestKeepAliveMaxRequests(t *testing.T) { }, nil) 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) { rw.WriteHeader(http.StatusOK) })) @@ -290,7 +300,9 @@ func TestKeepAliveMaxTime(t *testing.T) { }, nil) 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) { rw.WriteHeader(http.StatusOK) }))