Using Remotely configured Role Names on a Palo Alto firewall

I’ve previously used a mix of LDAP, RADIUS, and TACACS authentication for administrator access on Palo Alto firewalls, but have never done so without local accounts configured on each device. Since our Palo Alto VM-300s are being turned over to the larger parent company with over 20 admins, it is no longer practical to have individual accounts as we needed to control group policy / admin role centrally on the authentication server.

Still on software version 8.1.18, it was a bit confusing how to do this as there were several outdated docs but there, but eventually I found https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClIxCAK which got me on the right track.

Palo Alto Device Setup

Here’s the steps to do this on the Palo Alto device:

  1. If not done already, create a RADIUS or TACACS server profile
  2. If not done already, create an Authentication Profile
  3. Under Device -> Admin Roles, create a new role.
  4. Create or modify a test admin account, defined locally, by setting it to use that role
  5. After verifying roles work as expected, delete that account.
  6. Under Device -> Setup -> Management Tab -> Authentication Settings, set the Authentication Profile for administrative accounts that aren’t defined locally

RADIUS Server Setup

If not done so already, setup a user group to Admin role name mapping on the authentication server. In RADIUS, this is done by adding vendor-specific attribute (VSA) which maps vendor code 25461 to the Admin Role name for the appropriate group. Use Attribute number 1, format = String, and set the attribute value to the admin role name that was created above. This is similar to how the CheckPoints (vendor code 2620) operate.

Here’s an example using NPS on Windows Server 2012R2

Upon successful authentication, the authentication server will result the role name, and the user should be set to that role.

Cisco ISE (TACACS) Server Setup

The process is fundamentally the same, and can be found here:

https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000PMYmCAO

Note the case is not consistent on their group names: they use “Read-Write” and “Read-only”. You can change these to whatever values you want, as long as they’re in sync.

Advertisement

IPAddress vs NetAddr in Python3

I’d heard about netaddr a few weeks ago and had made a note to start. What I learned today as a similar library called ipaddress is included in Python 3, and offers most of netaddr’s functionality just with different syntax.

It is very handy for subnetting. Here’s some basic code that takes the 198.18.128.0/18 CIDR block and splits in it to 4 /20s:

#!/usr/bin/env python

import ipaddress

cidr = "198.18.128.0/18"
subnet_size = "/20"

[network_address, prefix_len] = cidr.split('/')
power = int(subnet_size[1:]) - int(prefix_len)
subnets = list(ipaddress.ip_network(cidr).subnets(power))

print("{} splits in to {} {}s:".format(cidr, len(subnets), subnet_size))
for _ in range(len(subnets)):
    print("  Subnet #{} = {}".format(_+1, subnets[_]))

Here’s the output:

198.18.128.0/18 splits in to 4 /20s:
  Subnet #1 = 198.18.128.0/20
  Subnet #2 = 198.18.144.0/20
  Subnet #3 = 198.18.160.0/20
  Subnet #4 = 198.18.176.0/20

Migrating from CGI to WSGI for Python Web Scripts on Apache

I began finally migrating some old scripts from PHP to Python late last year, and while I was happy to finally have my PHP days behind me, I noticed the script execution was disappointing. On average, a Python CGI script would run 20-80% slower than an equivalent PHP script. At first I chalked it up to slower libraries, but even basic ones that didn’t rely on database or anything fancy still seemed to be incurring a performance hit.

Yesterday I happened to come across mention of WSGI, which is essentially a Python-specific replacement for CGI. I realized the overhead of CGI probably explained why my Python scripts were slower than PHP. So I wanted to give WSGI a spin and see if it could help.

Like PHP, WSGI is an Apache module that is not included in many pre-packaged versions. So first step is to install it.

On Debian/Ubuntu:

sudo apt-get install libapache2-mod-wsgi-py3

The install process should auto-activate the module.

cd /etc/apache2/mods-enabled/

ls -la wsgi*
lrwxrwxrwx 1 root root 27 Mar 23 22:13 wsgi.conf -> ../mods-available/wsgi.conf
lrwxrwxrwx 1 root root 27 Mar 23 22:13 wsgi.load -> ../mods-available/wsgi.load

On FreeBSD, the module does not get auto-activated and must be loaded via a config file:

sudo pkg install ap24-py37-mod_wsgi

# Create /usr/local/etc/apache24/Includes/wsgi.conf
# or similar, and add this line:
LoadModule wsgi_module libexec/apache24/mod_wsgi.so

Like CGI, the directory with the WSGI script will need special permissions. As a security best practice, it’s a good idea to have scripts located outside of any DocumentRoot, so the scripts can’t accidentally get served as plain files.

<Directory "/var/www/scripts">
  Require all granted
</Directory>

As for the WSGI script itself, it’s similar to AWS Lambda, using a pre-defined function. However, it returns an array or bytes rather than a dictionary. Here’s a simple one that will just spit out the host, path, and query string as JSON:

def application(environ, start_response):

    import json, traceback

    try:
        request = {
            'host': environ.get('HTTP_HOST', 'localhost'),
            'path': environ.get('REQUEST_URI', '/'),
            'query_string': {}
        }
        if '?' in request['path']:
            request['path'], query_string = environ.get('REQUEST_URI', '/').split('?')
            for _ in query_string.split('&'):
                [key, value] = _.split('=')
                request['query_string'][key] = value

        output = json.dumps(request, sort_keys=True, indent=2)
        response_headers = [
            ('Content-type', 'application/json'),
            ('Content-Length', str(len(output))),
            ('X-Backend-Server', 'Apache + mod_wsgi')
        ]
        start_response('200 OK', response_headers)
        return [ output.encode('utf-8') ]
            
    except:
        response_headers = [ ('Content-type', 'text/plain') ]
        start_response('500 Internal Server Error', response_headers)
        error = traceback.format_exc()
        return [ str(error).encode('utf-8') ]

The last step is route certain paths to WSGI script. This is done in the Apache VirtualHost configuration:

WSGIPythonPath /var/www/scripts

<VirtualHost *:80>
  ServerName python.mydomain.com
  ServerAdmin nobody@mydomain.com
  DocumentRoot /home/www/html
  Header set Access-Control-Allow-Origin: "*"
  Header set Access-Control-Allow-Methods: "*"
  Header set Access-Control-Allow-Headers: "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  WSGIScriptAlias /myapp /var/www/scripts/myapp.wsgi
</VirtualHost>

Upon migrating a test URL from CGI to WSGI, the page load time dropped significantly:

The improvement is thanks to a 50-90% reduction in “wait” and “receive” times, via ThousandEyes:

I’d next want to look at more advanced Python Web Frameworks like Flask, Bottle, WheezyWeb and Tornado. Django is of course a popular option too, but I know from experience it won’t be the fastest. Flask isn’t the fastest either, but it is the framework for Google SAE which I plan to learn after mastering AWS Lambda.