Creating Access Policy
You can now create access policy rules which validate the codelab network configuration.
Invariant will evaluate all access policy rules found in the invariant/policies/
directory relative to the codelab directory.
Rules must be placed in an access-policy
list. Each item in the list is called an "access policy" and must specifiy subnets to use as the source (egress network) or the destination (ingress network). In advanced cases (e.g. duplicate IP addresses), specific interfaces can be used instead of subnets. All rules in the same policy must match the type of the parent policy, either ingress or egress. For example, an ingress-deny
rule may only be placed in a policy where ingress-network
is defined.
Deny rules
Create a new access policy which asserts that traffic from subnet VLAN30 cannot reach the DATACENTER subnet over SSH.
cat >> ./invariant/policies/datacenter.yaml << EOF
access-policy:
- name: datacenter-security-policy
comment: Access to the data center is controlled by this policy
owner: [email protected]
ingress-network: DATACENTER
rules:
- type: ingress-deny
comment: VLAN30 must not be able to reach DATACENTER through SSH
source-address: VLAN30
destination-port: SSH
protocol: tcp
EOF
Re-analyze the network now using invariant run
to evaluate your new rule.
A new "Access Policy" section will appear in the output. Your new rule should pass and you should see one row in the policy_ok
report.
$ invariant run
Uploading snapshot...
Processing... (6c334806-126c-4b63-bafe-495b39fdf995)
Analysis complete.
╭───────────────────────┬────────╮
│ Network Information │ Rows │
├───────────────────────┼────────┤
│ nodes │ 12 │
│ interfaces │ 94 │
│ named_structures │ 66 │
│ defined_structures │ 351 │
│ referenced_structures │ 350 │
│ unused_structures │ 5 │
│ vlan_properties │ 12 │
│ hsrp_properties │ 0 │
│ mlag_properties │ 0 │
│ ip_owners │ 51 │
│ undefined_references │ 0 │
│ vrrp_properties │ 8 │
╰───────────────────────┴────────╯
╭───────────────┬────────╮
│ Topology │ Rows │
├───────────────┼────────┤
│ edges │ 42 │
│ layer_1_edges │ 0 │
│ layer_3_edges │ 42 │
│ network_map │ 1 │
╰───────────────┴────────╯
╭────────────────────────────┬────────╮
│ Routing │ Rows │
├────────────────────────────┼────────┤
│ routes │ 270 │
│ bgp_process_config │ 4 │
│ bgp_peer_config │ 8 │
│ bgp_session_compatibility │ 8 │
│ bgp_session_status │ 8 │
│ bgp_edges │ 8 │
│ bgp_ribs │ 8 │
│ ospf_process_config │ 6 │
│ ospf_interface_config │ 36 │
│ ospf_area_config │ 6 │
│ ospf_session_compatibility │ 20 │
╰────────────────────────────┴────────╯
╭───────────────────┬────────╮
│ Setup │ Rows │
├───────────────────┼────────┤
│ unconnected_nodes │ 0 │
│ ignored_lines │ 32 │
│ file_parse_status │ 9 │
│ parse_warnings │ 59 │
│ errors │ 0 │
╰─── ────────────────┴────────╯
╭────────────────────────┬────────╮
│ Inconsistent Traffic │ Rows │
├────────────────────────┼────────┤
│ subnet_multipath │ 0 │
│ loopback_multipath │ 0 │
╰────────────────────────┴────────╯
╭──────────┬────────╮
│ Probes │ Rows │
├──────────┼────────┤
│ probes │ 3 │
╰──────────┴────────╯
╭──────────────────────────────────────┬────────╮
│ Access Policy │ Rows │
├──────────────────────────────────────┼────────┤
│ critical_flows_ok │ 0 │
│ critical_flows_violations │ 0 │
│ critical_flows_violations_unenforced │ 0 │
│ critical_flows_skipped │ 0 │
│ critical_flows_details │ 0 │
│ critical_flows_logs │ 0 │
│ policy_ok │ 1 │
│ policy_violations │ 0 │
│ policy_violations_unenforced │ 0 │
│ policy_skipped │ 0 │
│ policy_details │ 3 │
│ policy_logs │ 1 │
╰──────────────────────────────────────┴────────╯
Run 'invariant show <file> --snapshot 6c334806-126c-4b63-bafe-495b39fdf995' to examine any file,
or run 'export INVARIANT_SNAPSHOT=6c334806-126c-4b63-bafe-495b39fdf995' to skip the --snapshot argument.
Notice that the policy_detail
report has three rows even though you only added a single rule. The Invariant rule engine may split up a rule into smaller parts; each part produces a row in this report.
Open the policy_ok
report. You can confirm your new rule is indeed passing. You may find this report easier to read using the --json
switch as shown.
$ invariant show policy_ok --json
[
{
"index": 0,
"ok": true,
"skipped": false,
"policy": {
"comment": "Access to the data center is controlled by this policy",
"egress-network": null,
"enforce": null,
"ingress-network": {
"list": [
"DATACENTER"
],
"object": null
},
"name": "datacenter-security-policy",
"owner": "[email protected]"
},
"rule": {
"comment": "VLAN30 must not be able to reach DATACENTER through SSH",
"destination-port": [
"SSH"
],
"protocol": [
"tcp"
],
"source-address": [
"VLAN30"
],
"type": "ingress-deny"
},
"errors": [],
"rule_type": "ingress-deny",
"violations": 0,
"checks": 6,
"enforce": true
}
]
Now add a rule to the policy which asserts that VLAN40 access to the Data Center is denied entirely.
$ cat >> ./invariant/policies/datacenter.yaml << EOF
access-policy:
- name: datacenter-security-policy
comment: Access to the datacenter is controlled by this policy
owner: [email protected]
ingress-network: DATACENTER
rules:
- type: ingress-deny
comment: VLAN30 must not be able to reach DATACENTER through SSH
source-address: VLAN30
destination-port: SSH
protocol: tcp
- type: ingress-deny
comment: VLAN40 must not be able to reach DATACENTER at all.
source-address: VLAN40
EOF
$ invariant run
Re-analyze the network again using invariant run
to evaluate your new rule.
Unlike the first rule, this rule should fail. The policy_violations
report should now have one row.
Examine policy_details
to determine why the rule failed. Example traceroutes were created showing how traffic can violate the rule.
invariant show policy_details --json
Locate an example traceroute for the failing rule by searching for where ok
is false.
...
{
"index": 2,
"ok": false,
"policy": {
"comment": null,
"egress-network": null,
"enforce": null,
"ingress-network": {
"list": [
"DATACENTER"
],
"object": null
},
"name": "datacenter-security-policy",
"owner": null
},
"rule": {
"comment": "VLAN40 must not be able to reach DATACENTER at all.",
"deny-all-except": null,
"destination-port": null,
"protocol": null,
"source-address": [
"VLAN10"
],
"type": "ingress-deny",
"within": null
},
"resolved_as": [
{
"destination_address": [
"172.16.50.0/24"
],
"destination_exclude": null,
"destination_node": null,
"destination_port": null,
"enter_interface": null,
"protocol": null,
"source_address": [
"192.168.10.0/24"
],
"source_exclude": null,
"source_interface": null,
"source_port": null
}
],
"errors": [],
"rule_type": "ingress-deny",
"direction": "INGRESS_DENY",
"start": "@enter(dist-2[Vlan10])",
...
The details report can be overwhelming, but you can break it down by field.
The policy
, rule
, and ok
sections overlap with the policy_ok
and policy_violations
reports. The resolved_as
section shows you what IP addresses were actually checked; if those are not resolved to the IP addressses you expected, you may have an issue with your network definitions.
You can find the node and interface where the flow originated in the start
section. The content of start
should look something like @enter(dist-2[Vlan40])
. This expression tells you that the virtual packet entered node dist-2
on interface Vlan40
.
The flow
section gives you information about the example packet: TCP flags, fragment offset, and packet length are all shown.
The most important section is traces
, containing virtual traceroutes. Unlike a live traceroute, these virtual traceroutes contain detailed information about the packet's internal progress within each node. This includes evaluating it against any firewall filters, establishing a state, and forwarding the packet itself. You can observe if and how the packet is transformed.
The traces section should contain the following:
{
"action": "PERMITTED",
"detail": {
"arpIp": null,
"filter": "vlan40-in",
"filterType": "INGRESS_FILTER",
"flow": {
"dscp": 0,
"dstIp": "172.16.50.0",
"dstPort": 80,
"ecn": 0,
"fragmentOffset": 0,
"icmpCode": null,
"icmpVar": null,
"ingressInterface": "Vlan40",
"ingressNode": "dist-2",
"ingressVrf": null,
"ipProtocol": "TCP",
"packetLength": 512,
"srcIp": "192.168.40.253",
"srcPort": 49152,
"tcpFlagsAck": 0,
"tcpFlagsCwr": 0,
"tcpFlagsEce": 0,
"tcpFlagsFin": 0,
"tcpFlagsPsh": 0,
"tcpFlagsRst": 0,
"tcpFlagsSyn": 1,
"tcpFlagsUrg": 0
},
"forwardingDetail": null,
"inputInterface": "Vlan40",
"inputVrf": null,
"matchCriteria": null,
"outputInterface": null,
"resolvedNexthopIp": null,
"routes": null,
"sessionAction": null,
"sessionScope": null,
"transformation": null,
"transformedFlow": null
}
},
{
"action": "PERMITTED",
"detail": {
"arpIp": null,
"filter": "internal-to-external",
"filterType": "POST_TRANSFORMATION_INGRESS_FILTER",
"flow": {
"dscp": 0,
"dstIp": "172.16.50.0",
"dstPort": 80,
"ecn": 0,
"fragmentOffset": 0,
"icmpCode": null,
"icmpVar": null,
"ingressInterface": "Vlan40",
"ingressNode": "dist-2",
"ingressVrf": null,
"ipProtocol": "TCP",
"packetLength": 512,
"srcIp": "192.168.40.253",
"srcPort": 49152,
"tcpFlagsAck": 0,
"tcpFlagsCwr": 0,
"tcpFlagsEce": 0,
"tcpFlagsFin": 0,
"tcpFlagsPsh": 0,
"tcpFlagsRst": 0,
"tcpFlagsSyn": 1,
"tcpFlagsUrg": 0
},
"forwardingDetail": null,
"inputInterface": "INSIDE1",
"inputVrf": null,
"matchCriteria": null,
"outputInterface": null,
"resolvedNexthopIp": null,
"routes": null,
"sessionAction": null,
"sessionScope": null,
"transformation": null,
"transformedFlow": null
}
},
Both of these firewalls are letting the packets through. Fixing either firewall would close off this access and resolve the violation.
Close off this traffic with the following steps:
- Remove line 119 from
config/asa.cfg
- Remove line 216 from
config/dist-1.cfg
- Remove line 216 from
config/dist-2.cfg
Re-analyze the network again using invariant run
after removing these lines to confirm that all violations are resolved. All rules should now pass.
Deny-others rules
Deny-others rules express exactly which traffic should be allowed in or out of a subnet within some constraints. A simple deny rule specifies what is not permitted, but a deny-others rule specifies a scope of traffic to deny and then punches holes for exempt traffic.
To see this in action, create a new policy that covers VLAN40. In this policy, you will blanket deny all internal network access to VLAN40 over TCP, then carve out an exemption for traffic originating from a single host (ALICE_DESKTOP). Create the policy with the following command:
cat >> ./invariant/policies/sensitive.yaml << EOF
access-policy:
- name: sensitive-vlan-policy
comment: Policy for the sensitive vlan.
ingress-network:
destination-address: VLAN40
destination-exclude: VLAN40_IF
rules:
- type: ingress-deny-others
comment: Limit ingress to only SSH from Alice's desktop
within:
- protocol: tcp
source-address: RFC1918
deny-all-except:
flows:
- comment: Internal SSH access
source-address: ALICE_DESKTOP
destination-port: SSH
protocol: tcp
EOF
Re-analyze the network again using invariant run
. Your new rule should pass. The existing network configuration permits access from Alice's desktop to VLAN40.
Be aware that deny-others rules do not test whether the exempted traffic is actually deliverable. The ‘deny-all-except’ section does not assert that the exempted flows are deliverable. It asserts that flows outside the exempted traffic are never deliverable.
To actually assert that a key traffic flow is deliverable, see critical flow rules in the next section.
You can confirm that your deny-others rule does not test whether the exempted traffic is actually deliverable by making a change that would reject traffic from ALICE_DESKTOP to VLAN40. If you delete line 64 from config/dist-1.cfg
and config/dist-2.cfg
and then re-run invariant run
you should observe that there are no violations. However, if you restore the deleted line, but then modify it to allow traffic from some other IP instead of 192.168.10.98
(which is ALICE_DESKTOP), Invariant should flag this unpermitted access as a violation.
Critical flow rules
To assert that a key traffic flow must be allowed through the network, use a critical flow rule. Critical flow rules will fail if a change to the firewall or routing partially or completely prevents delivery of the target flow.
Create a policy which asserts that VLAN10 can connect to the data center.
cat >> ./invariant/policies/sensitive.yaml << EOF
access-policy:
- name: vlan10-security-policy
comment: Access exiting VLAN10 is controlled by this policy
owner: [email protected]
egress-network: VLAN10
rules:
- type: egress-critical-flow
comment: VLAN10 must be able to reach the data center.
destination-address: DATACENTER
destination-port: HTTPS
protocol: tcp
EOF
Re-analyzing the network using invariant run
will show this test passes. One row should appear in the critical_flows_ok
report.
Reports that start with critical_flows_
will have the same schema as their policy_
counterparts. One key difference: a passing critical flow rule will always have at least one virtual traceroute in the critical_flows_details
report, while a deny or deny-others rule will only have a virtual traceroute when it fails.
A failing critical flow rule usually has one or more virtual traceroutes showing where traffic was terminated. A virtual traceroute might not be generated if the originating host has no route for the packet.
Next steps
Now that we are at the end, try adding more policies and rules to the codelab and try modifying the configs to introduce violations. You may also want to try adding custom probes. Take time to experiment and learn the ins and outs of everything Invariant offers.