Skip to content

Commit

Permalink
Add Capirca Integration (#63)
Browse files Browse the repository at this point in the history
* Implement model to capirca capability

* Apply suggestions from code review

Co-authored-by: Justin Drew <[email protected]>

Co-authored-by: Justin Drew <[email protected]>
  • Loading branch information
itdependsnetworks and jdrew82 authored Jul 19, 2022
1 parent 93e67a5 commit 3e68de3
Show file tree
Hide file tree
Showing 45 changed files with 2,112 additions and 126 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Use the `default_status` plugin configuration setting to change the default valu
PLUGINS_CONFIG = {
"nautobot_firewall_models": {
"default_status": "active"
"allowed_status": ["active"], # default shown, `[]` allows all
"capirca_remark_pass": True,
"capirca_os_map": {
"cisco_ios": "cisco",
"arista_eos": "arista",
},
# "custom_capirca": "my.custom.func", # provides ability to overide capirca logic
}
}
```
Expand Down
74 changes: 74 additions & 0 deletions docs/capirca.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

### Capirca Integrations

The firewall model plugin provides the ability to integrate with Capirca for configuration generation. The authors have applied a very light opinion onto the translation from the firewall models to generate valid policy (.pol), network (.net), and service (.svc) files that are consumed by Capirca.

FW Model | Capirca
--------------------------- | -------
Name (as applicable) | Header - Filter Name
Zone (as applicable) | Header - to-zone/from-zone
Source Address / Group | Term - source-address
Destination Address / Group | Term - destination-address
Destination Service / Group | Term - destination-port, protocol
Action | Term - action
Logging | Term - logging
Address - IP | *.net
Address - Prefix | *.net
Address Group | *.net
Service - tcp/udp | *.svc
Service Group | *.svc

> Note: If this terminology is not familiar, please review the documentation at [Capirca](https://github.com/google/capirca).
Special Considerations
* Capirca does not allow special characters in a majority of the named objects, as such named objects are modified to the ouput used when processed via a modified (to allow for capital letters) [Django slugify](https://docs.djangoproject.com/en/4.0/ref/utils/#django.utils.text.slugify), this includes:
* Policy name, policy rule name, address, address group, zone, service, service group
* e.g. Policy called "Allow to Internet" will be called "Allow-to-Internet"
* Note: This **will not change** barring a major update from Capirca
* FQDN and IP Range are not supported by Capirca and will fail if attempting to use those features
* The zone is only used where Capirca supports it, at the time of this writing is only Palo Alto and Juniper SRX
* Zone based firewalls have headers on every rule
* Both Juniper SRX and Palo Alto support using the named zone "all" to represent all zones, but in all cases a zone must be set
* The "Filter Name" is a concatenation of the Policies applied to a given firewall
* Not all firewalls get a filter name, such as zone or direction based firewalls, which require a `chd_` custom field (more details below)
* An object (policy, policy rule, src-addr, dst-addr, etc.) is put into and out of use based on whether or not the status is `active` or as defined in your plugin configuration
* Anything other than active or defined in plugin setting `allowed_status` is ignored
* Removing the last active object in an source-address, destination-address, or service will fail the process to avoid your policy failing open
* The Platform slug must match the Capirca generator name
* You can optionally provide a mapping in the settings `capirca_os_map` to map from the current platform name, to the Capirca generator name
* The action of "remark" on a rule is not conidered, you can set the setting `capirca_remark_pass=False` if you want it to fail by default rather than silently skipping


In addition to the above, you can add to any header or term by creating specific custom fields on the `PolicyRule` data model. They must start with:
* `chd_` - Capirca Header Data - will be applied to the `header` for any given rule.
* `ctd_` - Capirca Term Data - will be applied to the `term` for any given rule.

The process is to create a custom field, such as `ctd_pan-application`, this will be applied to the PolicyRule as you describe. This can become problematic if you share the model for multiple firewall OSs. This can be conditionally applied via a custom field to the `Platform` model. This custom field **must** be named `capirca_allow` and be of type JSON and be a single list. For each OS defined by the platform, you can allow that custom field to populate. This allows you to use the same model, and not let the custom fields for one OS conflict with another OS.

```python
capirca_allow = ['ctd_pan-application', 'ctd_expiration']
```

> Note: This is pseudo-code and is technically the custom_field called `capirca_allow` that has the dat `['ctd_pan-application', 'ctd_expiration']` in this example.
As previously mentioned, there is only a small opinion that is applied from the translation between the model and Capirca. That being said, Capirca has an opinion on how rules and objects are deployed, and within this project there is no consideration for how that may not align with anyone's intention on how Capirca should work. All such considerations should be referred to the Capirca project. There is no intention to modify the output that Capirca creates **in any situation** within this plugin.

That being said, in an effort to provide flexibility, you can override the translation process. However, you would be responsible for that implementation. You can provide within your setting, a dotted path [import_string](https://docs.djangoproject.com/en/4.0/ref/utils/#django.utils.module_loading.import_string) to your own function. This is provided in the `custom_capirca` setting within your Plugin Configurations. The signature takes a `Device` object instance and must return a tuple of `(pol, svc, net, cfg)`, none of which are required to have data.

```python
self.pol, self.svc, self.net, self.cfg = import_string(PLUGIN_CFG["custom_capirca"])(self.device)
```

To Summarize what this integration provides and does not provide

Provides:
* Integrations with Capirca
* The ability to manage per platform Headers and Terms
* A Job that generated the configurations at the time you want
* The ability to override the opinionated Capirca solution

Does not Provide:
* An opinionated configuration management solution that matches anything other than Capirca-provided configurations
* The ability to push configurations directly and natively from Nautobot
* The immediate updating from data in a `Policy` or `PolicyRule` that gets reflected in the configuration, instead when the job is ran
* Any post processing of configuration or pre-validation of data (such as checking if object name starts with an integer)
2 changes: 1 addition & 1 deletion nautobot_firewall_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class NautobotFirewallModelsConfig(PluginConfig):
required_settings = []
min_version = "1.3.0"
max_version = "1.9999"
default_settings = {}
default_settings = {"capirca_remark_pass": True, "capirca_os_map": {}, "allowed_status": ["active"]}
caching_config = {"*": {"timeout": 0}}
docs_view_name = "plugins:nautobot_firewall_models:docs"

Expand Down
14 changes: 14 additions & 0 deletions nautobot_firewall_models/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,17 @@ class PolicyDeepSerializer(PolicySerializer):
"""Overload for create & update views."""

policy_rules = PolicyRuleM2MDeepNestedSerializer(many=True, required=False, source="policyrulem2m_set")


class CapircaPolicySerializer(TaggedObjectSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
"""CapircaPolicy Serializer."""

url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_firewall_models-api:capircapolicy-detail"
)

class Meta:
"""Meta attributes."""

model = models.CapircaPolicy
fields = "__all__"
9 changes: 5 additions & 4 deletions nautobot_firewall_models/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@


router = OrderedDefaultRouter()
router.register("ip-range", views.IPRangeViewSet)
router.register("fqdn", views.FQDNViewSet)
router.register("address-object", views.AddressObjectViewSet)
router.register("address-object-group", views.AddressObjectGroupViewSet)
router.register("capirca-policy", views.CapircaPolicyViewSet)
router.register("fqdn", views.FQDNViewSet)
router.register("ip-range", views.IPRangeViewSet)
router.register("policy-rule", views.PolicyRuleViewSet)
router.register("policy", views.PolicyViewSet)
router.register("service-object", views.ServiceObjectViewSet)
router.register("service-object-group", views.ServiceObjectGroupViewSet)
router.register("user-object", views.UserObjectViewSet)
router.register("user-object-group", views.UserObjectGroupViewSet)
router.register("zone", views.ZoneViewSet)
router.register("policy-rule", views.PolicyRuleViewSet)
router.register("policy", views.PolicyViewSet)

urlpatterns = router.urls
8 changes: 8 additions & 0 deletions nautobot_firewall_models/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,11 @@ def get_serializer_class(self):
if self.action == "retrieve" and is_truthy(self.request.GET.get("deep", False)):
self.serializer_class = serializers.PolicyDeepSerializer
return super().get_serializer_class()


class CapircaPolicyViewSet(ModelViewSet):
"""CapircaPolicy viewset."""

queryset = models.CapircaPolicy.objects.all()
serializer_class = serializers.CapircaPolicySerializer
filterset_class = filters.CapircaPolicyFilterSet
124 changes: 124 additions & 0 deletions nautobot_firewall_models/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Constants file."""
from django.conf import settings

# This is used to map the slug of the platform in the customers environment to the expected name that Capirca is looking for
CAPIRCA_OS_MAPPER = {}

PLUGIN_CFG = settings.PLUGINS_CONFIG.get("nautobot_firewall_models", {})

if PLUGIN_CFG.get("capirca_os_map"):
CAPIRCA_OS_MAPPER = PLUGIN_CFG["capirca_os_map"]

# This is used to determine which status slug names are valid
ALLOW_STATUS = ["active"]
if PLUGIN_CFG.get("allowed_status"):
ALLOW_STATUS = PLUGIN_CFG["allowed_status"]

# This is used to whitelist actions that align with Capirca
ACTION_MAP = {"allow": "accept", "deny": "deny", "drop": "reject"} # no next or reject-with-tcp-rst
# This is used to transpose string booleans to Capirca expectations
LOGGING_MAP = {"true": "true", "false": "disable"}

# This is used to provide hints (for type), and dotted string back (for lib) to Capirca
CAPIRCA_MAPPER = {
"arista": {
"lib": "capirca.lib.arista.Arista",
"type": "filter-name",
},
"aruba": {
"lib": "capirca.lib.aruba.Aruba",
"type": "filter-name",
},
"brocade": {
"lib": "capirca.lib.brocade.Brocade",
"type": "filter-name",
},
"cisco": {
"lib": "capirca.lib.cisco.Cisco",
"type": "filter-name",
},
"ciscoasa": {
"lib": "capirca.lib.ciscoasa.CiscoASA",
"type": "filter-name",
},
"cisconx": {
"lib": "capirca.lib.cisconx.ciscoNX",
"type": "filter-name",
},
"cloudarmor": {
"lib": "capirca.lib.cloudarmor.CloudArmor",
"type": "filter_type",
},
"gce": {
"lib": "capirca.lib.gce.GCE",
"type": "filter-name",
},
"gcp_hf": {
"lib": "capirca.lib.gcp.GCP",
"type": "filter-name",
},
"ipset": {
"lib": "capirca.lib.ipset.Ipset",
"type": "direction",
},
"iptables": {
"lib": "capirca.lib.iptables.Iptables",
"type": "direction",
},
"juniper": {
"lib": "capirca.lib.juniper.Juniper",
"type": "filter-name",
},
"juniperevo": {
"lib": "capirca.lib.juniperevo.JuniperEvo",
"type": "filter-name",
},
"junipermsmpc": {
"lib": "capirca.lib.junipermsmpc.JuniperMSMPC",
"type": "filter-name",
},
"srx": {
"lib": "capirca.lib.junipersrx.JuniperSRX",
"type": "zone",
},
"k8s": {
"lib": "capirca.lib.k8s.K8s",
"type": "direction",
},
"nftables": {
"lib": "capirca.lib.nftables.Nftables",
"type": "address_family",
},
"nsxv": {
"lib": "capirca.lib.nsxv.Nsxv",
"type": "filter-name",
},
"packetfilter": {
"lib": "capirca.lib.packetfilter.PacketFilter",
"type": "filter-name",
},
"paloalto": {
"lib": "capirca.lib.paloaltofw.PaloAltoFW",
"type": "zone",
},
"pcap": {
"lib": "capirca.lib.pcap.PcapFilter",
"type": "filter-name",
},
"speedway": {
"lib": "capirca.lib.speedway.Speedway",
"type": "direction",
},
"srxlo": {
"lib": "capirca.lib.srxlo.SRXlo",
"type": "filter-name",
},
"windows_advfirewall": {
"lib": "capirca.lib.windows_advfirewall.WindowsAdvFirewall",
"type": "direction",
},
"windows_ipsec": {
"lib": "capirca.lib.windows_ipsec.WindowsIPSec",
"type": "filter-name",
},
}
23 changes: 23 additions & 0 deletions nautobot_firewall_models/filters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Filtering for Firewall Model Plugin."""

import django_filters
from django.db.models import Q
from django_filters import CharFilter, FilterSet
from nautobot.dcim.models import Device
from nautobot.extras.filters import StatusModelFilterSetMixin, NautobotFilterSet
from nautobot.utilities.filters import TagFilter

Expand Down Expand Up @@ -155,3 +157,24 @@ class Meta:

model = models.Policy
fields = ["id", "name", "description", "policy_rules", "assigned_devices", "assigned_dynamic_groups"]


class CapircaPolicyFilterSet(NautobotFilterSet):
"""Filter for CapircaPolicy."""

device = django_filters.ModelMultipleChoiceFilter(
field_name="device__name",
queryset=Device.objects.all(),
to_field_name="name",
label="Device Name",
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label="Device ID",
)

class Meta:
"""Meta attributes for filter."""

model = models.CapircaPolicy
fields = ["id"]
Loading

0 comments on commit 3e68de3

Please sign in to comment.