Monday, June 8, 2020

Coding and Automation Basics - Part 2

Previous sections:

Part 1

Introduction

In the comments of first post, we got some examples of jobs that people have automated like:

  • upgrading code on hundreds of devices

  • pushing config to dozens of routers at once

  • automatically failing over a firewall if it hit a memory usage threshold

All of these examples have a single thread in common; they took a tedious, easily repeatable problem, and made a computer do it instead. However, in order to do that, they first had to break down the problem in a way that was easy for computers to handle, that is, they had to think about it programmatically.

The purpose of this post is to act as an introduction to programmatic thinking.

Starting with the Basics

As far as we're concerned, there are three basic steps in an automation workflow: Inputs, Processing, and Outputs.

  • Inputs refer to the information that you (or another system) provides that goes into the process. For example, let's say you want to make a new firewall rule. The inputs would be the source/destination addresses and whatever other info the firewall requires.

  • Processing refers to what the computer will do with this information. For example, will it write all of your inputs to a file? Will it interface directly with the firewall to apply your rule?

  • Outputs refer to the results of the processing. You gave the computer an input, it did something with the input, and now you have a result. Often times the output of one process turns into an input of another.

This isn't a programming specific concept by the way. The same idea applies even if you work with a CLI or a GUI. For example, let's say you wanted to create a rule to block traffic.

  1. First, you use CLI commands to pass relevant info to the firewall in a format it understands (input)

  2. The firewall's underlying programming runs some magic and applies the information you gave it (processing)

  3. the rule is now in effect, and you can see that traffic is now being blocked . Alternatively, maybe something went wrong and you get an error. (output)

They key takeaway here is that the fundamentals do not change with automation. In many cases, all you're really doing is presenting inputs to devices more efficiently so you don't have to do it all by hand.

Fundamental Programming Concepts

There are only a handful of things you need to understand in order to get started. Some of this may be common knowledge, but I believe they're worth reviewing. This is by no means a comprehensive list since this is just a general introduction and not a textbook.

Variables

A variable is something that stores information that will be referenced later. Variables can basically be anything, from numbers, to words, to lists of items. Parameterizing configuration is the idea of taking the parts of the config that can vary between devices and replacing them with variables that can be updated dynamically. This is a core concept in automation, and one that you will see over and over.

Arrays and Loops

Arrays, also known as lists, represent a series of related items. These are extremely powerful because they can be coupled with loops to process lots of items at once. A typical workflow is to:

  1. Load items into an array (input)
  2. Use a loop to do something to each item in the array (processing)
  3. Post the result of the process (output)

As an example, this is a simple bash script that loops through a list of IPs and prints the contents to a screen.

#!/bin/bash ip_addresses=(192.168.10.10 172.16.50.8 10.80.90.3) for ip in ${ip_addresses[*]}; do # go through the entire array echo "This is an IP address: $ip" # append some text to each item in the array and print to the screen done Output -------- This is an IP address: 192.168.10.10 This is an IP address: 172.16.50.8 This is an IP address: 10.80.90.3 

Again, the syntax doesn't matter here. What's important is the concept.

Automation in Context

Example 1

Now that we've got the basics out of the way, let's move on to the real stuff.

Suppose you get a ticket one day asking you to make a firewall rule that allows certain untrusted addresses to access a trusted web server. Easy enough. You connect to you FW management interface, make a rule that looks something like this, and go on your way.

Name: "allow_http" Source Zone: ["untrust"] Source IP: ["192.168.10.0/24","10.15.0.0/24"] Destination Zone:["trust"] Destination IP: ["172.16.10.200/32"] Application: ["http"] Action: "allow" 

Let's take a moment to consider the structure of this rule.

  • First, a firewall rule must have certain attributes to be considered valid. For example, it's not possible to have a rule that has no action or no name

  • Second, each attribute only takes certain types of values. For example, the "Name" attribute can't be an array because it doesn't even make sense to have a list of names for a single firewall rule. On the other hand, "Destination IP" must be an array (even if it has a single value) because it's possible to have multiple destinations in a firewall rule.

  • Third, all values must conform to certain rules. For example, I can't just set the destination IP to "the webserver in the back room" and expect the firewall to know what I'm talking about. It must be a valid IP address.

  • Finally, this combination of attributes and their values is what creates an "object". In other words, in order for something to be considered a firewall rule, it must have these attributes that with values conform to set specifications.

Here's why this is important.It means that the representation of data doesn't matter as long as the data itself is valid. You could be using a CLI, or a GUI, or a fancy API. The end result will be the same as long as the device receives commands in a format that it accepts.

If I were using an SRX, the policy above would look something like this. As you can see, the only thing that's really changed is how the information is presented; the actual data and its function does not change.

set security policies from-zone untrust to-zone trust policy allow_http match source-address 192.168.10.0/24 set security policies from-zone untrust to-zone trust policy allow_http match source-address 10.15.0.0/24 set security policies from-zone untrust to-zone trust policy allow_http match destination-address 172.16.10.200/32 set security policies from-zone untrust to-zone trust policy allow_http match application http set security policies from-zone untrust to-zone trust policy allow_http then permit 

But what if you were tasked with adding a 100 different non-contiguous source addresses to the policy? Setting aside whether or not it's a good idea, the reality is that doing it by hand would be pretty painful. That will be the focus of the second example, which I will present in the third part of this series.


I'm going to stop this post here since it's starting to run a bit long. This is also a good point to pause for discussions and/questions. If there's anything I got wrong or isn't clear, please, please let me know. I'd rather not be another peddler of bad information.

I also mentioned in the first post that I'd be sharing ansible roles. The first one can be found here: https://github.com/wakapeil/palo-alto-ansible-roles/tree/master/role-panos-vpn



No comments:

Post a Comment