snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
95
oryx/tlsx/termination.go
Normal file
95
oryx/tlsx/termination.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tlsx
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/negroni"
|
||||
|
||||
"github.com/ory/x/healthx"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/prometheusx"
|
||||
)
|
||||
|
||||
type dependencies interface {
|
||||
logrusx.Provider
|
||||
httpx.WriterProvider
|
||||
}
|
||||
|
||||
// EnforceTLSRequests creates a middleware that enforces TLS for incoming HTTP requests.
|
||||
// It allows termination (non-HTTPS traffic) from specific CIDR ranges provided in the `allowTerminationFrom` slice.
|
||||
// If the request is not secure and does not match the allowed CIDR ranges, an error response is returned.
|
||||
// The middleware also validates the `X-Forwarded-Proto` header to ensure it is set to "https".
|
||||
func EnforceTLSRequests(d dependencies, allowTerminationFrom []string) (negroni.Handler, error) {
|
||||
networks := make([]*net.IPNet, 0, len(allowTerminationFrom))
|
||||
for _, rn := range allowTerminationFrom {
|
||||
_, network, err := net.ParseCIDR(rn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
networks = append(networks, network)
|
||||
}
|
||||
|
||||
return negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if r.TLS != nil ||
|
||||
r.URL.Path == healthx.AliveCheckPath ||
|
||||
r.URL.Path == healthx.ReadyCheckPath ||
|
||||
r.URL.Path == prometheusx.MetricsPrometheusPath {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
d.Logger().WithRequest(r).WithError(errors.New("TLS termination is not enabled")).Error("Could not serve http connection")
|
||||
d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := matchesRange(r, networks); err != nil {
|
||||
d.Logger().WithRequest(r).WithError(err).Warnln("Could not serve http connection")
|
||||
d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
|
||||
return
|
||||
}
|
||||
|
||||
proto := r.Header.Get("X-Forwarded-Proto")
|
||||
if proto == "" {
|
||||
d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection")
|
||||
d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
|
||||
return
|
||||
} else if proto != "https" {
|
||||
d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection")
|
||||
d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.Errorf("expected X-Forwarded-Proto header to be https but got: %s", proto))
|
||||
return
|
||||
}
|
||||
|
||||
next(rw, r)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func matchesRange(r *http.Request, networks []*net.IPNet) error {
|
||||
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
check := []string{remoteIP}
|
||||
for fwd := range strings.SplitSeq(r.Header.Get("X-Forwarded-For"), ",") {
|
||||
check = append(check, strings.TrimSpace(fwd))
|
||||
}
|
||||
|
||||
for _, ipNet := range networks {
|
||||
for _, ip := range check {
|
||||
addr := net.ParseIP(ip)
|
||||
if ipNet.Contains(addr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.Errorf("neither remote address nor any x-forwarded-for values match CIDR ranges %+v: %v, ranges, check)", networks, check)
|
||||
}
|
||||
Reference in New Issue
Block a user