def allowed_connections(self, from_peer, to_peer, is_ingress): """ Evaluate the set of connections this policy allows/denies/passes between two peers :param Peer.Peer from_peer: The source peer :param Peer.Peer to_peer: The target peer :param bool is_ingress: whether we evaluate ingress rules only or egress rules only :return: A PolicyConnections object containing sets of allowed/denied/pass connections :rtype: PolicyConnections """ # TODO: currently not handling egress, istio authorization policies have no egress rules if not is_ingress: return PolicyConnections(False, ConnectionSet(True)) captured = to_peer in self.selected_peers if not captured: return PolicyConnections(False) allowed_conns = ConnectionSet() denied_conns = ConnectionSet() collected_conns = allowed_conns if self.action == IstioNetworkPolicy.ActionType.Allow else denied_conns for rule in self.ingress_rules: if from_peer in rule.peer_set: collected_conns |= rule.connections return PolicyConnections(True, allowed_conns, denied_conns)
def parse_ingress_egress_rule(self, rule, peer_array_key): """ Parse a single ingres/egress rule, producing a K8sPolicyRule :param dict rule: The rule to parse :param str peer_array_key: The key which defined the peer set ('from' for ingress, 'to' for egress) :return: A K8sPolicyRule with the proper PeerSet and ConnectionSet :rtype: K8sPolicyRule """ self.check_fields_validity(rule, 'ingress/egress rule', { peer_array_key: [0, list], 'ports': [0, list] }) peer_array = rule.get(peer_array_key, []) if peer_array: res_pods = Peer.PeerSet() for peer in peer_array: res_pods |= self.parse_peer(peer) else: res_pods = self.peer_container.get_all_peers_group(True) ports_array = rule.get('ports', []) if ports_array: res_ports = ConnectionSet() for port in ports_array: res_ports |= self.parse_port(port) else: res_ports = ConnectionSet(True) if not res_pods: self.warning('Rule selects no pods', rule) return K8sPolicyRule(res_pods, res_ports)
def allowed_connections(self, from_peer, to_peer): """ This is the core of the whole application - computes the set of allowed connections from one peer to another. In our connectivity model, this function computes the labels for the edges in our directed graph. :param Peer.Peer from_peer: The source peer :param Peer.Peer to_peer: The target peer :return: a 4-tuple with: - allowed_conns: all allowed connections (captured/non-captured) - captured_flag: flag to indicate if any of the policies captured one of the peers (src/dst) - allowed_captured_conns: allowed captured connections (can be used only if the captured flag is True) - denied_conns: connections denied by the policies (captured) :rtype: ConnectionSet, bool, ConnectionSet, ConnectionSet """ if isinstance(to_peer, Peer.IpBlock): ingress_conns = PolicyConnections( captured=False, all_allowed_conns=ConnectionSet(True)) else: ingress_conns = self._allowed_xgress_conns(from_peer, to_peer, True) if isinstance(from_peer, Peer.IpBlock): egress_conns = PolicyConnections( captured=False, all_allowed_conns=ConnectionSet(True)) else: egress_conns = self._allowed_xgress_conns(from_peer, to_peer, False) captured_flag = ingress_conns.captured or egress_conns.captured denied_conns = ingress_conns.denied_conns | egress_conns.denied_conns allowed_conns = ingress_conns.all_allowed_conns & egress_conns.all_allowed_conns # captured connections are where at least one if ingress / egress is captured allowed_captured_conns = (ingress_conns.allowed_conns & egress_conns.all_allowed_conns) | \ (egress_conns.allowed_conns & ingress_conns.all_allowed_conns) return allowed_conns, captured_flag, allowed_captured_conns, denied_conns
def allowed_connections(self, from_peer, to_peer, is_ingress): """ Evaluate the set of connections this ingress resource allows between two peers :param Peer.Peer from_peer: The source peer :param Peer.Peer to_peer: The target peer :param bool is_ingress: For compatibility with other policies. Will return the set of allowed connections only for is_ingress being False. :return: A PolicyConnections object containing sets of allowed connections :rtype: PolicyConnections """ captured = from_peer in self.selected_peers if not captured: return PolicyConnections(False) if is_ingress: return PolicyConnections(False) allowed_conns = ConnectionSet() denied_conns = ConnectionSet() conns = allowed_conns if self.action == IngressPolicy.ActionType.Allow else denied_conns for rule in self.egress_rules: if to_peer in rule.peer_set: assert not rule.connections.has_named_ports() conns |= rule.connections return PolicyConnections(True, allowed_conns, denied_conns)
def parse_ingress_rule(self, rule): """ Parse a single ingress rule, producing a IstioPolicyRule. :param dict rule: The dict with the rule fields :return: A IstioPolicyRule with the proper PeerSet and ConnectionSet :rtype: IstioPolicyRule """ if rule is None: self.syntax_error('Authorization policy rule cannot be null. ') allowed_elements = { 'from': [0, list], 'to': [0, list], 'when': [0, list] } self.check_fields_validity(rule, 'authorization policy rule', allowed_elements) for key_elem in allowed_elements: self.validate_existing_key_is_not_null(rule, key_elem) # collect source peers into res_peers from_array = self.get_key_array_and_validate_not_empty(rule, 'from') if from_array is not None: res_peers = PeerSet() for source_dict in from_array: res_peers |= self.parse_source(source_dict) else: # no 'from' in the rule => all source peers allowed res_peers = self.peer_container.get_all_peers_group(True) to_array = self.get_key_array_and_validate_not_empty(rule, 'to') # currently parsing only ports # TODO: extend operations parsing to include other attributes if to_array is not None: connections = ConnectionSet() for operation_dict in to_array: connections |= self.parse_operation(operation_dict) else: # no 'to' in the rule => all connections allowed connections = ConnectionSet(True) # condition possible result value: source-ip (from) , source-namespace (from) [Peerset], destination.port (to) [ConnectionSet] # should update either res_pods or connections according to the condition condition_array = rule.get( 'when') # this array can be empty (unlike 'to' and 'from') # the combined condition ("AND" of all conditions) should be applied if condition_array is not None: for condition in condition_array: condition_res = self.parse_condition(condition) if isinstance(condition_res, PeerSet): res_peers &= condition_res elif isinstance(condition_res, ConnectionSet): connections &= condition_res if not res_peers: self.warning('Rule selects no pods', rule) return IstioPolicyRule(res_peers, connections)
class PolicyConnections: """ A class to contain the effect of applying policies to a pair of peers """ captured: bool # Whether policy(ies) selectors captured relevant peers (can have empty allowed-conns with captured==True) allowed_conns: ConnectionSet = ConnectionSet( ) # Connections allowed (and captured) by the policy(ies) denied_conns: ConnectionSet = ConnectionSet( ) # Connections denied by the policy(ies) pass_conns: ConnectionSet = ConnectionSet( ) # Connections specified as PASS by the policy(ies) all_allowed_conns: ConnectionSet = ConnectionSet( ) # all (captured+ non-captured) Connections allowed by the policy(ies)
def verify_named_ports(self, rule, rule_pods, rule_conns): """ Check the validity of named ports in a given rule: whether a relevant pod refers to the named port and whether the protocol defined in the policy matches the protocol defined by the Pod. Issue warnings as required. :param dict rule: The unparsed rule (for reference in warnings) :param Peer.PeerSet rule_pods: The set of Pods in which the named ports should be defined :param ConnectionSet rule_conns: The rule-specified connections, possibly containing named ports :return: None """ if not rule_conns.has_named_ports(): return named_ports = rule_conns.get_named_ports() for protocol, rule_ports in named_ports: for port in rule_ports: port_used = False for pod in rule_pods: pod_named_port = pod.named_ports.get(port) if pod_named_port: port_used = True if ConnectionSet.protocol_name_to_number( pod_named_port[1]) != protocol: self.warning( f'Protocol mismatch for named port {port} (vs. Pod {pod.full_name()})', rule['ports']) if not port_used: self.warning( f'Named port {port} is not defined in any selected pod', rule['ports'])
def allowed_connections(self, from_peer, to_peer, is_ingress): """ Evaluate the set of connections this policy allows between two peers (either the allowed ingress into to_peer or the allowed egress from from_peer). :param Peer.Peer from_peer: The source peer :param Peer.Peer to_peer: The target peer :param bool is_ingress: whether we evaluate ingress rules only or egress rules only :return: A PolicyConnections object containing sets of allowed connections :rtype: PolicyConnections """ captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \ not is_ingress and self.affects_egress and from_peer in self.selected_peers if not captured: return PolicyConnections(False) allowed_conns = ConnectionSet() rules = self.ingress_rules if is_ingress else self.egress_rules other_peer = from_peer if is_ingress else to_peer for rule in rules: if other_peer in rule.peer_set: rule_conns = rule.port_set.copy() # we need a copy because convert_named_ports is destructive rule_conns.convert_named_ports(to_peer.get_named_ports()) allowed_conns |= rule_conns return PolicyConnections(True, allowed_conns)
def _get_connection_set_from_properties(dest_ports, method_set=MethodSet(True), paths_dfa=None, hosts_dfa=None): """ get ConnectionSet with TCP allowed connections, corresponding to input properties cube :param PortSet dest_ports: ports set for dset_ports dimension :param MethodSet method_set: methods set for methods dimension :param MinDFA paths_dfa: MinDFA obj for paths dimension :param MinDFA hosts_dfa: MinDFA obj for hosts dimension :return: ConnectionSet with TCP allowed connections , corresponding to input properties cube """ tcp_properties = TcpLikeProperties(source_ports=PortSet(True), dest_ports=dest_ports, methods=method_set, paths=paths_dfa, hosts=hosts_dfa) res = ConnectionSet() res.add_connections('TCP', tcp_properties) return res
def allowed_connections(self, from_peer, to_peer, is_ingress): """ Evaluate the set of connections this policy allows/denies/passes between two peers :param Peer.Peer from_peer: The source peer :param Peer.Peer to_peer: The target peer :param bool is_ingress: whether we evaluate ingress rules only or egress rules only :return: A PolicyConnections object containing sets of allowed/denied/pass connections :rtype: PolicyConnections """ captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \ not is_ingress and self.affects_egress and from_peer in self.selected_peers if not captured: return PolicyConnections(False) allowed_conns = ConnectionSet() denied_conns = ConnectionSet() pass_conns = ConnectionSet() rules = self.ingress_rules if is_ingress else self.egress_rules for rule in rules: if from_peer in rule.src_peers and to_peer in rule.dst_peers: rule_conns = rule.connections.copy( ) # we need a copy because convert_named_ports is destructive rule_conns.convert_named_ports(to_peer.get_named_ports()) if rule.action == CalicoPolicyRule.ActionType.Allow: rule_conns -= denied_conns rule_conns -= pass_conns allowed_conns |= rule_conns elif rule.action == CalicoPolicyRule.ActionType.Deny: rule_conns -= allowed_conns rule_conns -= pass_conns denied_conns |= rule_conns elif rule.action == CalicoPolicyRule.ActionType.Pass: rule_conns -= allowed_conns rule_conns -= denied_conns pass_conns |= rule_conns else: pass # Nothing to do for Log action - does not affect connectivity return PolicyConnections(True, allowed_conns, denied_conns, pass_conns)
def _make_deny_rules(self, allowed_conns): """ Make deny rules from the given connections :param TcpLikeProperties allowed_conns: the given allowed connections :return: the list of deny IngressPolicyRules """ all_peers_and_ip_blocks = self.peer_container.peer_set.copy() all_peers_and_ip_blocks.add( IpBlock.get_all_ips_block()) # add IpBlock of all IPs all_conns = self._make_tcp_like_properties(PortSet(True), all_peers_and_ip_blocks) denied_conns = all_conns - allowed_conns res = self._make_rules_from_conns(denied_conns) # Add deny rule for all protocols but TCP , relevant for all peers and ip blocks non_tcp_conns = ConnectionSet.get_non_tcp_connections() res.append(IngressPolicyRule(all_peers_and_ip_blocks, non_tcp_conns)) return res
def _parse_protocol(self, protocol, rule): """ Parse the protocol/notProtocol field in a rule :param protocol: The protocol (a string or an int) :param dict rule: The parsed rule object (for context) :return: The protocol number :rtype: int """ if not protocol: return None if isinstance(protocol, int): if protocol < 1 or protocol > 255: self.syntax_error( 'protocol must be a string or an integer in the range 1-255', rule) return protocol if protocol not in ['TCP', 'UDP', 'ICMP', 'ICMPv6', 'SCTP', 'UDPLite']: self.syntax_error('invalid protocol name: ' + protocol, rule) return ConnectionSet.protocol_name_to_number(protocol)
def __init__(self, cluster_info, allowed_labels, output_config): """ create an object of MinimizeCsFwRules :param cluster_info: an object of type ClusterInfo, with relevant cluster topology info :param allowed_labels: a set of label keys (set[str]) that appear in one of the policy yaml files. using this set to determine which label can be used for grouping pods in fw-rules computation :param output_config: an OutputConfiguration object """ self.cluster_info = cluster_info self.allowed_labels = allowed_labels self.output_config = output_config self.peer_pairs = set() self.connections = ConnectionSet() self.peer_pairs_in_containing_connections = set() self.ns_pairs = set() self.peer_pairs_with_partial_ns_expr = set() self.peer_pairs_without_ns_expr = set() self.covered_peer_pairs_union = set() self.results_info_per_option = dict() self.minimized_fw_rules = [ ] # holds the computation result of minimized fw-rules
def _allowed_xgress_conns(self, from_peer, to_peer, is_ingress): """ get allowed and denied ingress/egress connections between from_peer and to_peer, considering all config's policies (and defaults) :param from_peer: the source peer :param to_peer: the dest peer :param is_ingress: flag to indicate if should return ingress connections or egress connections :return: PolicyConnections object with: - captured: flag to indicate if any of the policies captured one of the peers (src/dst) - allowed_conns: allowed captured connections (can be used only if the captured flag is True) - denied_conns: connections denied by the policies (captured) - pass_conns: irrelevant , always empty - all_allowed_conns: all allowed connections (captured/non-captured) :rtype: PolicyConnections """ allowed_conns = ConnectionSet() denied_conns = ConnectionSet() pass_conns = ConnectionSet() policy_captured = False has_allow_policies_for_target = False for policy in self.sorted_policies: policy_conns = policy.allowed_connections(from_peer, to_peer, is_ingress) assert isinstance(policy_conns, PolicyConnections) if policy_conns.captured: policy_captured = True if isinstance( policy, IstioNetworkPolicy ) and policy.action == IstioNetworkPolicy.ActionType.Allow: has_allow_policies_for_target = True policy_conns.denied_conns -= allowed_conns policy_conns.denied_conns -= pass_conns denied_conns |= policy_conns.denied_conns policy_conns.allowed_conns -= denied_conns policy_conns.allowed_conns -= pass_conns allowed_conns |= policy_conns.allowed_conns policy_conns.pass_conns -= denied_conns policy_conns.pass_conns -= allowed_conns pass_conns |= policy_conns.pass_conns if self.type == NetworkConfig.ConfigType.Istio: # for istio initialize non-captured conns with non-TCP connections allowed_non_captured_conns = ConnectionSet.get_non_tcp_connections( ) if not is_ingress: allowed_non_captured_conns = ConnectionSet( True) # egress currently always allowed and not captured elif not has_allow_policies_for_target: # add connections allowed by default that are not captured allowed_non_captured_conns |= (ConnectionSet(True) - denied_conns) return PolicyConnections(has_allow_policies_for_target, allowed_conns, denied_conns, all_allowed_conns=allowed_conns | allowed_non_captured_conns) allowed_non_captured_conns = ConnectionSet() if not policy_captured: if self.type in [ NetworkConfig.ConfigType.K8s, NetworkConfig.ConfigType.Unknown ]: allowed_non_captured_conns = ConnectionSet( True ) # default Allow-all ingress in k8s or in case of no policy else: if self.profiles: allowed_non_captured_conns = self._get_profile_conns( from_peer, to_peer, is_ingress).allowed_conns elif pass_conns: allowed_conns |= pass_conns & self._get_profile_conns( from_peer, to_peer, is_ingress).allowed_conns return PolicyConnections(policy_captured, allowed_conns, denied_conns, all_allowed_conns=allowed_conns | allowed_non_captured_conns)
def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): """ Parse a single ingres/egress rule, producing a CalicoPolicyRule :param dict rule: The rule element to parse :param bool is_ingress: Whether this is an ingress rule :param PeerSet policy_selected_eps: The endpoints the policy captured :param bool is_profile: Whether the parsed policy is a Profile object :return: A CalicoPolicyRule with the proper PeerSets, ConnectionSets and Action :rtype: CalicoPolicyRule """ allowed_keys = { 'action': 1, 'protocol': 0, 'notProtocol': 0, 'icmp': 0, 'notICMP': 0, 'ipVersion': 0, 'source': 0, 'destination': 0, 'http': 2 } self.check_fields_validity(rule, 'ingress/egress rule', allowed_keys) action = CalicoPolicyRule.action_str_to_action_type(rule['action']) if action is None: self.syntax_error('Invalid rule action: ' + rule['action'], rule) if is_profile and action == CalicoPolicyRule.ActionType.Pass: self.warning('Pass actions in Profile rules will be ignored', rule) protocol = self._parse_protocol(rule.get('protocol'), rule) protocol_supports_ports = ConnectionSet.protocol_supports_ports( protocol) not_protocol = self._parse_protocol(rule.get('notProtocol'), rule) src_entity_rule = rule.get('source') if src_entity_rule: src_res_pods, src_res_ports = self._parse_entity_rule( src_entity_rule, protocol_supports_ports) else: src_res_pods = self.peer_container.get_all_peers_group(True) src_res_ports = PortSet(True) dst_entity_rule = rule.get('destination') if dst_entity_rule: dst_res_pods, dst_res_ports = self._parse_entity_rule( dst_entity_rule, protocol_supports_ports) else: dst_res_pods = self.peer_container.get_all_peers_group(True) dst_res_ports = PortSet(True) if is_ingress: # FIXME: We do not handle well the case where dst_res_pods or src_res_pods contain ipBlocks dst_res_pods &= policy_selected_eps else: src_res_pods &= policy_selected_eps connections = ConnectionSet() if protocol is not None: if not_protocol is not None: if protocol == not_protocol: self.warning( 'Protocol and notProtocol are conflicting, no traffic will be matched', rule) else: self.warning('notProtocol field has no effect', rule) else: if protocol_supports_ports: connections.add_connections( protocol, TcpLikeProperties(src_res_ports, dst_res_ports)) elif ConnectionSet.protocol_is_icmp(protocol): connections.add_connections( protocol, self._parse_icmp(rule.get('icmp'), rule.get('notICMP'))) else: connections.add_connections(protocol, True) elif not_protocol is not None: connections.add_all_connections() connections.remove_protocol(not_protocol) else: connections.allow_all = True self._verify_named_ports(rule, dst_res_pods, connections) if not src_res_pods and policy_selected_eps and (is_ingress or not is_profile): self.warning('Rule selects no source endpoints', rule) if not dst_res_pods and policy_selected_eps and (not is_ingress or not is_profile): self.warning('Rule selects no destination endpoints', rule) return CalicoPolicyRule(src_res_pods, dst_res_pods, connections, action)
def parse_port(self, port): """ Parse an element of the "ports" phrase of a policy rule :param dict port: The element to parse :return: A ConnectionSet representing the allowed connections by this element (protocols X port numbers) :rtype: ConnectionSet """ self.check_fields_validity(port, 'NetworkPolicyPort', { 'port': 0, 'protocol': [0, str], 'endPort': [0, int] }, {'protocol': ['TCP', 'UDP', 'SCTP']}) port_id = port.get('port') protocol = port.get('protocol') end_port_num = port.get('endPort') if not protocol: protocol = 'TCP' res = ConnectionSet() dest_port_set = PortSet(port_id is None) if port_id is not None and end_port_num is not None: if isinstance(port_id, str): self.syntax_error( 'endPort cannot be defined if the port field is defined ' 'as a named (string) port', port) if not isinstance(port_id, int): self.syntax_error( 'type of port is not numerical in NetworkPolicyPort', port) self.validate_value_in_domain(port_id, 'dst_ports', port, 'Port number') self.validate_value_in_domain(end_port_num, 'dst_ports', port, 'endPort number') if port_id > end_port_num: self.syntax_error('endPort must be equal or greater than port', port) dest_port_set.add_port_range(port_id, end_port_num) elif port_id is not None: if not isinstance(port_id, str) and not isinstance(port_id, int): self.syntax_error( 'type of port is not numerical or named (string) in NetworkPolicyPort', port) if isinstance(port_id, int): self.validate_value_in_domain(port_id, 'dst_ports', port, 'Port number') if isinstance(port_id, str): if len(port_id) > 15: self.syntax_error( 'port name must be no more than 15 characters', port) if re.fullmatch(r"[a-z\d]([-a-z\d]*[a-z\d])?", port_id) is None: self.syntax_error( 'port name should contain only lowercase alphanumeric characters or "-", ' 'and start and end with alphanumeric characters', port) dest_port_set.add_port(port_id) elif end_port_num: self.syntax_error( 'endPort cannot be defined if the port field is not defined ', port) res.add_connections( protocol, TcpLikeProperties( PortSet(True), dest_port_set)) # K8s doesn't reason about src ports return res