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