Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,8 @@ func (a *Agent) consulConfig() (*consul.Config, error) {

base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.SerfBindAddrLAN.IP.String()
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.AdvertisePort = a.config.SerfAdvertiseAddrLAN.Port
base.SerfLANConfig.MemberlistConfig.GossipVerifyIncoming = a.config.EncryptVerifyIncoming
Expand Down
12 changes: 12 additions & 0 deletions agent/config/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr/template"
"github.com/hashicorp/memberlist"
"golang.org/x/time/rate"
)

Expand Down Expand Up @@ -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
//
Expand Down Expand Up @@ -960,6 +970,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
Segments: segments,
SerfAdvertiseAddrLAN: serfAdvertiseAddrLAN,
SerfAdvertiseAddrWAN: serfAdvertiseAddrWAN,
SerfAllowedCIDRsLAN: serfAllowedCIDRSLAN,
SerfAllowedCIDRsWAN: serfAllowedCIDRSWAN,
SerfBindAddrLAN: serfBindAddrLAN,
SerfBindAddrWAN: serfBindAddrWAN,
SerfPortLAN: serfPortLAN,
Expand Down
2 changes: 2 additions & 0 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ type Config struct {
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"`
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"`
SerfBindAddrWAN *string `json:"serf_wan,omitempty" hcl:"serf_wan" mapstructure:"serf_wan"`
ServerMode *bool `json:"server,omitempty" hcl:"server" mapstructure:"server"`
Expand Down
2 changes: 2 additions & 0 deletions agent/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.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.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.")
Expand Down
20 changes: 20 additions & 0 deletions agent/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,18 @@ type RuntimeConfig struct {
// hcl: bind_addr = string advertise_addr_wan = string ports { serf_wan = int }
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
// 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
Expand Down Expand Up @@ -1794,6 +1806,14 @@ func sanitize(name string, v reflect.Value) reflect.Value {

case isArray(typ) || isSlice(typ):
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++ {
ma = append(ma, sanitize(fmt.Sprintf("%s[%d]", name, i), v.Index(i)).Interface())
}
Expand Down
42 changes: 41 additions & 1 deletion agent/config/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down Expand Up @@ -6201,7 +6235,11 @@ func TestSanitize(t *testing.T) {
},
},
KVMaxValueSize: 1234567800000000,
TxnMaxReqLen: 5678000000000000,
SerfAllowedCIDRsLAN: []net.IPNet{
*parseCIDR(t, "192.168.1.0/24"),
*parseCIDR(t, "127.0.0.0/8"),
},
TxnMaxReqLen: 5678000000000000,
}

rtJSON := `{
Expand Down Expand Up @@ -6424,6 +6462,8 @@ func TestSanitize(t *testing.T) {
"Segments": [],
"SerfAdvertiseAddrLAN": "tcp://1.2.3.4:5678",
"SerfAdvertiseAddrWAN": "",
"SerfAllowedCIDRsLAN": ["192.168.1.0/24", "127.0.0.0/8"],
"SerfAllowedCIDRsWAN": [],
"SerfBindAddrLAN": "",
"SerfBindAddrWAN": "",
"SerfPortLAN": 0,
Expand Down
9 changes: 8 additions & 1 deletion agent/consul/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
if cb != nil {
cb(config)
Expand All @@ -91,6 +91,13 @@ func testClientWithConfig(t *testing.T, cb func(c *Config)) (string, *Client) {
client, err := NewClientLogger(config, logger, tlsConf)
if err != nil {
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)
}
return dir, client
Expand Down
64 changes: 64 additions & 0 deletions agent/consul/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/google/tcpproxy"
"github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/memberlist"

"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/metadata"
Expand All @@ -28,6 +29,7 @@ import (
"github.com/hashicorp/go-uuid"
"golang.org/x/time/rate"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -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) {
t.Parallel()

Expand Down
10 changes: 10 additions & 0 deletions website/pages/docs/agent/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.
This overrides the default Serf LAN port 8301. This is available in Consul 1.2.2
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.
This overrides the default Serf WAN port 8302. This is available in Consul 1.2.2
and later.
Expand Down