admin: default to 0.0.0.0 for authenticated HTTP, keep worker gRPC on loopback (#11314)

* admin: extract isFlagExplicitlySet helper from applyViperFallback

No behavior change; the inline flag-visit check becomes a reusable
helper so the upcoming bind-address default can share it.

* admin: default to 0.0.0.0 for authenticated HTTP, keep worker gRPC on loopback

PR #11185 made the admin HTTP server default to 127.0.0.1 to stop
exposing the unauthenticated admin API on the network by accident.
That also locked out operators who already authenticated with
-adminPassword: their UI became unreachable from the network after
upgrade unless they added -ip=0.0.0.0 (see #11303).

An authenticated deployment is safe to expose, so auto-upgrade the -ip
default to 0.0.0.0 when -adminPassword or [https.admin] mTLS is
configured. The loopback default stays for the unauthenticated case, so
the unauthenticated API is never exposed on the network. An explicit
-ip is always honored.

The worker gRPC control plane has no password auth (only mTLS), so it
must not follow the HTTP upgrade. Give it a separate bind address that
stays on loopback unless -ip is explicit, so adminPassword no longer
re-exposes the unauthenticated worker stream.

* admin: hint loopback-only bind in startup banner

When the admin server binds to loopback (the default for the
unauthenticated case), print a one-line hint that it is not reachable
from other hosts and how to expose it. This helps operators who, after
the #11185 loopback default, can no longer reach the UI from another
machine quickly see the cause and the fix without reading the docs.

* admin: keep worker gRPC on loopback, decouple from https.admin mTLS

The worker gRPC auto-upgrade to 0.0.0.0 was gated on hasMTLS, which
reads the https.admin (HTTP) mTLS config. The worker gRPC mTLS comes
from grpc.admin + grpc.ca, a separate config, so:

- https.admin mTLS without grpc.admin mTLS widened the worker gRPC to
  0.0.0.0 unauthenticated (re-exposing the control plane), and
- grpc.admin mTLS without https.admin mTLS left the worker gRPC on
  loopback, blocking authenticated remote workers.

Drop the worker gRPC auto-upgrade entirely. The worker gRPC keeps the
raw -ip value (loopback by default), matching the pre-existing
behavior; an operator who wants remote workers sets -ip explicitly.
Only the HTTP admin listener auto-upgrades to 0.0.0.0 when
authenticated.

Addresses review feedback on #11314 from Devin and Greptile.
This commit is contained in:
Chris Lu
2026-09-14 14:04:35 -07:00
committed by GitHub
parent 2aa6af033d
commit c997e54096
+66 -22
View File
@@ -68,6 +68,13 @@ type AdminOptions struct {
// binds it only after every other service is up.
workerGrpcListener net.Listener
// workerGrpcBindIp, when non-empty, is the address the worker gRPC
// listener binds to. It is separate from ip because the worker gRPC has
// no password auth (its mTLS comes from grpc.admin, not https.admin), so
// it must not follow ip's auto-upgrade to 0.0.0.0 based on adminPassword.
// `weed mini` leaves it empty to fall back to ip.
workerGrpcBindIp string
// defaultS3PublicEndpoint, when set, is used for object URLs when
// s3.public_endpoint is not configured. `weed mini` sets it to its own
// S3 address.
@@ -78,7 +85,7 @@ func init() {
cmdAdmin.Run = runAdmin // break init cycle
a.port = cmdAdmin.Flag.Int("port", 23646, "admin server port")
a.grpcPort = cmdAdmin.Flag.Int("port.grpc", 0, "gRPC server port for worker connections (default: http port + 10000)")
a.ip = cmdAdmin.Flag.String("ip", "127.0.0.1", "ip address to listen on. Default is loopback; set to 0.0.0.0 to listen on all interfaces (requires -adminPassword or [https.admin] mTLS in security.toml).")
a.ip = cmdAdmin.Flag.String("ip", "127.0.0.1", "ip address to listen on. Defaults to loopback when auth is disabled, or 0.0.0.0 when -adminPassword or [https.admin] mTLS is configured. Set explicitly to override.")
a.master = cmdAdmin.Flag.String("master", "localhost:9333", "comma-separated master servers")
a.masters = cmdAdmin.Flag.String("masters", "", "comma-separated master servers (deprecated, use -master instead)")
a.filerGroup = cmdAdmin.Flag.String("filerGroup", "", "filerGroup for the filers, brokers, and S3 servers")
@@ -143,13 +150,15 @@ var cmdAdmin = &Command{
- Precedence: CLI flag > env var / security.toml > default value
Network Binding:
- By default the admin server binds to 127.0.0.1 (loopback only).
- Use -ip=0.0.0.0 to listen on all interfaces.
- When binding to a non-loopback address, authentication MUST be enabled
(-adminPassword) or mTLS configured ([https.admin] key and ca in security.toml).
Otherwise the server refuses to start.
- Use -allowInsecureBind to start anyway with an unauthenticated admin API
exposed on the network. INSECURE; only for trusted isolated networks.
- When authentication is disabled, the admin server binds to 127.0.0.1
(loopback only) so the unauthenticated API is never exposed on the network.
- When -adminPassword or [https.admin] mTLS is configured, the default
upgrades to 0.0.0.0 (all interfaces) so authenticated deployments stay
reachable from the network without an explicit -ip flag.
- Set -ip explicitly to override either default.
- Binding a non-loopback address with authentication disabled (no
-adminPassword and no mTLS) is refused unless -allowInsecureBind is set
(INSECURE; only for trusted isolated networks).
Security Configuration:
- The admin server reads TLS configuration from security.toml
@@ -287,6 +296,25 @@ func runAdmin(cmd *Command, args []string) bool {
*a.grpcPort = *a.port + 10000
}
hasMTLS := viper.GetString("https.admin.key") != "" && viper.GetString("https.admin.ca") != ""
// The worker gRPC control plane has no password auth (its mTLS comes from
// grpc.admin, separate from https.admin), so its bind address must not
// follow the HTTP auto-upgrade below. Capture the raw -ip value first;
// the worker gRPC stays on loopback unless the operator sets -ip
// explicitly, matching the pre-existing behavior.
a.workerGrpcBindIp = *a.ip
// -ip defaults to loopback so an unauthenticated admin API is never
// exposed on the network by accident. An authenticated deployment
// (adminPassword or mTLS) is safe to reach from the network, so upgrade
// the default to 0.0.0.0 and keep existing deployments reachable after
// upgrade without forcing a -ip=0.0.0.0 config change. An operator who
// explicitly set -ip is left alone.
if !isFlagExplicitlySet(cmd, "ip") && (*a.adminPassword != "" || hasMTLS) {
*a.ip = "0.0.0.0"
}
// Security validation: refuse to bind a non-loopback address without
// authentication or mTLS. This prevents accidental exposure of the
// unauthenticated admin REST API on the network. Server-only TLS
@@ -295,7 +323,6 @@ func runAdmin(cmd *Command, args []string) bool {
// or configure mTLS (both key and ca).
// -allowInsecureBind opts out of this check for operators who knowingly
// keep the pre-existing unauthenticated setup.
hasMTLS := viper.GetString("https.admin.key") != "" && viper.GetString("https.admin.ca") != ""
insecureAllowed := a.allowInsecureBind != nil && *a.allowInsecureBind
if !isLoopbackIp(*a.ip) && *a.adminPassword == "" && !hasMTLS {
if !insecureAllowed {
@@ -320,6 +347,9 @@ func runAdmin(cmd *Command, args []string) bool {
fmt.Println(" Set -adminPassword for production use")
}
fmt.Printf("Starting SeaweedFS Admin Interface on %s\n", util.JoinHostPort(*a.ip, *a.port))
if isLoopbackIp(*a.ip) {
fmt.Printf(" (loopback only; not reachable from other hosts. Set -ip=0.0.0.0 with -adminPassword or mTLS to expose.)\n")
}
fmt.Printf("Worker gRPC server will run on port %d\n", *a.grpcPort)
fmt.Printf("Masters: %s\n", *a.master)
fmt.Printf("Filers will be discovered automatically from masters\n")
@@ -460,12 +490,19 @@ func startAdminServer(ctx context.Context, options AdminOptions, enableUI bool,
glog.Infof("No filers discovered from masters")
}
// Start worker gRPC server for worker connections
err = adminServer.StartWorkerGrpcServer(*options.ip, *options.grpcPort, options.workerGrpcListener)
// Start worker gRPC server for worker connections. The worker gRPC binds
// to its own address (workerGrpcBindIp) which, unlike the HTTP ip, does
// not auto-upgrade to 0.0.0.0 based on adminPassword, since the worker
// gRPC has no password auth.
workerGrpcIp := options.workerGrpcBindIp
if workerGrpcIp == "" {
workerGrpcIp = *options.ip
}
err = adminServer.StartWorkerGrpcServer(workerGrpcIp, *options.grpcPort, options.workerGrpcListener)
if err != nil {
return fmt.Errorf("failed to start worker gRPC server: %w", err)
}
warnInsecureWorkerGrpcBind(*options.ip, *options.grpcPort, adminServer.WorkerGrpcMTLSEnabled())
warnInsecureWorkerGrpcBind(workerGrpcIp, *options.grpcPort, adminServer.WorkerGrpcMTLSEnabled())
// Set up cleanup for gRPC server
defer func() {
@@ -729,19 +766,26 @@ func loadOrGenerateSessionKeys(dataDir string) ([]byte, []byte, error) {
return key[:keyLen], key[keyLen:], nil
}
// isFlagExplicitlySet reports whether the named flag was passed on the
// command line (as opposed to left at its default).
func isFlagExplicitlySet(cmd *Command, flagName string) bool {
set := false
cmd.Flag.Visit(func(f *flag.Flag) {
if f.Name == flagName {
set = true
}
})
return set
}
// applyViperFallback sets a flag's value from viper (security.toml / env var)
// when the flag was not explicitly set on the command line.
func applyViperFallback(cmd *Command, flagPtr *string, flagName, viperKey string) {
explicitlySet := false
cmd.Flag.Visit(func(f *flag.Flag) {
if f.Name == flagName {
explicitlySet = true
}
})
if !explicitlySet {
if v := util.GetViper().GetString(viperKey); v != "" {
*flagPtr = v
}
if isFlagExplicitlySet(cmd, flagName) {
return
}
if v := util.GetViper().GetString(viperKey); v != "" {
*flagPtr = v
}
}