mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 06:49:07 +01:00
[management] fix posture check evaluation for direct peers in policy definition (#7348)
This commit is contained in:
@@ -577,7 +577,7 @@ func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string)
|
||||
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
|
||||
continue
|
||||
}
|
||||
if !isPeerInPolicySourceGroupsFromData(nmData, peerID, policy) {
|
||||
if !isPeerInPolicySourcesFromData(nmData, peerID, policy) {
|
||||
continue
|
||||
}
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
@@ -590,11 +590,14 @@ func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string)
|
||||
return maps.Values(peerPostureChecks)
|
||||
}
|
||||
|
||||
func isPeerInPolicySourceGroupsFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool {
|
||||
func isPeerInPolicySourcesFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool {
|
||||
for _, rule := range policy.Rules {
|
||||
if rule == nil || !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID == peerID {
|
||||
return true
|
||||
}
|
||||
for _, groupID := range rule.Sources {
|
||||
if group := nmData.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
|
||||
return true
|
||||
@@ -1314,7 +1317,7 @@ func computeForwarderPortFromVersions(wtVersions []string, requiredVersion strin
|
||||
|
||||
// addPolicyPostureChecks adds posture checks from a policy to the peer posture checks map if the peer is in the policy's source groups.
|
||||
func addPolicyPostureChecks(account *types.Account, peerID string, policy *types.Policy, peerPostureChecks map[string]*posture.Checks) error {
|
||||
isInGroup, err := isPeerInPolicySourceGroups(account, peerID, policy)
|
||||
isInGroup, err := isPeerInPolicySources(account, peerID, policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1334,13 +1337,17 @@ func addPolicyPostureChecks(account *types.Account, peerID string, policy *types
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPeerInPolicySourceGroups checks if a peer is present in any of the policy rule source groups.
|
||||
func isPeerInPolicySourceGroups(account *types.Account, peerID string, policy *types.Policy) (bool, error) {
|
||||
// isPeerInPolicySources checks if a peer is a source of the policy, directly or through a source group.
|
||||
func isPeerInPolicySources(account *types.Account, peerID string, policy *types.Policy) (bool, error) {
|
||||
for _, rule := range policy.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID == peerID {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, sourceGroup := range rule.Sources {
|
||||
group := account.GetGroup(sourceGroup)
|
||||
if group == nil {
|
||||
|
||||
@@ -4,35 +4,65 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
func TestPeerPostureChecksFromData_ReturnsTwinsUnchanged(t *testing.T) {
|
||||
check := &nmdata.PostureChecks{
|
||||
ID: "pc1",
|
||||
Checks: nmdata.ChecksDefinition{
|
||||
NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.30.0"},
|
||||
OSVersionCheck: &nmdata.OSVersionCheck{Linux: &nmdata.MinKernelVersionCheck{MinKernelVersion: "6.1"}},
|
||||
func postureSelectionData(policies ...*nmdata.Policy) *networkmap.NetworkMapData {
|
||||
return &networkmap.NetworkMapData{
|
||||
Groups: map[string]*nmdata.Group{"g-src": {ID: "g-src", Peers: []string{"peer-group"}}},
|
||||
Policies: policies,
|
||||
PostureChecks: map[string]*nmdata.PostureChecks{
|
||||
"pc1": {ID: "pc1", Checks: nmdata.ChecksDefinition{NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.30.0"}}},
|
||||
},
|
||||
}
|
||||
nmData := &networkmap.NetworkMapData{
|
||||
Groups: map[string]*nmdata.Group{"g1": {ID: "g1", Peers: []string{"peer1"}}},
|
||||
Policies: []*nmdata.Policy{{
|
||||
ID: "policy1",
|
||||
Enabled: true,
|
||||
SourcePostureChecks: []string{"pc1"},
|
||||
Rules: []*nmdata.PolicyRule{{ID: "rule1", Enabled: true, Sources: []string{"g1"}}},
|
||||
}},
|
||||
PostureChecks: map[string]*nmdata.PostureChecks{"pc1": check},
|
||||
}
|
||||
|
||||
func gatedPolicy(id string, rule *nmdata.PolicyRule, checkIDs ...string) *nmdata.Policy {
|
||||
return &nmdata.Policy{ID: id, Enabled: true, SourcePostureChecks: checkIDs, Rules: []*nmdata.PolicyRule{rule}}
|
||||
}
|
||||
|
||||
func checkIDs(checks []*nmdata.PostureChecks) []string {
|
||||
ids := make([]string, 0, len(checks))
|
||||
for _, c := range checks {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func TestPeerPostureChecksFromData_SelectsPolicySourcePeers(t *testing.T) {
|
||||
groupRule := &nmdata.PolicyRule{ID: "r-group", Enabled: true, Sources: []string{"g-src"}, Destinations: []string{"g-dst"}}
|
||||
directRule := &nmdata.PolicyRule{ID: "r-direct", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypePeer)}, Destinations: []string{"g-dst"}}
|
||||
|
||||
t.Run("source group member and direct source peer both get the checks", func(t *testing.T) {
|
||||
nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", directRule, "pc1"))
|
||||
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group")))
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-direct")))
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-elsewhere"))
|
||||
})
|
||||
|
||||
t.Run("source resource of a non-peer type never matches a peer", func(t *testing.T) {
|
||||
hostRule := &nmdata.PolicyRule{ID: "r-host", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypeHost)}, Destinations: []string{"g-dst"}}
|
||||
nmData := postureSelectionData(gatedPolicy("p1", hostRule, "pc1"))
|
||||
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-direct"))
|
||||
})
|
||||
|
||||
t.Run("same check through two policies is returned once", func(t *testing.T) {
|
||||
nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", groupRule, "pc1"))
|
||||
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group")))
|
||||
})
|
||||
|
||||
got := peerPostureChecksFromData(nmData, "peer1")
|
||||
require.Len(t, got, 1)
|
||||
assert.Same(t, check, got[0])
|
||||
assert.Len(t, got[0].GetChecks(), 2)
|
||||
t.Run("disabled policy, disabled rule and dangling check are ignored", func(t *testing.T) {
|
||||
disabledPolicy := gatedPolicy("p-off", groupRule, "pc1")
|
||||
disabledPolicy.Enabled = false
|
||||
disabledRule := &nmdata.PolicyRule{ID: "r-off", Enabled: false, Sources: []string{"g-src"}}
|
||||
nmData := postureSelectionData(disabledPolicy, gatedPolicy("p-rule-off", disabledRule, "pc1"), gatedPolicy("p-dangling", groupRule, "pc-missing"))
|
||||
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-outside-source-group"))
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-group"))
|
||||
})
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "A peer named directly as a rule source or destination is subject to approval exactly like a group member: unvalidated peer-b is neither a source for peer-c nor a destination for peer-a, while the validated direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.",
|
||||
"peers": ["peer-a", "peer-c"],
|
||||
"modes": ["full", "envelope"]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "4deEImv8zGvsyBmmfC2G0eQkbyMzyGuz/YK7pcYETwM=",
|
||||
"allowedIps": [
|
||||
"100.64.0.3/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"Network": {"Serial": 22},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"ValidatedPeers": {"peer-a": {}, "peer-c": {}},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-direct-ok",
|
||||
"PublicID": "pol-direct-ok-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-a", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-src-unval",
|
||||
"PublicID": "pol-src-unval-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["8443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-b", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-dst-unval",
|
||||
"PublicID": "pol-dst-unval-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["9443"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "peer-b", "Type": "peer"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/case.json
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "A peer named directly as a rule source is gated by the policy's posture checks exactly like a group member: peer-b (0.40.0) fails the 0.45.0 minimum, so it gets no connectivity and peer-c must not see it, while the compliant direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.",
|
||||
"peers": ["peer-b", "peer-c"],
|
||||
"modes": ["full", "envelope"]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"Network": {"Serial": 21},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.40.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"PostureChecks": {
|
||||
"chk-ver": {"Checks": {"NBVersionCheck": {"MinVersion": "0.45.0"}}}
|
||||
},
|
||||
"PostureCheckXIDToPublicID": {"chk-ver": "chk-ver-pub"},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-direct-ok",
|
||||
"PublicID": "pol-direct-ok-pub",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-ver"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-a", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-direct-denied",
|
||||
"PublicID": "pol-direct-denied-pub",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-ver"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["8443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-b", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user