Allow handling ACME challenges with custom routers
This commit is contained in:
parent
d547b943df
commit
0cf2032c15
12 changed files with 142 additions and 24 deletions
|
@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```)
|
|||
`--entrypoints.<name>.address`:
|
||||
Entry point address.
|
||||
|
||||
`--entrypoints.<name>.allowacmebypass`:
|
||||
Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```)
|
||||
|
||||
`--entrypoints.<name>.forwardedheaders.insecure`:
|
||||
Trust all forwarded headers. (Default: ```false```)
|
||||
|
||||
|
|
|
@ -108,6 +108,9 @@ Entry points definition. (Default: ```false```)
|
|||
`TRAEFIK_ENTRYPOINTS_<NAME>_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`:
|
||||
Trust all forwarded headers. (Default: ```false```)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[entryPoints]
|
||||
[entryPoints.EntryPoint0]
|
||||
address = "foobar"
|
||||
allowACMEByPass = true
|
||||
[entryPoints.EntryPoint0.transport]
|
||||
keepAliveMaxTime = "42s"
|
||||
keepAliveMaxRequests = 42
|
||||
|
|
|
@ -16,6 +16,7 @@ serversTransport:
|
|||
entryPoints:
|
||||
EntryPoint0:
|
||||
address: foobar
|
||||
allowACMEByPass: true
|
||||
transport:
|
||||
lifeCycle:
|
||||
requestAcceptGraceTimeout: 42s
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -212,6 +212,7 @@ func Test_Routing(t *testing.T) {
|
|||
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{
|
||||
|
|
|
@ -23,12 +23,13 @@ import (
|
|||
type RouterFactory struct {
|
||||
entryPointsTCP []string
|
||||
entryPointsUDP []string
|
||||
allowACMEByPass map[string]bool
|
||||
|
||||
managerFactory *service.ManagerFactory
|
||||
|
||||
metricsRegistry metrics.Registry
|
||||
|
||||
pluginBuilder middleware.PluginsBuilder
|
||||
|
||||
chainBuilder *middleware.ChainBuilder
|
||||
tlsManager *tls.Manager
|
||||
}
|
||||
|
@ -37,9 +38,20 @@ type RouterFactory struct {
|
|||
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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}))
|
||||
|
|
Loading…
Reference in a new issue