Allow to restrict servers that can join a given Serf Consul cluster. (#7628)

Based on work done in https://github.com/hashicorp/memberlist/pull/196
this allows to restrict the IP ranges that can join a given Serf cluster
and be a member of the cluster.

Restrictions on IPs can be done separatly using 2 new differents flags
and config options to restrict IPs for LAN and WAN Serf.
This commit is contained in:
Pierre Souchay 2020-05-20 11:31:19 +02:00 committed by GitHub
parent c985035aac
commit 3b548f0d77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 161 additions and 2 deletions

View File

@ -1177,6 +1177,8 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.SerfBindAddrLAN.IP.String() base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.SerfBindAddrLAN.IP.String()
base.SerfLANConfig.MemberlistConfig.BindPort = a.config.SerfBindAddrLAN.Port base.SerfLANConfig.MemberlistConfig.BindPort = a.config.SerfBindAddrLAN.Port
base.SerfLANConfig.MemberlistConfig.CIDRsAllowed = a.config.SerfAllowedCIDRsLAN
base.SerfWANConfig.MemberlistConfig.CIDRsAllowed = a.config.SerfAllowedCIDRsWAN
base.SerfLANConfig.MemberlistConfig.AdvertiseAddr = a.config.SerfAdvertiseAddrLAN.IP.String() base.SerfLANConfig.MemberlistConfig.AdvertiseAddr = a.config.SerfAdvertiseAddrLAN.IP.String()
base.SerfLANConfig.MemberlistConfig.AdvertisePort = a.config.SerfAdvertiseAddrLAN.Port base.SerfLANConfig.MemberlistConfig.AdvertisePort = a.config.SerfAdvertiseAddrLAN.Port
base.SerfLANConfig.MemberlistConfig.GossipVerifyIncoming = a.config.EncryptVerifyIncoming base.SerfLANConfig.MemberlistConfig.GossipVerifyIncoming = a.config.EncryptVerifyIncoming

View File

@ -24,6 +24,7 @@ import (
"github.com/hashicorp/consul/types" "github.com/hashicorp/consul/types"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr/template" "github.com/hashicorp/go-sockaddr/template"
"github.com/hashicorp/memberlist"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@ -738,6 +739,15 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
} }
} }
serfAllowedCIDRSLAN, err := memberlist.ParseCIDRs(c.SerfAllowedCIDRsLAN)
if err != nil {
return RuntimeConfig{}, fmt.Errorf("serf_lan_allowed_cidrs: %s", err)
}
serfAllowedCIDRSWAN, err := memberlist.ParseCIDRs(c.SerfAllowedCIDRsWAN)
if err != nil {
return RuntimeConfig{}, fmt.Errorf("serf_wan_allowed_cidrs: %s", err)
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// build runtime config // build runtime config
// //
@ -960,6 +970,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
Segments: segments, Segments: segments,
SerfAdvertiseAddrLAN: serfAdvertiseAddrLAN, SerfAdvertiseAddrLAN: serfAdvertiseAddrLAN,
SerfAdvertiseAddrWAN: serfAdvertiseAddrWAN, SerfAdvertiseAddrWAN: serfAdvertiseAddrWAN,
SerfAllowedCIDRsLAN: serfAllowedCIDRSLAN,
SerfAllowedCIDRsWAN: serfAllowedCIDRSWAN,
SerfBindAddrLAN: serfBindAddrLAN, SerfBindAddrLAN: serfBindAddrLAN,
SerfBindAddrWAN: serfBindAddrWAN, SerfBindAddrWAN: serfBindAddrWAN,
SerfPortLAN: serfPortLAN, SerfPortLAN: serfPortLAN,

View File

@ -270,6 +270,8 @@ type Config struct {
RetryJoinMaxAttemptsLAN *int `json:"retry_max,omitempty" hcl:"retry_max" mapstructure:"retry_max"` RetryJoinMaxAttemptsLAN *int `json:"retry_max,omitempty" hcl:"retry_max" mapstructure:"retry_max"`
RetryJoinMaxAttemptsWAN *int `json:"retry_max_wan,omitempty" hcl:"retry_max_wan" mapstructure:"retry_max_wan"` RetryJoinMaxAttemptsWAN *int `json:"retry_max_wan,omitempty" hcl:"retry_max_wan" mapstructure:"retry_max_wan"`
RetryJoinWAN []string `json:"retry_join_wan,omitempty" hcl:"retry_join_wan" mapstructure:"retry_join_wan"` RetryJoinWAN []string `json:"retry_join_wan,omitempty" hcl:"retry_join_wan" mapstructure:"retry_join_wan"`
SerfAllowedCIDRsLAN []string `json:"serf_lan_allowed_cidrs,omitempty" hcl:"serf_lan_allowed_cidrs" mapstructure:"serf_lan_allowed_cidrs"`
SerfAllowedCIDRsWAN []string `json:"serf_wan_allowed_cidrs,omitempty" hcl:"serf_wan_allowed_cidrs" mapstructure:"serf_wan_allowed_cidrs"`
SerfBindAddrLAN *string `json:"serf_lan,omitempty" hcl:"serf_lan" mapstructure:"serf_lan"` SerfBindAddrLAN *string `json:"serf_lan,omitempty" hcl:"serf_lan" mapstructure:"serf_lan"`
SerfBindAddrWAN *string `json:"serf_wan,omitempty" hcl:"serf_wan" mapstructure:"serf_wan"` SerfBindAddrWAN *string `json:"serf_wan,omitempty" hcl:"serf_wan" mapstructure:"serf_wan"`
ServerMode *bool `json:"server,omitempty" hcl:"server" mapstructure:"server"` ServerMode *bool `json:"server,omitempty" hcl:"server" mapstructure:"server"`

View File

@ -105,6 +105,8 @@ func AddFlags(fs *flag.FlagSet, f *Flags) {
add(&f.Config.RetryJoinWAN, "retry-join-wan", "Address of an agent to join -wan at start time with retries enabled. Can be specified multiple times.") add(&f.Config.RetryJoinWAN, "retry-join-wan", "Address of an agent to join -wan at start time with retries enabled. Can be specified multiple times.")
add(&f.Config.RetryJoinMaxAttemptsLAN, "retry-max", "Maximum number of join attempts. Defaults to 0, which will retry indefinitely.") add(&f.Config.RetryJoinMaxAttemptsLAN, "retry-max", "Maximum number of join attempts. Defaults to 0, which will retry indefinitely.")
add(&f.Config.RetryJoinMaxAttemptsWAN, "retry-max-wan", "Maximum number of join -wan attempts. Defaults to 0, which will retry indefinitely.") add(&f.Config.RetryJoinMaxAttemptsWAN, "retry-max-wan", "Maximum number of join -wan attempts. Defaults to 0, which will retry indefinitely.")
add(&f.Config.SerfAllowedCIDRsLAN, "serf-lan-allowed-cidrs", "Networks (eg: 192.168.1.0/24) allowed for Serf LAN. Can be specified multiple times.")
add(&f.Config.SerfAllowedCIDRsWAN, "serf-wan-allowed-cidrs", "Networks (eg: 192.168.1.0/24) allowed for Serf WAN (other datacenters). Can be specified multiple times.")
add(&f.Config.SerfBindAddrLAN, "serf-lan-bind", "Address to bind Serf LAN listeners to.") add(&f.Config.SerfBindAddrLAN, "serf-lan-bind", "Address to bind Serf LAN listeners to.")
add(&f.Config.Ports.SerfLAN, "serf-lan-port", "Sets the Serf LAN port to listen on.") add(&f.Config.Ports.SerfLAN, "serf-lan-port", "Sets the Serf LAN port to listen on.")
add(&f.Config.SegmentName, "segment", "(Enterprise-only) Sets the network segment to join.") add(&f.Config.SegmentName, "segment", "(Enterprise-only) Sets the network segment to join.")

View File

@ -1153,6 +1153,18 @@ type RuntimeConfig struct {
// hcl: bind_addr = string advertise_addr_wan = string ports { serf_wan = int } // hcl: bind_addr = string advertise_addr_wan = string ports { serf_wan = int }
SerfAdvertiseAddrWAN *net.TCPAddr SerfAdvertiseAddrWAN *net.TCPAddr
// SerfAllowedCIDRsLAN if set to a non-empty value, will restrict which networks
// are allowed to connect to Serf on the LAN.
// hcl: serf_lan_allowed_cidrs = []string
// flag: serf-lan-allowed-cidrs string (can be specified multiple times)
SerfAllowedCIDRsLAN []net.IPNet
// SerfAllowedCIDRsWAN if set to a non-empty value, will restrict which networks
// are allowed to connect to Serf on the WAN.
// hcl: serf_wan_allowed_cidrs = []string
// flag: serf-wan-allowed-cidrs string (can be specified multiple times)
SerfAllowedCIDRsWAN []net.IPNet
// SerfBindAddrLAN is the address to bind the Serf LAN TCP and UDP // SerfBindAddrLAN is the address to bind the Serf LAN TCP and UDP
// listeners to. The ip address is either the default bind address or the // listeners to. The ip address is either the default bind address or the
// 'serf_lan' address which can be either an ip address or a go-sockaddr // 'serf_lan' address which can be either an ip address or a go-sockaddr
@ -1794,6 +1806,14 @@ func sanitize(name string, v reflect.Value) reflect.Value {
case isArray(typ) || isSlice(typ): case isArray(typ) || isSlice(typ):
ma := make([]interface{}, 0, v.Len()) ma := make([]interface{}, 0, v.Len())
if strings.HasPrefix(name, "SerfAllowedCIDRs") {
for i := 0; i < v.Len(); i++ {
addr := v.Index(i).Addr()
ip := addr.Interface().(*net.IPNet)
ma = append(ma, ip.String())
}
return reflect.ValueOf(ma)
}
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
ma = append(ma, sanitize(fmt.Sprintf("%s[%d]", name, i), v.Index(i)).Interface()) ma = append(ma, sanitize(fmt.Sprintf("%s[%d]", name, i), v.Index(i)).Interface())
} }

View File

@ -1538,6 +1538,40 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
}, },
}, },
{
desc: "Serf Allowed CIDRS LAN, multiple values from flags",
args: []string{`-data-dir=` + dataDir, `-serf-lan-allowed-cidrs=127.0.0.0/4`, `-serf-lan-allowed-cidrs=192.168.0.0/24`},
json: []string{},
hcl: []string{},
patch: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))}
},
},
{
desc: "Serf Allowed CIDRS LAN/WAN, multiple values from HCL/JSON",
args: []string{`-data-dir=` + dataDir},
json: []string{`{"serf_lan_allowed_cidrs": ["127.0.0.0/4", "192.168.0.0/24"]}`,
`{"serf_wan_allowed_cidrs": ["10.228.85.46/25"]}`},
hcl: []string{`serf_lan_allowed_cidrs=["127.0.0.0/4", "192.168.0.0/24"]`,
`serf_wan_allowed_cidrs=["10.228.85.46/25"]`},
patch: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))}
rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "10.228.85.46/25"))}
},
},
{
desc: "Serf Allowed CIDRS WAN, multiple values from flags",
args: []string{`-data-dir=` + dataDir, `-serf-wan-allowed-cidrs=192.168.4.0/24`, `-serf-wan-allowed-cidrs=192.168.3.0/24`},
json: []string{},
hcl: []string{},
patch: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "192.168.4.0/24")), *(parseCIDR(t, "192.168.3.0/24"))}
},
},
// ------------------------------------------------------------ // ------------------------------------------------------------
// validations // validations
// //
@ -6201,7 +6235,11 @@ func TestSanitize(t *testing.T) {
}, },
}, },
KVMaxValueSize: 1234567800000000, KVMaxValueSize: 1234567800000000,
TxnMaxReqLen: 5678000000000000, SerfAllowedCIDRsLAN: []net.IPNet{
*parseCIDR(t, "192.168.1.0/24"),
*parseCIDR(t, "127.0.0.0/8"),
},
TxnMaxReqLen: 5678000000000000,
} }
rtJSON := `{ rtJSON := `{
@ -6424,6 +6462,8 @@ func TestSanitize(t *testing.T) {
"Segments": [], "Segments": [],
"SerfAdvertiseAddrLAN": "tcp://1.2.3.4:5678", "SerfAdvertiseAddrLAN": "tcp://1.2.3.4:5678",
"SerfAdvertiseAddrWAN": "", "SerfAdvertiseAddrWAN": "",
"SerfAllowedCIDRsLAN": ["192.168.1.0/24", "127.0.0.0/8"],
"SerfAllowedCIDRsWAN": [],
"SerfBindAddrLAN": "", "SerfBindAddrLAN": "",
"SerfBindAddrWAN": "", "SerfBindAddrWAN": "",
"SerfPortLAN": 0, "SerfPortLAN": 0,

View File

@ -67,7 +67,7 @@ func testClientDC(t *testing.T, dc string) (string, *Client) {
}) })
} }
func testClientWithConfig(t *testing.T, cb func(c *Config)) (string, *Client) { func testClientWithConfigWithErr(t *testing.T, cb func(c *Config)) (string, *Client, error) {
dir, config := testClientConfig(t) dir, config := testClientConfig(t)
if cb != nil { if cb != nil {
cb(config) cb(config)
@ -91,6 +91,13 @@ func testClientWithConfig(t *testing.T, cb func(c *Config)) (string, *Client) {
client, err := NewClientLogger(config, logger, tlsConf) client, err := NewClientLogger(config, logger, tlsConf)
if err != nil { if err != nil {
config.NotifyShutdown() config.NotifyShutdown()
}
return dir, client, err
}
func testClientWithConfig(t *testing.T, cb func(c *Config)) (string, *Client) {
dir, client, err := testClientWithConfigWithErr(t, cb)
if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
return dir, client return dir, client

View File

@ -13,6 +13,7 @@ import (
"github.com/google/tcpproxy" "github.com/google/tcpproxy"
"github.com/hashicorp/consul/agent/connect/ca" "github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/memberlist"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/metadata" "github.com/hashicorp/consul/agent/metadata"
@ -28,6 +29,7 @@ import (
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -370,6 +372,68 @@ func TestServer_JoinLAN(t *testing.T) {
}) })
} }
// TestServer_JoinLAN_SerfAllowedCIDRs test that IPs might be blocked
// with Serf.
// To run properly, this test requires to be able to bind and have access
// on 127.0.1.1 which is the case for most Linux machines and Windows,
// so Unit test will run in the CI.
// To run it on Mac OS, please run this commandd first, otherwise the
// test will be skipped: `sudo ifconfig lo0 alias 127.0.1.1 up`
func TestServer_JoinLAN_SerfAllowedCIDRs(t *testing.T) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.BootstrapExpect = 1
lan, err := memberlist.ParseCIDRs([]string{"127.0.0.1/32"})
assert.NoError(t, err)
c.SerfLANConfig.MemberlistConfig.CIDRsAllowed = lan
wan, err := memberlist.ParseCIDRs([]string{"127.0.0.0/24", "::1/128"})
assert.NoError(t, err)
c.SerfWANConfig.MemberlistConfig.CIDRsAllowed = wan
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
targetAddr := "127.0.1.1"
dir2, a2, err := testClientWithConfigWithErr(t, func(c *Config) {
c.SerfLANConfig.MemberlistConfig.BindAddr = targetAddr
})
defer os.RemoveAll(dir2)
if err != nil {
t.Skipf("Cannot bind on %s, to run on Mac OS: `sudo ifconfig lo0 alias 127.0.1.1 up`", targetAddr)
}
defer a2.Shutdown()
dir3, rs3 := testServerWithConfig(t, func(c *Config) {
c.BootstrapExpect = 1
c.Datacenter = "dc2"
})
defer os.RemoveAll(dir3)
defer rs3.Shutdown()
leaderAddr := joinAddrLAN(s1)
if _, err := a2.JoinLAN([]string{leaderAddr}); err != nil {
t.Fatalf("Expected no error, had: %#v", err)
}
// Try to join
joinWAN(t, rs3, s1)
retry.Run(t, func(r *retry.R) {
if got, want := len(s1.LANMembers()), 1; got != want {
// LAN is blocked, should be 1 only
r.Fatalf("got %d s1 LAN members want %d", got, want)
}
if got, want := len(a2.LANMembers()), 2; got != want {
// LAN is blocked a2 can see s1, but not s1
r.Fatalf("got %d a2 LAN members want %d", got, want)
}
if got, want := len(s1.WANMembers()), 2; got != want {
r.Fatalf("got %d s1 WAN members want %d", got, want)
}
if got, want := len(rs3.WANMembers()), 2; got != want {
r.Fatalf("got %d rs3 WAN members want %d", got, want)
}
})
}
func TestServer_LANReap(t *testing.T) { func TestServer_LANReap(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -445,10 +445,20 @@ The options below are all specified on the command-line.
more details. By default, this is an empty string, which is the default network more details. By default, this is an empty string, which is the default network
segment. segment.
- `-serf-lan-allowed-cidrs` - The Serf LAN allowed CIDRs allow to accept incoming
connections for Serf only from several networks (mutiple values are supported).
Those networks are specified with CIDR notation (eg: 192.168.1.0/24).
This is available in Consul 1.8 and later.
- `-serf-lan-port` ((#\_serf_lan_port)) - the Serf LAN port to listen on. - `-serf-lan-port` ((#\_serf_lan_port)) - the Serf LAN port to listen on.
This overrides the default Serf LAN port 8301. This is available in Consul 1.2.2 This overrides the default Serf LAN port 8301. This is available in Consul 1.2.2
and later. and later.
- `-serf-lan-allowed-cidrs` - he Serf LAN allowed CIDRs allow to accept incoming
connections for Serf only from several networks (mutiple values are supported).
Those networks are specified with CIDR notation (eg: 192.168.1.0/24).
This is available in Consul 1.8 and later.
- `-serf-wan-port` ((#\_serf_wan_port)) - the Serf WAN port to listen on. - `-serf-wan-port` ((#\_serf_wan_port)) - the Serf WAN port to listen on.
This overrides the default Serf WAN port 8302. This is available in Consul 1.2.2 This overrides the default Serf WAN port 8302. This is available in Consul 1.2.2
and later. and later.