From 639788cf70b3070bb76988451baf1207c096c4b8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 20 Jul 2026 12:32:05 -0500 Subject: [PATCH] waved: allow notls/no-macaroons on mainnet behind an opt-in flag The daemon refuses rpc.notls and rpc.no-macaroons on mainnet TCP listeners so a stray flag can't silently stand up an unauthenticated, plaintext RPC surface on real funds. That guard assumes the daemon terminates its own transport security, which isn't true for deployments that front waved with a proxy that terminates TLS and enforces auth before traffic ever reaches the listener. For those operators the guard is a false positive that blocks startup outright. In this commit, we add an allow-insecure-mainnet escape hatch. The flag is off by default, so the guard still stands for everyone who hasn't deliberately opted out; when set, it lifts the mainnet-TCP refusal for both options. The lightning-infra tracking issue carries the rationale for why external TLS termination is the deployment model here. (cherry picked from commit 6b4cf14c4b79f73832f555a146f5631e5a728a8f) --- cmd/waved/main.go | 10 ++++++ sample-waved.conf | 5 +++ waved/config.go | 20 ++++++++++-- waved/config_network_test.go | 63 ++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/cmd/waved/main.go b/cmd/waved/main.go index d1da04016..773116fec 100644 --- a/cmd/waved/main.go +++ b/cmd/waved/main.go @@ -175,6 +175,16 @@ func newRootCmd() *cobra.Command { "run on mainnet (required when network=mainnet)", ) + // Escape hatch for deployments that terminate TLS and enforce auth + // at an external proxy: permit rpc.notls/rpc.no-macaroons on mainnet + // TCP listeners, which Validate() otherwise refuses. + f.Bool( + "allow-insecure-mainnet", cfg.AllowInsecureMainnet, "permit "+ + "rpc.notls and rpc.no-macaroons on mainnet TCP "+ + "listeners (transport security is expected to be "+ + "provided externally)", + ) + // Cap the per-round operator fee the client is willing to pay // under the #270 seal-time fee handshake. Zero is rejected at // config-load time as an explicit misconfiguration. diff --git a/sample-waved.conf b/sample-waved.conf index 064027efe..9e542d095 100644 --- a/sample-waved.conf +++ b/sample-waved.conf @@ -43,6 +43,11 @@ # Explicit opt-in for mainnet operation. # allow-mainnet=false +# Permit rpc.notls and rpc.no-macaroons on mainnet TCP listeners, for +# deployments that terminate TLS and enforce authentication at an external +# proxy layer. Off by default; both options are otherwise refused on mainnet. +# allow-insecure-mainnet=false + # Maximum operator fee, in satoshis, accepted per seal-time quote. # maxoperatorfeesat=1000000 diff --git a/waved/config.go b/waved/config.go index 5bb8db4ab..fda6d3b41 100644 --- a/waved/config.go +++ b/waved/config.go @@ -324,6 +324,16 @@ type Config struct { // mainnet during development, since DefaultNetwork is "mainnet". AllowMainnet bool `mapstructure:"allow-mainnet"` + // AllowInsecureMainnet permits the rpc.notls and rpc.no-macaroons + // options on mainnet TCP listeners. Both are refused on mainnet by + // default so a stray flag can't silently expose an unauthenticated, + // plaintext RPC surface. Deployments that terminate TLS and enforce + // authentication at an external proxy must set this explicitly to + // acknowledge that the daemon's own RPC listener runs without + // transport security. See the lightning-infra tracking issue for the + // deployment rationale. + AllowInsecureMainnet bool `mapstructure:"allow-insecure-mainnet"` + // Unroll configures the unilateral-exit subsystem. Unroll *UnrollConfig `mapstructure:"unroll"` @@ -1610,14 +1620,18 @@ func (c *Config) rpcMacaroonPath() string { // validateRPCSecurity normalizes daemon RPC TLS and macaroon paths. func (c *Config) validateRPCSecurity() error { - if c.Network == "mainnet" && c.RPC.Listener == nil { + if c.Network == "mainnet" && c.RPC.Listener == nil && + !c.AllowInsecureMainnet { + if c.RPC.NoTLS { return fmt.Errorf("rpc.notls cannot be used on " + - "mainnet TCP listeners") + "mainnet TCP listeners unless " + + "allow-insecure-mainnet is set") } if c.RPC.NoMacaroons { return fmt.Errorf("rpc.no-macaroons cannot be used " + - "on mainnet TCP listeners") + "on mainnet TCP listeners unless " + + "allow-insecure-mainnet is set") } } diff --git a/waved/config_network_test.go b/waved/config_network_test.go index f1494299d..d2c1cae12 100644 --- a/waved/config_network_test.go +++ b/waved/config_network_test.go @@ -73,6 +73,69 @@ func TestConfigValidateAllowsTestnet4InsecureRPC(t *testing.T) { require.NoError(t, cfg.Validate()) } +// TestConfigValidateMainnetInsecureRPC verifies that rpc.notls and +// rpc.no-macaroons are refused on mainnet TCP listeners by default, and +// permitted once the operator opts in with allow-insecure-mainnet. +func TestConfigValidateMainnetInsecureRPC(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + noTLS bool + noMacaroons bool + override bool + wantErr string + }{ + { + name: "notls refused by default", + noTLS: true, + wantErr: "rpc.notls cannot be used", + }, + { + name: "no-macaroons refused by default", + noMacaroons: true, + wantErr: "rpc.no-macaroons cannot be used", + }, + { + name: "notls allowed with override", + noTLS: true, + override: true, + }, + { + name: "no-macaroons allowed with override", + noMacaroons: true, + override: true, + }, + { + name: "both allowed with override", + noTLS: true, + noMacaroons: true, + override: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfg := DefaultConfig() + cfg.Network = "mainnet" + cfg.AllowMainnet = true + cfg.RPC.NoTLS = tc.noTLS + cfg.RPC.NoMacaroons = tc.noMacaroons + cfg.AllowInsecureMainnet = tc.override + + err := cfg.Validate() + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + // TestConfigValidateWalletDefaults verifies Validate fills in the // network-default Esplora/fee URL for the lwwallet and btcwallet backends // when left empty, and still requires an explicit value on networks with no