diff --git a/modules/private/internal.go b/modules/private/internal.go index 146b4d97990..a3227eacb78 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -54,12 +54,7 @@ func dialContextInternalAPI(ctx context.Context, network, address string) (conn return conn, nil } -// internalAPIConnectionIsLocal reports whether the internal API transport connects to a local target, -// where the self-signed local certificate cannot be verified so skipping verification is safe. It mirrors -// what dialContextInternalAPI actually dials: a unix socket whenever Protocol is HTTPUnix (always local, -// whatever LOCAL_ROOT_URL says), otherwise the LOCAL_ROOT_URL host directly. A non-loopback LOCAL_ROOT_URL -// is a real network hop, so its certificate must be verified, else the internal token can be MITM'd. An -// unparseable LOCAL_ROOT_URL is a hard misconfiguration and fails closed (verify). +// unspecified IPs dial the local host too, other targets are network hops that must be verified to protect INTERNAL_TOKEN func internalAPIConnectionIsLocal(protocol setting.Scheme, localURL string) bool { if protocol == setting.HTTPUnix { return true @@ -73,19 +68,21 @@ func internalAPIConnectionIsLocal(protocol setting.Scheme, localURL string) bool return true } ip := net.ParseIP(host) - return ip != nil && ip.IsLoopback() + return ip != nil && (ip.IsLoopback() || ip.IsUnspecified()) +} + +func internalAPITLSConfig(protocol setting.Scheme, localURL, domain string) *tls.Config { + if internalAPIConnectionIsLocal(protocol, localURL) { + // the ACME listener selects its certificate by SNI, so send the public domain instead of the local host + return &tls.Config{InsecureSkipVerify: true, ServerName: domain} + } + return &tls.Config{} } var internalAPITransport = sync.OnceValue(func() http.RoundTripper { return &http.Transport{ - DialContext: dialContextInternalAPI, - TLSClientConfig: &tls.Config{ - // Skip verification only for a local target (unix socket, or a loopback LOCAL_ROOT_URL), where the - // self-signed local cert can't be verified anyway; a non-loopback LOCAL_ROOT_URL is a real network - // hop and must be verified so the internal token can't be MITM'd. When verifying, Go's default - // ServerName (the dialed LOCAL_ROOT_URL host) is already correct, so it is not overridden. - InsecureSkipVerify: internalAPIConnectionIsLocal(setting.Protocol, setting.LocalURL), - }, + DialContext: dialContextInternalAPI, + TLSClientConfig: internalAPITLSConfig(setting.Protocol, setting.LocalURL, setting.AppDomain), } }) diff --git a/modules/private/internal_test.go b/modules/private/internal_test.go index 35713d1b0fb..e0ee1a9bd4b 100644 --- a/modules/private/internal_test.go +++ b/modules/private/internal_test.go @@ -7,23 +7,26 @@ import ( "testing" "gitea.dev/modules/setting" + "gitea.dev/modules/util" "github.com/stretchr/testify/assert" ) -func TestInternalAPIConnectionIsLocal(t *testing.T) { +func TestInternalAPITLSConfig(t *testing.T) { cases := []struct { name string protocol setting.Scheme localURL string - want bool + local bool }{ // HTTPUnix always dials the unix socket (a local target), whatever LOCAL_ROOT_URL says {"unix socket", setting.HTTPUnix, "https://gitea.example.com/", true}, {"localhost", setting.HTTP, "http://localhost:3000/", true}, {"loopback ipv4", setting.HTTPS, "https://127.0.0.1:3000/", true}, {"loopback ipv6", setting.HTTPS, "https://[::1]:3000/", true}, - // a non-loopback LOCAL_ROOT_URL is a real network hop and must be verified + {"unspecified ipv4", setting.HTTPS, "https://0.0.0.0:3000/", true}, + {"unspecified ipv6", setting.HTTPS, "https://[::]:3000/", true}, + // any other LOCAL_ROOT_URL is a real network hop and must be verified {"remote host", setting.HTTPS, "https://gitea.internal:443/", false}, {"remote ip", setting.HTTPS, "https://10.0.0.5:3000/", false}, // an unparseable LOCAL_ROOT_URL is a hard misconfiguration; fail closed to verification @@ -31,7 +34,9 @@ func TestInternalAPIConnectionIsLocal(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - assert.Equal(t, c.want, internalAPIConnectionIsLocal(c.protocol, c.localURL)) + config := internalAPITLSConfig(c.protocol, c.localURL, "gitea.example.com") + assert.Equal(t, c.local, config.InsecureSkipVerify) + assert.Equal(t, util.Iif(c.local, "gitea.example.com", ""), config.ServerName) }) } }