mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:18:31 +02:00
feat(config): opt-in flag for non-loopback local API bind
Validate GODOXY_LOCAL_API_ADDR before starting the unauthenticated local API. Loopback listeners still succeed by default; addresses that bind all interfaces, unspecified IPs, LAN hosts, or non-loopback names need GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK=true. When that opt-in is set and the host is not loopback, log a warning so non-local exposure is obvious. Wire common.LocalAPIAllowNonLoopback from LOCAL_API_ALLOW_NON_LOOPBACK and document it (with a risk note) in .env.example. Add TestValidateLocalAPIAddr for loopback, wildcard, LAN, and hostname cases with the allow flag on and off.
This commit is contained in:
@@ -58,8 +58,12 @@ GODOXY_API_ADDR=127.0.0.1:8888
|
|||||||
|
|
||||||
# Local API listening address (unauthenticated, optional)
|
# Local API listening address (unauthenticated, optional)
|
||||||
# Useful for local development, debugging or automation
|
# Useful for local development, debugging or automation
|
||||||
|
# Must bind to loopback only (localhost / 127.0.0.1 / ::1) unless GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK is true
|
||||||
GODOXY_LOCAL_API_ADDR=
|
GODOXY_LOCAL_API_ADDR=
|
||||||
|
|
||||||
|
# WARNING: exposing local API to either LAN or WAN is dangerous, do not enable this unless you know what you're doing
|
||||||
|
GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK=false
|
||||||
|
|
||||||
# Metrics
|
# Metrics
|
||||||
GODOXY_METRICS_DISABLE_CPU=false
|
GODOXY_METRICS_DISABLE_CPU=false
|
||||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||||
@@ -76,4 +80,4 @@ DOCKER_SOCKET=/var/run/docker.sock
|
|||||||
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
|
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
|
||||||
|
|
||||||
# Debug mode
|
# Debug mode
|
||||||
GODOXY_DEBUG=false
|
GODOXY_DEBUG=false
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var (
|
|||||||
LocalAPIHTTPHost,
|
LocalAPIHTTPHost,
|
||||||
LocalAPIHTTPPort,
|
LocalAPIHTTPPort,
|
||||||
LocalAPIHTTPURL = env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
LocalAPIHTTPURL = env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
||||||
|
LocalAPIAllowNonLoopback = env.GetEnvBool("LOCAL_API_ALLOW_NON_LOOPBACK", false)
|
||||||
|
|
||||||
APIJWTSecure = env.GetEnvBool("API_JWT_SECURE", true)
|
APIJWTSecure = env.GetEnvBool("API_JWT_SECURE", true)
|
||||||
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))
|
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))
|
||||||
|
|||||||
77
internal/config/local_api_test.go
Normal file
77
internal/config/local_api_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestValidateLocalAPIAddr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr string
|
||||||
|
allowNonLoopback bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "localhost",
|
||||||
|
addr: "localhost:8888",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4_loopback",
|
||||||
|
addr: "127.0.0.1:8888",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6_loopback",
|
||||||
|
addr: "[::1]:8888",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all_interfaces",
|
||||||
|
addr: ":8888",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all_interfaces_allowed",
|
||||||
|
addr: ":8888",
|
||||||
|
allowNonLoopback: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4_unspecified",
|
||||||
|
addr: "0.0.0.0:8888",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4_unspecified_allowed",
|
||||||
|
addr: "0.0.0.0:8888",
|
||||||
|
allowNonLoopback: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lan_ip",
|
||||||
|
addr: "192.168.1.10:8888",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lan_ip_allowed",
|
||||||
|
addr: "192.168.1.10:8888",
|
||||||
|
allowNonLoopback: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname_not_loopback",
|
||||||
|
addr: "godoxy.internal:8888",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname_not_loopback_allowed",
|
||||||
|
addr: "godoxy.internal:8888",
|
||||||
|
allowNonLoopback: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateLocalAPIAddr(tt.addr, tt.allowNonLoopback)
|
||||||
|
if tt.wantErr && err == nil {
|
||||||
|
t.Fatalf("expected error for %q", tt.addr)
|
||||||
|
}
|
||||||
|
if !tt.wantErr && err != nil {
|
||||||
|
t.Fatalf("unexpected error for %q: %v", tt.addr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,6 +216,15 @@ func (state *state) StartAPIServers() {
|
|||||||
|
|
||||||
// Local API Handler is used for unauthenticated access.
|
// Local API Handler is used for unauthenticated access.
|
||||||
if common.LocalAPIHTTPAddr != "" {
|
if common.LocalAPIHTTPAddr != "" {
|
||||||
|
if err := validateLocalAPIAddr(common.LocalAPIHTTPAddr, common.LocalAPIAllowNonLoopback); err != nil {
|
||||||
|
log.Err(err).Str("addr", common.LocalAPIHTTPAddr).Msg("refusing to start local API server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if common.LocalAPIAllowNonLoopback && !isLoopbackLocalAPIHost(common.LocalAPIHTTPAddr) {
|
||||||
|
log.Warn().
|
||||||
|
Str("addr", common.LocalAPIHTTPAddr).
|
||||||
|
Msg("local API server is allowed to bind to non-loopback addresses")
|
||||||
|
}
|
||||||
_, err := server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
|
_, err := server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
|
||||||
Name: "local_api",
|
Name: "local_api",
|
||||||
HTTPAddr: common.LocalAPIHTTPAddr,
|
HTTPAddr: common.LocalAPIHTTPAddr,
|
||||||
@@ -227,6 +236,51 @@ func (state *state) StartAPIServers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateLocalAPIAddr(addr string, allowNonLoopback bool) error {
|
||||||
|
if isLoopbackLocalAPIHost(addr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowNonLoopback {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(host) {
|
||||||
|
case "localhost":
|
||||||
|
return nil
|
||||||
|
case "":
|
||||||
|
return errors.New("local API address must bind to a loopback host, not all interfaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := netip.ParseAddr(host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("local API address must use a loopback host: %w", err)
|
||||||
|
}
|
||||||
|
if !ip.IsLoopback() {
|
||||||
|
return fmt.Errorf("local API address must bind to a loopback host, got %q", host)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLoopbackLocalAPIHost(addr string) bool {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(host, "localhost") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := netip.ParseAddr(host)
|
||||||
|
return err == nil && ip.IsLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
func (state *state) StartMetrics() {
|
func (state *state) StartMetrics() {
|
||||||
systeminfo.Poller.Start(state.task)
|
systeminfo.Poller.Start(state.task)
|
||||||
uptime.Poller.Start(state.task)
|
uptime.Poller.Start(state.task)
|
||||||
|
|||||||
Reference in New Issue
Block a user