mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-14 04:29:39 +02:00
feat(entrypoint): add inbound mTLS profiles for HTTPS
Add root-level inbound_mtls_profiles combining optional system CAs with PEM CA files, and entrypoint.inbound_mtls_profile to require client certificates on every HTTPS connection. Route-level inbound_mtls_profile is allowed only without a global profile; per-handshake TLS picks ClientCAs from SNI, and requests fail with 421 when Host and SNI would select different mTLS routes. Compile pools at init (SetInboundMTLSProfiles from state.initEntrypoint) and reject unknown profile refs or mixed global-plus-route configuration. Extend config.example.yml and package READMEs; add entrypoint and config tests for TLS mutation, handshakes, and validation.
This commit is contained in:
@@ -42,6 +42,7 @@ type Route struct {
|
||||
|
||||
// Route rules and middleware
|
||||
HTTPConfig
|
||||
InboundMTLSProfile string
|
||||
PathPatterns []string
|
||||
Rules rules.Rules
|
||||
RuleFile string
|
||||
@@ -61,6 +62,24 @@ type Route struct {
|
||||
}
|
||||
```
|
||||
|
||||
`InboundMTLSProfile` references a named root-level inbound mTLS profile for this route.
|
||||
|
||||
- It is only honored when no global `entrypoint.inbound_mtls_profile` is configured.
|
||||
- It is only valid for HTTP-based routes.
|
||||
- Route-scoped inbound mTLS is selected by TLS SNI.
|
||||
- Requests for secured routes must resolve to the same route by both HTTP `Host` and TLS SNI.
|
||||
- If the profile name does not exist, route validation fails.
|
||||
|
||||
Example route fragment:
|
||||
|
||||
```yaml
|
||||
alias: secure-api
|
||||
host: api.example.com
|
||||
scheme: https
|
||||
port: 443
|
||||
inbound_mtls_profile: corp-clients
|
||||
```
|
||||
|
||||
```go
|
||||
type Scheme string
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ example: # matching `example.y.z`
|
||||
no_tls_verify: true
|
||||
disable_compression: false
|
||||
response_header_timeout: 30s
|
||||
inbound_mtls_profile: corp # optional, only supported when no global entrypoint inbound_mtls_profile is configured; selected by TLS SNI and Host/SNI must resolve to the same route
|
||||
ssl_server_name: "" # empty uses target hostname, "off" disables SNI
|
||||
ssl_trusted_certificate: /etc/ssl/certs/ca-certificates.crt
|
||||
ssl_certificate: /etc/ssl/client.crt
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
entrypointimpl "github.com/yusing/godoxy/internal/entrypoint"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
@@ -54,6 +55,7 @@ type (
|
||||
Index string `json:"index,omitempty"` // Index file to serve for single-page app mode
|
||||
|
||||
route.HTTPConfig
|
||||
InboundMTLSProfile string `json:"inbound_mtls_profile,omitempty"`
|
||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||
Rules rules.Rules `json:"rules,omitempty" extensions:"x-nullable"`
|
||||
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
|
||||
@@ -171,8 +173,23 @@ func (r *Route) validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if workingState := config.WorkingState.Load(); workingState != nil {
|
||||
cfg := workingState.Value()
|
||||
if err := entrypointimpl.ValidateInboundMTLSProfileRef(r.InboundMTLSProfile, cfg.Entrypoint.InboundMTLSProfile, cfg.InboundMTLSProfiles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.Finalize()
|
||||
|
||||
if r.InboundMTLSProfile != "" {
|
||||
switch r.Scheme {
|
||||
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C, route.SchemeFileServer:
|
||||
default:
|
||||
return errors.New("inbound_mtls_profile is only supported for HTTP-based routes")
|
||||
}
|
||||
}
|
||||
|
||||
r.started = make(chan struct{})
|
||||
// close the channel when the route is destroyed (if not closed yet).
|
||||
runtime.AddCleanup(r, func(ch chan struct{}) {
|
||||
@@ -767,6 +784,10 @@ func (r *Route) ContainerInfo() *types.Container {
|
||||
return r.Container
|
||||
}
|
||||
|
||||
func (r *Route) InboundMTLSProfileRef() string {
|
||||
return r.InboundMTLSProfile
|
||||
}
|
||||
|
||||
func (r *Route) IsDocker() bool {
|
||||
if r.Container == nil {
|
||||
return false
|
||||
|
||||
@@ -91,6 +91,19 @@ func TestRouteValidate(t *testing.T) {
|
||||
require.ErrorContains(t, err, "relay_proxy_protocol_header is only supported for tcp routes")
|
||||
})
|
||||
|
||||
t.Run("InboundMTLSProfileHTTPOnly", func(t *testing.T) {
|
||||
r := &Route{
|
||||
Alias: "test-udp-mtls",
|
||||
Scheme: route.SchemeUDP,
|
||||
Host: "127.0.0.1",
|
||||
Port: route.Port{Proxy: 53, Listening: 53},
|
||||
InboundMTLSProfile: "corp",
|
||||
}
|
||||
err := r.Validate()
|
||||
require.Error(t, err, "Validate should reject inbound mTLS on non-HTTP routes")
|
||||
require.ErrorContains(t, err, "inbound_mtls_profile is only supported for HTTP-based routes")
|
||||
})
|
||||
|
||||
t.Run("DockerContainer", func(t *testing.T) {
|
||||
r := &Route{
|
||||
Alias: "test",
|
||||
|
||||
Reference in New Issue
Block a user