Add initial support of device posture checks (#1540)

This PR implements the following posture checks:

* Agent minimum version allowed
* OS minimum version allowed
* Geo-location based on connection IP

For the geo-based location, we rely on GeoLite2 databases which are free IP geolocation databases. MaxMind was tested and we provide a script that easily allows to download of all necessary files, see infrastructure_files/download-geolite2.sh.

The OpenAPI spec should extensively cover the life cycle of current version posture checks.
This commit is contained in:
Yury Gargay
2024-02-20 09:59:56 +01:00
committed by GitHub
parent db3cba5e0f
commit 9bc7b9e897
61 changed files with 5162 additions and 348 deletions

View File

@ -9,6 +9,7 @@ import (
"golang.org/x/exp/slices"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
)
func TestAccount_getPeersByPolicy(t *testing.T) {
@ -474,6 +475,323 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
})
}
func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.7",
WtVersion: "0.25.9",
},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.0",
},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.25.8",
},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.0",
WtVersion: "0.25.9",
},
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.24.0",
},
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.25.9",
},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.2",
},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP("100.65.29.55"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.1",
},
},
"peerI": {
ID: "peerI",
IP: net.ParseIP("100.65.21.56"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "windows",
KernelVersion: "10.0.14393.2430",
WtVersion: "0.25.1",
},
},
},
Groups: map[string]*Group{
"GroupAll": {
ID: "GroupAll",
Name: "All",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerC",
"peerF",
"peerG",
"peerH",
"peerI",
},
},
"GroupSwarm": {
ID: "GroupSwarm",
Name: "swarm",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerE",
"peerG",
"peerH",
"peerI",
},
},
},
PostureChecks: []*posture.Checks{
{
ID: "PostureChecksDefault",
Name: "Default",
Description: "This is a posture checks that check if peer is running required versions",
Checks: posture.ChecksDefinition{
NBVersionCheck: &posture.NBVersionCheck{
MinVersion: "0.25",
},
OSVersionCheck: &posture.OSVersionCheck{
Linux: &posture.MinKernelVersionCheck{
MinKernelVersion: "6.6.0",
},
},
},
},
},
}
account.Policies = append(account.Policies, &Policy{
ID: "PolicyPostureChecks",
Name: "",
Description: "This is the policy with posture checks applied",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "RuleSwarm",
Name: "Swarm",
Enabled: true,
Action: PolicyTrafficActionAccept,
Destinations: []string{
"GroupSwarm",
},
Sources: []string{
"GroupAll",
},
Bidirectional: false,
Protocol: PolicyRuleProtocolTCP,
Ports: []string{"80"},
},
},
SourcePostureChecks: []string{
"PostureChecksDefault",
},
})
t.Run("verify peer's network map with default group peer list", func(t *testing.T) {
// peerB doesn't fulfill the NB posture check but is included in the destination group Swarm,
// will establish a connection with all source peers satisfying the NB posture check.
peers, firewallRules := account.getPeerConnectionResources("peerB")
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules = account.getPeerConnectionResources("peerC")
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, 1)
expectedFirewallRules := []*FirewallRule{
{
PeerIP: "0.0.0.0",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
}
assert.ElementsMatch(t, firewallRules, expectedFirewallRules)
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources("peerE")
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
// peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources("peerI")
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
})
t.Run("verify peer's network map with modified group peer list", func(t *testing.T) {
// Removing peerB as the part of destination group Swarm
account.Groups["GroupSwarm"].Peers = []string{"peerA", "peerD", "peerE", "peerG", "peerH"}
// peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules := account.getPeerConnectionResources("peerB")
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules = account.getPeerConnectionResources("peerI")
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules = account.getPeerConnectionResources("peerC")
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers))
peerIDs := make([]string, 0, len(peers))
for _, peer := range peers {
peerIDs = append(peerIDs, peer.ID)
}
assert.ElementsMatch(t, peerIDs, account.Groups["GroupSwarm"].Peers)
// Removing peerF as the part of source group All
account.Groups["GroupAll"].Peers = []string{"peerB", "peerA", "peerD", "peerC", "peerG", "peerH"}
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources("peerE")
assert.Len(t, peers, 3)
assert.Len(t, firewallRules, 3)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
peers, firewallRules = account.getPeerConnectionResources("peerA")
assert.Len(t, peers, 5)
// assert peers from Group Swarm
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerE"])
assert.Contains(t, peers, account.Peers["peerG"])
assert.Contains(t, peers, account.Peers["peerH"])
// assert peers from Group All
assert.Contains(t, peers, account.Peers["peerC"])
expectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.32.206",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.13.186",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.29.55",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
}
assert.Len(t, firewallRules, len(expectedFirewallRules))
assert.ElementsMatch(t, firewallRules, expectedFirewallRules)
})
}
func sortFunc() func(a *FirewallRule, b *FirewallRule) int {
return func(a, b *FirewallRule) int {
// Concatenate PeerIP and Direction as string for comparison