Note
This guide is the source-based administrator and developer reference for
paloaltonetworks.panos, the official PAN-OS Ansible Collection. It is built from the in-tree sources at commit0ea91f8(collection version3.3.0) and covers architecture, configuration reference, scaling, operations, performance, security, internals, and integration. Pair it with the User Guide for usage and recipes.
The collection is a thin Ansible facade over the pan-os-python SDK and the
PAN-OS XML API. It contains 118 modules in plugins/modules/, one
shared ConnectionHelper in plugins/module_utils/panos.py, an httpapi
plugin at plugins/httpapi/panos.py, and an Event-Driven Ansible (EDA)
event source under extensions/eda/. There is no on-firewall agent —
everything talks to PAN-OS over its XML API on TCP/443.
1. Architecture Overview
Where the collection sits
+-------------------+ +----------------------+ +----------------+
| Ansible Control | | paloaltonetworks. | | pan-os-python |
| Node (your CI, | | panos collection | | SDK (1.12.4+) |
| AAP, or laptop) |--------| plugins/modules/ |---------| PanDevice |
| | | plugins/httpapi/ | | Firewall |
| ansible-playbook | | plugins/module_utils| | Panorama |
| (Python 3.10+) | | (ConnectionHelper) | +----------------+
+-------------------+ +----------------------+ |
| XML over HTTPS
v
+-------------------+
| PAN-OS XML API |
| (TCP/443) |
| /api/?type=... |
+-------------------+
|
v
+-------------------+
| NGFW / Panorama |
| candidate config |
| running config |
+-------------------+
The control node runs ansible-playbook, which loads the
paloaltonetworks.panos collection and executes one of the 118 modules.
Each module imports pan-os-python (import panos) and uses
ConnectionHelper.get_pandevice_parent() to construct the
SDK object tree. The SDK then makes XML API calls to the firewall over
HTTPS. The firewall mutates its candidate configuration; nothing is
active on the firewall until a commit module is invoked.
Component inventory
| Component | Path | Role |
|---|---|---|
| 118 task modules | plugins/modules/panos_*.py |
One module per PAN-OS resource — the public surface |
| Shared helper | plugins/module_utils/panos.py |
ConnectionHelper, get_connection(), cmd_xml(), idempotency engine |
| HTTP API plugin | plugins/httpapi/panos.py |
Inventory-driven httpapi connection; raw XML API client |
| Doc fragments | plugins/doc_fragments/fragments.py |
16 reusable doc fragments (provider, vsys, device_group, state, …) |
| EDA event source | extensions/eda/plugins/event_source/logs.py |
Stream PAN-OS logs to EDA rulebooks |
| Sample rulebook | extensions/eda/rulebooks/decryption.yaml |
Reference EDA rulebook |
| Integration tests | tests/integration/firewall/, tests/integration/panorama/ |
Live-firewall idempotency tests |
| Unit tests | tests/unit/httpapi/, tests/unit/plugins/module_utils/ |
httpapi plugin and module_utils unit coverage |
| Sanity-test exclusions | tests/sanity/ignore-2.16.txt … ignore-2.20.txt |
Per-Ansible-version sanity-test allowlists |
| Galaxy metadata | galaxy.yml |
Collection name, version, dependencies, tags |
| Runtime routing | meta/runtime.yml |
Required Ansible version, deprecation redirects |
Technology stack
| Layer | Component | Version |
|---|---|---|
| Ansible Core | The runtime | >=2.16.0 per meta/runtime.yml |
| Python | Control-node interpreter | >=3.10 per requirements.txt |
| PAN-OS SDK | pan-os-python (panos) |
>=1.12.4,<2.0 |
| Upgrade Assurance | panos-upgrade-assurance |
>=2.0.0,<3.0 |
| XML/HTTP | xmltodict, aiohttp, dpath |
Pinned in requirements.txt |
| Distribution | Galaxy + GitHub releases | Versioned via semantic-release (see .releaserc.json) |
| Test framework | pytest, molecule-style integration playbooks |
In tests/ |
Note
The SDK rename history matters.
pandevicewas the original SDK name; it becamepan-os-python(importable aspanos) when 1.0 shipped. Every module in this collection has a fallbacktry/except ImportErrorchain that imports from the new name first and falls back topandevicefor backwards compat. New code should targetpanosonly.
Data flow for a single task
ansible-playbook task plugins/modules/panos_address_object.py
| |
| invokes |
v v
+----------------------+ +------------------------+
| AnsibleModule init | | get_connection(...) |
| with argument_spec |------------->| -> ConnectionHelper |
+----------------------+ +------------------------+
|
| get_pandevice_parent()
v
+------------------------+
| PanDevice.create_ |
| from_device(provider) |
| -> Firewall|Panorama |
+------------------------+
|
| helper.process(module)
v
+------------------------+
| sdk_cls.refresh() |
| -> compare candidate vs|
| -> apply_state() |
| -> set/edit/delete XML |
+------------------------+
|
v
module.exit_json(changed=...)
Key invariants
- Modules never commit by default. Configuration changes always land in
the candidate; you must run a commit module (
panos_commit_firewall,panos_commit_panorama, orpanos_commit_push) to make them active. The legacywith_commitflag on some older modules is deprecated for the 4.0.0 major release. - One module per resource type. Address objects →
panos_address_object, security rules →panos_security_rule, NAT rules →panos_nat_rule2, and so on. The 1:1 mapping makes diff/idempotency cheap. - All state lives on the device. No collection-side cache or DB. Every task fetches the current candidate state on demand and computes the diff.
- The XML API is the source of truth for capability. A module's parameters are the union of what the SDK exposes and what the underlying PAN-OS XML schema accepts.
Note
Despite 118 modules, the design is non-obvious for one reason: every module is essentially a thin descriptor that hands off to
ConnectionHelper.process()inplugins/module_utils/panos.py. That single 1900-line file handles connection, version checks, scope walking, idempotency, diff, gathered-filter, and XML serialisation. Read that file once and you have read 80 % of the collection.
2. System Requirements
Control node (where ansible-playbook runs)
| Item | Requirement |
|---|---|
| OS | Any OS that runs Ansible Core 2.16+ (Linux, macOS, WSL2) |
| RAM | 1 GB minimum; 4 GB recommended for parallel forks |
| Disk | 200 MB for the collection + SDK |
| Python | 3.10, 3.11, 3.12, or 3.13 |
| Ansible Core | 2.16 → 2.20 (sanity-tested per tests/sanity/ignore-2.*.txt) |
| Network egress | TCP/443 to firewalls and Panorama (no inbound required) |
Python dependencies
From requirements.txt:
aiohttp>=3.8.4 ; python_version >= "3.10" and python_version < "4.0"
dpath>=2.1.5 ; python_version >= "3.10" and python_version < "4.0"
pan-os-python>=1.12.4,<2.0 ; python_version >= "3.10" and python_version < "4.0"
panos-upgrade-assurance>=2.0.0,<3.0 ; python_version >= "3.10" and python_version < "4.0"
xmltodict>=0.12.0 ; python_version >= "3.10" and python_version < "4.0"
Important
Issues #648, #655 — the upper bounds in
requirements.txtare tighter than strictly necessary on some non-PANW deps. If you hit a pip resolver conflict, you can usually relaxaiohttp,dpath, orxmltodictupper bounds. Do not relaxpan-os-python<2.0— the collection uses 1.x APIs.
PAN-OS device
| Item | Requirement |
|---|---|
| PAN-OS version | 8.0+ (per-module checks declare more recent minimums) |
| Management plane | Reachable on TCP/443 from the control node |
| Account | Admin role with XML API permission, or pre-generated API key |
| TLS | Self-signed cert OK; trust it on the control node or set validate_certs: false |
Panorama device (when applicable)
Same as a firewall, plus:
- A
device_groupnamed scope for the objects/policies you'll manage. - A
templateand/ortemplate_stacknamed scope for network/device config. - Managed firewalls registered and reachable for
panos_commit_pushto succeed.
3. Configuration Reference
This is the complete parameter reference. Parameters split into four groups:
- Provider parameters — credentials and connection details (
providerdict). - Scope parameters — where in PAN-OS the operation targets (
vsys,device_group,template, …). - State and movement parameters —
state,location,audit_comment,target, … - Resource parameters — fields specific to the object the module manages.
3.1 Provider parameters (provider dict)
Source: plugins/doc_fragments/fragments.py, PROVIDER fragment.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
ip_address |
str | yes | — | IP/hostname of the firewall or Panorama |
username |
str | no | admin |
Account name (ignored if api_key set) |
password |
str | no | — | Password (ignored if api_key set) |
api_key |
str | no | — | XML API key (use instead of user/pass) |
port |
int | no | 443 |
TLS port |
serial_number |
str | no | — | If ip_address is a Panorama, target a managed firewall by serial |
The transitional_provider fragment also exposes the deprecated classic
params (ip_address, username, password, api_key, port) directly on
the task — these are kept for backwards compat but are scheduled for removal
in 4.0.0.
3.2 Scope parameters
| Parameter | Source fragment | Default | Description |
|---|---|---|---|
vsys |
vsys |
vsys1 |
Firewall vsys for objects/interfaces |
vsys |
vsys_shared |
shared |
Vsys for shared services (syslog/snmp profiles) |
vsys |
vsys_import |
(none) | Vsys to import into for importable objects |
vsys_dg |
vsys_dg |
vsys1 (NGFW) / shared (Pano) |
Combined vsys+device-group |
device_group |
device_group |
shared |
Panorama device group |
template |
template_only / full_template_support |
(none) | Panorama template |
template_stack |
full_template_support |
(none) | Panorama template stack |
rulebase |
rulebase |
pre-rulebase (Pano) / rulebase (NGFW) |
Pre/Post rulebase |
3.3 State parameters
| Parameter | Source fragment | Choices | Default |
|---|---|---|---|
state |
state |
present / absent |
present |
state |
enabled_state |
present / absent / enabled / disabled |
present |
state |
network_resource_module_state |
present / absent / replaced / merged / deleted / gathered |
present |
3.4 Movement and rule parameters (policy modules)
| Parameter | Source fragment | Choices | Description |
|---|---|---|---|
location |
movement |
top / bottom / before / after |
Position of the new rule |
existing_rule |
movement |
str | When location=before/after, the relative rule |
target |
target |
list | Restrict rule to specific firewall serials |
negate_target |
target |
bool | Negate the target list |
audit_comment |
audit_comment |
str | Audit message for the rule (only on change) |
uuid |
uuid |
str | Read-only rule UUID (advanced) |
gathered_filter |
gathered_filter |
str | Filter expression for state: gathered |
3.5 Per-module-family resource parameters
The collection has a few major module families. Below is the parameter set for each.
Address-object family
| Module | Key parameters |
|---|---|
panos_address_object |
name, value, address_type (ip-netmask/ip-range/fqdn/ip-wildcard), description, tag |
panos_address_group |
name, static_value (list), dynamic_value (str expression), description, tag |
panos_service_object |
name, protocol (tcp/udp), source_port, destination_port, description, tag |
panos_service_group |
name, value (list), tag |
panos_application_object |
name, category, subcategory, technology, risk, etc. |
panos_application_group |
name, value (list) |
panos_application_filter |
name, category, subcategory, technology, risk, etc. |
panos_tag_object |
name, color (40+ named choices) or color_value (XML), comments |
panos_schedule_object |
name, disable_override, type (recurring/non-recurring), schedule rules |
panos_custom_url_category |
name, url_value, type, description |
panos_dynamic_user_group |
name, description, filter, tag |
panos_edl (External Dynamic List) |
name, source, repeat, description |
panos_region |
name, address, latitude, longitude |
Policy family
| Module | Key parameters |
|---|---|
panos_security_rule |
rule_name, source_zone, source_ip, source_user, destination_zone, destination_ip, application, service, category, action (allow/deny/drop/reset-client/reset-server/reset-both), log_setting, log_start, log_end, description, rule_type (universal/intrazone/interzone), tag_name, negate_source, negate_destination, disabled, schedule, icmp_unreachable, disable_server_response_inspection, group_profile, antivirus, vulnerability, spyware, url_filtering, file_blocking, data_filtering, wildfire_analysis, group_tag |
panos_nat_rule2 |
name, description, nat_type (ipv4/nat64/nptv6), from_zones, to_zones, to_interface, service, source_addresses, destination_addresses, source_translation_* family (type, address_type, interface, ip_address, translated_addresses, fallback_, static_), destination_translated_address, destination_translated_port, ha_binding (primary/both/0/1), disabled, tags, destination_dynamic_translated_* family, group_tag |
panos_pbf_rule |
name, description, tags, from_type (zone/interface), from_value, source_addresses, source_users, negate_source, destination_addresses, negate_destination, applications, services, schedule, disabled, action (forward/forward-to-vsys/discard/no-pbf), forward_* family, enable_enforce_symmetric_return, symmetric_return_addresses, group_tag |
panos_decryption_rule |
name, description, source/destination zones+IPs, applications, action |
panos_match_rule |
Test traffic against rules (replaces panos_query_rules) |
Network family
| Module | Purpose |
|---|---|
panos_interface |
Ethernet data-port (if_name, mode, ip, enable_dhcp, zone_name, vr_name) |
panos_aggregate_interface |
Aggregate (LACP) interface |
panos_l2_subinterface, panos_l3_subinterface |
Sub-interfaces |
panos_loopback_interface, panos_tunnel, panos_vlan_interface |
Special interface types |
panos_zone |
Security zone (zone, mode tap/virtual-wire/layer2/layer3/external, interface, zone_profile, enable_userid) |
panos_vlan, panos_virtual_wire |
L2 plumbing |
panos_virtual_router |
Legacy VR (name, interface, ad_* distances) |
panos_logical_router, panos_logical_router_vrf |
New advanced-routing engine |
panos_logical_router_vrf_static_route, panos_logical_router_vrf_ospf_area |
Advanced routing details |
panos_static_route |
Static route on a VR (name, destination, nexthop_type ip-address/discard/none/next-vr, nexthop, metric, virtual_router, interface, enable_path_monitor, failure_condition) |
panos_redistribution |
Redistribution profile |
panos_bgp, panos_bgp_* (12 BGP modules) |
BGP configuration |
panos_ipsec_tunnel, panos_ipsec_profile, panos_ipsec_ipv4_proxyid |
IPsec |
panos_ike_gateway, panos_ike_crypto_profile |
IKE |
panos_gre_tunnel |
GRE |
panos_management_profile |
Interface management profile |
panos_dhcp, panos_dhcp_relay, panos_dhcp_relay_ipv6_address |
DHCP |
Device / system family
| Module | Purpose |
|---|---|
panos_facts |
Gather facts (gather_subset of system/session/interfaces/ha/routing/vr/vsys/config) |
panos_check |
Wait for autocommit / device readiness |
panos_op |
Run an arbitrary op command (cmd, cmd_is_xml, ignore_disconnect, vsys) |
panos_administrator |
Add/remove/rotate admin users |
panos_admin (deprecated) |
Subset of panos_administrator |
panos_admpwd |
Set admin password via SSH (used for first-boot) |
panos_api_key |
Generate an API key |
panos_cert_gen_ssh |
Generate certificates via SSH (first-boot) |
panos_lic |
License install |
panos_software |
Install PAN-OS version (version, download, install, restart, sync_to_peer, timeout, named_config, perform_software_check) |
panos_dynamic_updates |
Content / antivirus / app updates |
panos_restart |
Reboot the firewall |
panos_loadcfg |
Load a saved/named config |
panos_mgtconfig |
Management config (DNS, NTP, hostname) |
panos_active_in_ha |
Verify node is HA-active |
panos_ha |
HA configuration |
panos_readiness_checks |
Pre-upgrade readiness (panos-upgrade-assurance) |
panos_state_snapshot, panos_snapshot_report |
Pre/post upgrade snapshots |
panos_userid |
User-ID API |
panos_registered_ip, panos_registered_ip_facts |
Dynamic registered IPs |
panos_export |
Export config / certs / pcaps / tech-support (28 categories) |
panos_import |
Import config / certs / pcaps |
panos_config_element |
Raw XML config element get/set/edit/delete |
panos_type_cmd |
Raw type=keygen/op/config |
panos_vm_auth_key |
VM-Series auth-key |
Profile family
| Module | Purpose |
|---|---|
panos_log_forwarding_profile (+ _match_list, _match_list_action) |
Log forwarding |
panos_syslog_profile, panos_syslog_server |
Syslog server profile |
panos_snmp_profile, panos_snmp_v2c_server, panos_snmp_v3_server |
SNMP server profile |
panos_email_profile, panos_email_server |
Email server profile |
panos_http_profile, panos_http_profile_header, panos_http_profile_param, panos_http_server |
HTTP server profile |
panos_pg (security profile group) |
Profile group (virus/spyware/vulnerability/url/file/data/wildfire) |
Panorama family
| Module | Purpose |
|---|---|
panos_device_group |
Manage device groups (name, parent, devices) |
panos_template, panos_template_stack, panos_template_variable |
Templates |
panos_commit_panorama |
Commit Panorama itself |
panos_commit_push |
Push to managed devices (style, name, admins, include_template, force_template_values, devices, sync) |
3.6 Doc fragments inventory (16 total)
From plugins/doc_fragments/fragments.py:
| Fragment | Adds |
|---|---|
provider |
The provider dict |
transitional_provider |
provider + classic params (deprecated) |
state |
state: present/absent |
enabled_state |
state: present/absent/enabled/disabled |
network_resource_module_state |
state: present/absent/replaced/merged/deleted/gathered |
vsys |
vsys (default vsys1) |
vsys_shared |
vsys (default shared) |
vsys_import |
vsys for imported objects |
vsys_dg |
combined vsys_dg |
device_group |
device_group (default shared) |
template_only |
template (Panorama) |
full_template_support |
template + template_stack |
rulebase |
rulebase: pre-rulebase/rulebase/post-rulebase |
deprecated_commit |
Legacy commit: bool flag (deprecated) |
uuid, movement, target, audit_comment, gathered_filter |
Policy-rule extras |
3.7 meta/runtime.yml deprecation routing
Twelve modules redirect to a successor and will be removed in 4.0.0:
| Old | Redirected to | Reason |
|---|---|---|
panos_admin |
panos_administrator |
Subset of admin module |
panos_commit |
panos_commit_firewall |
Replaced by 3 specialised commits |
panos_dag |
panos_address_group |
Idempotent rewrite |
panos_dag_tags |
panos_registered_ip |
Modern API |
panos_object |
panos_address_object |
Idempotent rewrite |
panos_object_facts |
(no redirect) | Replaced by state: gathered |
panos_nat_rule |
panos_nat_rule2 |
Network-resource rewrite |
panos_nat_rule_facts |
(no redirect) | Use state: gathered |
panos_query_rules |
panos_match_rule |
Better implementation |
panos_sag |
panos_address_group |
Static address group rewrite |
panos_security_rule_facts |
panos_security_rule |
Use state: gathered |
panos_virtual_router_facts |
panos_virtual_router |
Use state: gathered |
panos_zone_facts |
panos_zone |
Use state: gathered |
4. Deployment & Scaling
Single control node (default)
The simplest topology: one Linux host (laptop, jump server, CI runner) with the collection installed, talking to one or many firewalls. No persistence, no shared state — every run starts cold. Forks scale the parallelism:
ansible-playbook -i hosts.yml site.yml --forks 25
forks controls how many firewalls are configured in parallel. Each fork is
an isolated Python process, so there's no shared SDK state between them.
Tip
For 100+ firewalls, set
forks: 50–100and useserial: <N>in the playbook to batch the rollout. PAN-OS handles concurrent API requests well, but you can saturate Panorama if every firewall is doing acommit_pushsimultaneously.
Ansible Automation Platform (AAP) / AWX
AAP is the supported enterprise deployment mode. Build an Execution Environment (EE) container image:
# execution-environment.yml
version: 3
images:
base_image:
name: quay.io/ansible/awx-ee:latest
dependencies:
galaxy:
collections:
- name: paloaltonetworks.panos
version: ">=3.0.0,<4.0.0"
python:
- pan-os-python>=1.12.4,<2.0
- panos-upgrade-assurance>=2.0.0,<3.0
- xmltodict>=0.12.0
- dpath>=2.1.5
Build with ansible-builder build and push to your registry. AAP job
templates point at the EE and execute playbooks on schedule or on demand.
CI/CD scheduler
The collection is fully scriptable from CI. A typical GitLab/GitHub Actions job:
- name: Apply firewall config
run: |
pip install ansible-core>=2.16 pan-os-python panos-upgrade-assurance xmltodict dpath
ansible-galaxy collection install paloaltonetworks.panos
ansible-playbook -i inventory/prod site.yml \
--vault-password-file <(echo "$ANSIBLE_VAULT_PW")
env:
PANOS_PASSWORD: ${{ secrets.PANOS_PASSWORD }}
ANSIBLE_VAULT_PW: ${{ secrets.ANSIBLE_VAULT_PW }}
Event-Driven Ansible (EDA)
The collection ships an EDA event source at
extensions/eda/plugins/event_source/logs.py (declared in
meta/extensions.yml). This lets a rulebook react to PAN-OS log events —
for example, auto-block a malicious IP after a threat log:
# rulebook.yml
- name: Auto-block on threat
hosts: all
sources:
- paloaltonetworks.panos.logs:
host: 0.0.0.0
port: 5514
rules:
- name: Block bad actor
condition: event.threatid == 'malware-foo'
action:
run_playbook:
name: playbooks/auto_block.yml
Scaling concerns
| Concern | Mitigation |
|---|---|
| Slow Panorama commits at scale | Use panos_commit_panorama with device_groups: [...] to limit scope; chunk pushes with serial: |
| API rate limits | Keep forks ≤ 50 for a single Panorama; spread across multiple Panoramas if you have HA pairs |
| Long ops timing out | Bump timeout on panos_software, panos_export, panos_op |
| Fork-per-firewall RAM | ~50 MB per Python fork; budget accordingly |
5. Operations
5.1 Day-to-day runbook
| Task | Module(s) | Cadence |
|---|---|---|
| Verify firewall reachability | panos_check |
Daily smoke test |
| Pull config backup | panos_export (category: configuration) |
Daily |
| Snapshot pre-change state | panos_state_snapshot |
Per-change |
| Push config from Panorama | panos_commit_panorama + panos_commit_push |
Per-change |
| Validate post-change | panos_facts, panos_op (show jobs all) |
Per-change |
| Rotate API keys | panos_api_key + update vault |
Quarterly |
| Pre-stage a new PAN-OS image | panos_software (install: false) |
Per release |
5.2 Health checks
- name: Daily health check
hosts: firewalls
connection: local
gather_facts: false
tasks:
- name: Confirm firewall is responsive
paloaltonetworks.panos.panos_check:
provider: "{{ provider }}"
timeout: 60
- name: Gather session and HA facts
paloaltonetworks.panos.panos_facts:
provider: "{{ provider }}"
gather_subset: ['system', 'session', 'ha']
register: facts
- name: Alert if HA out of sync
ansible.builtin.fail:
msg: "HA out of sync on {{ inventory_hostname }}"
when:
- ansible_net_ha_enabled | default(false)
- ansible_net_ha_localstate not in ['active', 'passive']
5.3 Backup runbook
- name: Daily firewall backups
hosts: firewalls
connection: local
gather_facts: false
tasks:
- name: Export config
paloaltonetworks.panos.panos_export:
provider: "{{ provider }}"
category: 'configuration'
filename: "/var/backups/panos/{{ inventory_hostname }}-{{ ansible_date_time.date }}.xml"
create_directory: true
Caution
Backup XML files are sensitive — they contain admin password hashes, downstream credentials, and certificate private keys. See §7.4.
5.4 Monitoring integration
panos_facts returns a structured dict that maps cleanly to Prometheus
exporters or to a JSON-over-HTTP endpoint:
- name: Publish metrics
ansible.builtin.uri:
url: "https://metrics.example.com/api/v1/panos"
method: POST
body_format: json
body: "{{ ansible_facts | combine({'host': inventory_hostname}) | to_json }}"
Note
The collection deliberately does not ship monitoring code — it produces facts; you decide how to ship them. Common patterns: write to Prometheus textfile collector, push to InfluxDB, or POST to an internal metrics API.
5.5 Disaster recovery
Recovery from "firewall config corrupted":
- Find the most recent good backup XML.
- SCP it to the firewall (or use the restore-via-import workflow).
panos_opwithcmd: '<load><config><from>good.xml</from></config></load>'andcmd_is_xml: true.panos_commit_firewall.
Recovery from "firewall offline / replaced":
- Boot the new firewall to factory defaults.
panos_admpwdto set the admin password (via SSH).panos_licto install the license.panos_loadcfgto load the previously exported running config.panos_commit_firewallto activate.
Tip
For a full unattended bootstrap, chain the modules above with
panos_checkbetween each step so the next module waits until the previous has finished.
5.6 Operational pre-flight checklist
- [x]
pan-os-pythoninstalled on the control node - [x]
panos-upgrade-assuranceinstalled (for the upgrade modules) - [x]
providervars vaulted, never plaintext in playbook YAML - [x]
ansible_python_interpreterpinned in inventory - [x] Collection version pinned in
requirements.yml - [ ] Backup runbook scheduled in CI
- [ ] EDA rulebook deployed for log-driven response (optional)
6. Performance
6.1 Where time goes
A rough breakdown for a typical "add 100 address objects + 50 rules + commit" playbook against a real PA-VM:
| Phase | Wall time | Notes |
|---|---|---|
Connection setup (PanDevice.create_from_device) |
~1.5 sec | Once per task |
| Per-object refresh + diff | ~80 ms | Each object module reads then writes |
| XML set request | ~50 ms | Each individual object create |
| Commit | 30 sec – 5 min | Depends on candidate size and device model |
| Commit push (Panorama → device) | 1–10 min | Per device |
6.2 Tuning levers
| Knob | Effect |
|---|---|
forks: <N> (Ansible) |
Parallelize across hosts. ~50 is a safe ceiling for one Panorama. |
serial: <N> (Ansible) |
Roll out to N hosts at a time — reduces blast radius |
gather_subset |
Limit panos_facts to what you need; config is heavy |
sync: false (commit modules) |
Return immediately with a jobid; poll later |
timeout |
Per-module; bump for long ops |
poll_interval / poll_timeout (panos_op) |
XML API job polling |
6.3 Common performance pitfalls
gather_subset: ['all']against Panorama — Panorama only supportssystem,ha,configsubsets. Asking forinterfacesis wasted time.- Many small commits — batch all your changes in one play, then commit once. Each commit takes 30+ seconds on a real firewall.
panos_opin a tight loop — every op is a full HTTP round trip plus a job poll. Consider whether one structured XML show command can replace 20 CLI commands.- No
sync: falseon long pushes — if you're pushing to 50 device groups, run them async and poll for completion at the end.
Note
The 5 MB XML body limit (in
plugins/httpapi/panos.py:578) is a hard cap. If you ever hitData too large for XML API request, the only fix is to split the request — there's no server-side knob.
7. Security
7.1 Threat model
The control node holds credentials that have full administrative control over PAN-OS firewalls and Panorama. A compromised control node = compromised firewall fleet. Every recommendation below treats that as the central threat.
7.2 Credential handling
-
Never check unencrypted credentials into source control. Always use
ansible-vault encrypt_string:ansible-vault encrypt_string --stdin-name 'vault_panos_pw' # paste password, ctrl-D -
Set
ANSIBLE_VAULT_PASSWORD_FILE(or--vault-password-file) so you never type the master vault password into shell history:export ANSIBLE_VAULT_PASSWORD_FILE=$HOME/.ansible/vault_pw chmod 600 $HOME/.ansible/vault_pw -
Use
no_log: trueon every task that handles a secret. The collection's modules already mark the relevant parameters (api_key,password,admin_password,admin_phash, etc.) withno_log=Truein their argument spec, but Ansible may still print parameter values via thedebugtask —no_log: trueon the task is the belt-and-braces step:- name: Set admin password paloaltonetworks.panos.panos_administrator: provider: "{{ provider }}" admin_username: 'automation' admin_password: "{{ vault_automation_pw }}" superuser: true state: present no_log: true
Important
no_log: trueon a task suppresses parameter logging, but it does not suppress what your module returns inregister:. If youregister: resultafter a sensitive task, thendebug: var=result, you may still print secrets. Audit your debugging.
7.3 Secrets handling
- API keys are bearer tokens equivalent to admin credentials. Treat them like passwords — vault them, rotate them, never commit them.
- Admin password hashes (
admin_phash) are reusable: anyone who has the hash can paste it back in viapanos_administratorand inherit that admin's role. Treat hashes like passwords. - Downstream service credentials (LDAP, RADIUS, SNMPv3, syslog over TLS, email server passwords) are part of the device configuration, so a configuration export is sensitive too (see 7.4).
7.4 Protect configuration backups
Configuration backups taken with panos_export (category: configuration), with panos_op
(<show><config><running></running></config></show>) or with HttpApi.get(xpath="/config") are full XML copies of
the device configuration. They can include administrator password hashes, certificates and keys, and downstream
service credentials, so handle them as credential material:
- Restrict and encrypt. Limit ACLs to the backup operator (
chmod 600), encrypt at rest (disk-level or per-file GPG), and keep them in a private bucket or share with server-side encryption and a retention policy. - Keep API keys on a cadence. Rotate the automation API key on a known schedule (see 7.6) and after a backup has been handled outside the operator host.
- Plan for exposure. If a backup is ever exposed, follow your incident process: rotate the credentials and re-issue the certificates it contained.
For how PAN-OS protects stored secrets (including the master key), use the official PAN-OS administrator documentation as the reference, not this guide.
7.5 Network security
- The firewall management plane should never be exposed on the public Internet. Restrict TCP/443 to the control-node CIDR.
- Use TLS verification end-to-end. The
httpapiplugin'sansible_httpapi_validate_certs: falseis acceptable in lab; in production, install a real CA-signed cert on the firewall and enable validation. - Restrict the firewall's management profile (or whole management vsys) to the control plane.
- For Panorama, separate the management network from the production data plane.
7.6 Authentication and authorization
- Use a dedicated
ansibleadmin role with only the permissions Ansible needs. Don't usesuperuserif read-only would do. - Rotate the API key quarterly via
panos_api_key(or PAN-OS nativedebug api-key). - Use authentication profiles (LDAP/SAML/RADIUS) for human admins; reserve local admins for automation accounts only.
- Enable PAN-OS audit logging for all API actions; ship audit logs to a SIEM.
7.7 Code-supply-chain security
- Pin the collection version in
requirements.yml(>=3.0.0,<4.0.0). - Pin Python deps in your control-node lock file. Don't blindly upgrade
pan-os-pythonmid-release. pan-os-pythonis signed and published from PyPI by Palo Alto Networks. Verify withpip download --require-hashesif you mirror.- Signed releases of the collection itself live on Galaxy and as GitHub
release assets (signed via
semantic-releaseper.releaserc.json).
7.8 Hardening checklist
- [x] All credentials in
ansible-vault encrypt_string-protected vars - [x]
no_log: trueon every task that handles a secret - [x] Backup files have restricted ACLs (
chmod 600, owned by backup operator only) - [x] Backup files are encrypted at rest (disk-level or per-file GPG)
- [x] API keys are rotated on a known cadence (e.g., quarterly, plus after any backup leaves the operator host)
- [x] Vault password file has
chmod 600 - [x] Per-firewall least-privilege admin role for automation (not
superuser) - [x] Firewall management plane locked to the control-node CIDR
8. Developer Internals
8.1 Repository layout
pan-os-ansible/
├── galaxy.yml # Collection metadata: namespace, version, deps
├── meta/
│ ├── runtime.yml # requires_ansible >=2.16; deprecation routing
│ └── extensions.yml # EDA extension registration
├── plugins/
│ ├── modules/ # 118 task modules — the public surface
│ ├── module_utils/
│ │ └── panos.py # ConnectionHelper + get_connection() (~1900 lines)
│ ├── httpapi/
│ │ └── panos.py # HTTP API connection plugin (~640 lines)
│ └── doc_fragments/
│ └── fragments.py # 16 reusable doc fragments
├── extensions/
│ └── eda/
│ ├── plugins/event_source/logs.py
│ └── rulebooks/decryption.yaml
├── tests/
│ ├── integration/ # Live-firewall idempotency tests
│ │ ├── README.md
│ │ ├── ansible.cfg
│ │ ├── inventory.example
│ │ ├── reset.yml # Reset firewall to blank config
│ │ ├── run_all_tests.yml
│ │ ├── run_single_test.yml
│ │ ├── setup.yml
│ │ ├── firewall/test_panos_*.yml
│ │ ├── panorama/test_panos_*.yml
│ │ └── group_vars/all.yml
│ ├── unit/
│ │ ├── httpapi/test_panos.py
│ │ ├── plugins/module_utils/test_panos.py
│ │ └── plugins/modules/common/utils.py # AnsibleExitJson harness
│ └── sanity/
│ └── ignore-2.16.txt … ignore-2.20.txt
├── docs/
│ ├── source/ # Sphinx config for autodoc
│ ├── templates/module.rst.j2
│ └── contributing.md
├── requirements.txt
├── pyproject.toml
├── Makefile
├── README.md
├── CHANGELOG.md
└── .releaserc.json # semantic-release config
8.2 Module structure: anatomy of a panos_* module
Every module follows the same five-section layout. Take
plugins/modules/panos_address_object.py as the canonical example:
- License header — Apache 2.0.
DOCUMENTATIONstring — module name, short description, author, version_added, requirements, notes, doc fragments referenced viaextends_documentation_fragment, and module-specificoptions:.EXAMPLESstring — copy-pasteable YAML examples.RETURNstring — what the module returns to the caller.main()function — callsget_connection()to build a helper, thenhelper.process(module).
Example (panos_address_object.py, abridged):
def main():
helper = get_connection(
vsys=True,
device_group=True,
with_classic_provider_spec=True,
with_network_resource_module_state=True,
with_commit=True,
with_gathered_filter=True,
sdk_cls=("objects", "AddressObject"),
sdk_params=dict(
name=dict(required=True),
value=dict(),
address_type=dict(
default="ip-netmask",
choices=["ip-netmask", "ip-range", "fqdn", "ip-wildcard"],
sdk_param="type",
),
description=dict(),
tag=dict(type="list", elements="str"),
),
)
module = AnsibleModule(
argument_spec=helper.argument_spec,
required_one_of=helper.required_one_of,
supports_check_mode=True,
)
helper.process(module)
That's the entire module body. The get_connection() call is declarative:
it tells the helper which scope and capabilities to set up, and which SDK
class (objects.AddressObject) maps to which Ansible parameters.
8.3 ConnectionHelper deep dive
Source: plugins/module_utils/panos.py. The ConnectionHelper class is the
heart of the collection. Its lifecycle:
class ConnectionHelper(object):
def __init__(
self,
min_pandevice_version,
min_panos_version,
min_panos_upgrade_assurance_version,
error_on_firewall_shared,
panorama_error,
firewall_error,
template_is_optional,
):
# Sets up argument_spec, required_one_of, and the
# version-min checks for pan-os-python and PAN-OS itself.
...
def get_pandevice_parent(self, module, timeout=0):
# 1. Verify pan-os-python (and panos-upgrade-assurance) are
# installed at the required minimum version.
# 2. Build the auth tuple from provider or classic params.
# 3. PanDevice.create_from_device(*auth) — returns Firewall or
# Panorama instance.
# 4. Verify min_panos_version against device's _version_info.
# 5. Walk down to the right scope: vsys, device_group, template,
# rulebase. Returns the parent SDK object.
...
def process(self, module):
# The orchestrator. Calls:
# - get_pandevice_parent()
# - initial_handling(module) (subclass hook)
# - parent_handling(parent, module) (subclass hook)
# - sdk_cls.refresh(parent)
# - spec_handling(spec, module) (subclass hook)
# - object_handling(obj, module) (subclass hook)
# - pre_state_handling(obj, result, module) (subclass hook)
# - apply_state(obj, listing, module, ..., result)
# - post_state_handling(obj, result, module) (subclass hook)
# Then exits the module with the result dict.
...
def apply_state(self, obj, listing, module, ..., result):
# The diff-and-apply engine. For each state:
# - present: create or update
# - absent / deleted: delete
# - replaced: full overwrite
# - merged: additive merge
# - gathered: read only, return matching items via
# matches_gathered_filter()
...
def commit(self, module, include_template=False, admins=None):
# The shared commit helper used by older modules' with_commit=True.
# New modules use the dedicated commit modules instead.
...
8.4 Subclassing ConnectionHelper for module-specific behaviour
Many modules need extra logic — pre-flight, special spec handling, post-state
side-effects. They subclass ConnectionHelper and override hooks. Example:
plugins/modules/panos_security_rule.py:
class Helper(ConnectionHelper):
def initial_handling(self, module):
# Migrate the deprecated "devicegroup" param to "device_group"
if module.params["devicegroup"] is not None:
module.deprecate(...)
module.params["device_group"] = module.params["devicegroup"]
def spec_handling(self, spec, module):
# PAN-OS 10+ removed hip_profiles; supply a default for older
# versions so we don't break existing playbooks.
if module.params["state"] not in ("present", "replaced"):
return
if self.device._version_info < (10, 0, 0) and spec["hip_profiles"] is None:
spec["hip_profiles"] = ["any"]
def main():
helper = get_connection(
helper_cls=Helper, # custom subclass
...
)
The hooks (in execution order):
initial_handling(module)— before any device interaction. Validate or migrate parameters.parent_handling(parent, module)— after the parent SDK object is built, before the resource is fetched.spec_handling(spec, module)— mutate the parameter dict before it is serialized to XML.object_handling(obj, module)— mutate the SDK object before apply.pre_state_handling(obj, result, module)— final hook beforeapply_state. Used for hashing passwords (e.g.panos_administrator).apply_state(...)— the framework's diff-and-apply engine. Not usually overridden.post_state_handling(obj, result, module)— after apply. Used for post-write side effects (e.g., refreshing API key after admin's own password change inpanos_administrator).
8.5 Idempotency / diff logic
The framework computes idempotency by:
- Fetching the live SDK object via
parent.refresh(obj). - Comparing each field in
sdk_paramsbetween the desired spec and the live object. - If they match →
changed=False, return. - If they differ → mutate the live object, call
obj.apply()(orobj.create()/obj.delete()as appropriate), setchanged=True.
The state: replaced mode is a full overwrite — fields not in the spec are
set to defaults / nulls. state: merged is additive.
state: gathered uses matches_gathered_filter from
plugins/module_utils/panos.py:1220 to evaluate the user's filter
expression (e.g., name == 'Web-Server-A') against each candidate object,
returning matching specs.
8.6 The HTTP API plugin (plugins/httpapi/panos.py)
The httpapi plugin is a separate code path that lets users configure the
collection via Ansible's httpapi connection model rather than the
provider dict. It's a 640-line module that:
- Caches the API key per connection (via
keygenif not provided) - Implements XML API verbs as Python methods:
show,get,set,edit,delete,rename,move,override,commit,commit_all,op,version,poll_for_job - Exposes
is_panorama()to differentiate Firewall from Panorama - Maps PAN-OS API error codes (
_PANOS_API_ERROR_CODESdict) to aPanOSAPIErrorexception with code + name - Enforces a 5 MB body limit (line 578) to match the XML API server-side cap
- Handles per-request auth via
update_auth()— uses theX-PAN-KEYheader on PAN-OS 9.0+
8.7 Test harness
Unit tests
Under tests/unit/. Use pytest plus a custom harness in
tests/unit/plugins/modules/common/utils.py:
class ModuleTestCase:
@pytest.fixture(autouse=True)
def module_mock(self, mocker):
return mocker.patch.multiple(
basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
)
@pytest.fixture
def connection_mock(self, mocker):
connection_class_mock = mocker.patch(
"ansible_collections.paloaltonetworks.panos.plugins.module_utils.panos.Connection"
)
connection_class_mock.return_value.api_key.return_value = "foo"
connection_class_mock.return_value.get_device_info.return_value = {
"hostname": "PA-VM",
"model": "PA-VM",
"serial": "serial",
"version": "10.0.0",
"uptime": "uptime",
"multivsys": "multivsys",
}
return connection_class_mock.return_value
The harness mocks out AnsibleModule.exit_json / fail_json so the test
can assert on the args without actually exiting the test process.
connection_mock provides a mock httpapi connection.
Integration tests
Under tests/integration/. These are live-firewall tests — they
require a real (or VM) PAN-OS device. The tests/integration/README.md
describes the expected initial config:
ethernet1/1inoutsidezoneethernet1/2ininsidezone- DHCP management configuration
- a CA certificate with key named
local-camarked asforward trust
And for Panorama:
Test-DGdevice groupTest-Templatetemplate
Run a single test:
ansible-playbook -i inventory.ini run_single_test.yml -e test=test_panos_address_object
Run all tests:
ansible-playbook -i inventory.ini run_all_tests.yml
Each test pattern:
import_tasks: 'reset.yml'— restore fromblank.xml.- Create the resource — assert
changed=True. - Re-create with same params — assert
changed=False(idempotency). - Modify the resource — assert
changed=True. - Delete the resource — assert
changed=True. panos_commit_firewall— assert success.
Important
Idempotency is the contract every module must honour. A new module is not merge-ready until it passes the create-create-modify-delete-commit cycle in
tests/integration/.
8.8 Adding a new module
The contributing workflow (per docs/contributing.md) is roughly:
- Identify the SDK class in
pan-os-pythonthat maps to your PAN-OS resource (e.g.,panos.objects.AddressObject,panos.network.VirtualRouter). - Copy a similar module (an existing simple one —
panos_tag_object.pyis a good starter) intoplugins/modules/panos_<resource>.py. - Write the
DOCUMENTATION,EXAMPLES,RETURNstrings. - In
main(), callget_connection()with the rightsdk_cls,sdk_params,vsys/device_group/templateflags, and anywith_*capability flags (with_commit,with_movement,with_uuid,with_target, etc.). - If you need custom behaviour, subclass
ConnectionHelperand override the appropriate hook (see §8.4). - Add an integration test under
tests/integration/firewall/(andpanorama/if applicable). Mirror the create-modify-delete-idempotency pattern. - Run
ansible-test sanityfrom the collection root and fix any new warnings, or add to the relevanttests/sanity/ignore-*.txtif there's a justified pinning. - Open a PR using the
conventionalcommitspreset (feat:/fix:/chore:…) —semantic-releasewill use this to compute the next version number.
Tip
Before writing a custom subclass, check the 16 doc fragments and the existing
with_*flags. 90 % of new modules don't need a subclass — they only need the right declarative flags inget_connection().
8.9 Sanity tests and Ansible-version compatibility
The collection is sanity-tested against Ansible Core 2.16 → 2.20. Each
target version has its own ignore-list at tests/sanity/ignore-2.<N>.txt
that lists known-acceptable warnings (mostly old-Python-syntax warnings
that don't affect runtime). When you add a module, run:
ansible-test sanity --python 3.10 --docker
Add new ignores only when you understand the warning and have agreed it's acceptable.
8.10 Release pipeline
.releaserc.json configures semantic-release to:
- Analyse commits since the last tag (
conventionalcommitspreset). - Compute the next semver version (patch / minor / major).
- Update version in
galaxy.yml,pyproject.toml,docs/source/index.rst. - Update
CHANGELOG.md. - Tag and push.
- Build the Galaxy tarball; publish to Ansible Galaxy and as a GitHub release asset.
The collection is shipped to:
https://galaxy.ansible.com/ui/repo/published/paloaltonetworks/panos.
8.11 Potential improvements
These are open opportunities visible in the source and issue tracker:
- IPv6 in the advanced-routing modules.
panos_logical_router_vrf_static_routedoesn't yet support IPv6 next-hop (#660). - Skip-version upgrade logic in
panos_software. The base-image detection (#662) and post-install hang (#658) could use better diagnostics. - Pretty-print XML in
panos_export. Issue #636. - Multi-line
panos_opcommands. Issue #637. - Action groups in the collection. Issue
#635 —
group related modules for easier
collections:use. - Inbound decryption for PAN-OS 10.2+. Issue #401.
- Selective commit-by-admin in
panos_commit_panorama. Issue #628.
9. Integration Points
9.1 Galaxy / collection ecosystem
- Galaxy URL:
https://galaxy.ansible.com/ui/repo/published/paloaltonetworks/panos - GitHub:
https://github.com/PaloAltoNetworks/pan-os-ansible - Documentation site (pan.dev):
https://pan.dev/ansible/docs/panos - Issue tracker:
https://github.com/PaloAltoNetworks/pan-os-ansible/issues - Galaxy tags:
networking,paloaltonetworks,panw,security,panos(pergalaxy.yml).
9.2 SDK and adjacent libraries
pan-os-python(panos) — the primary SDK the collection wraps. Same vendor; the collection pins>=1.12.4,<2.0.panos-upgrade-assurance— companion library for pre/post upgrade health checks. Used bypanos_active_in_ha,panos_readiness_checks,panos_snapshot_report,panos_state_snapshot. Pinned>=2.0.0,<3.0.pan-python— older transitional SDK. Some legacy modules (e.g.,panos_admin,panos_admpwd) still import it.xmltodict— used bypanos_opto convert XML responses to JSON.dpath— used by thegathered_filterevaluator.aiohttp— used by the EDA event source for async log streaming.
9.3 Ansible Automation Platform (AAP)
Build an Execution Environment with the collection + Python deps; create
job templates that point at it; use AAP's credentials store to inject
PANOS_PASSWORD / vault password.
9.4 Event-Driven Ansible (EDA)
The collection registers an EDA extension via meta/extensions.yml:
extensions:
- args:
ext_dir: eda/plugins/event_filter
- args:
ext_dir: eda/plugins/event_source
The event source at extensions/eda/plugins/event_source/logs.py listens
for PAN-OS log events (typically forwarded as syslog) and emits structured
events to EDA rulebooks. Use case: auto-respond to a threat log by adding
the source IP to a registered-IP tag, then letting a Dynamic Address Group
block it.
9.5 SIEM / observability
- Forward the firewall's logs to Splunk, Sentinel, ELK, or any syslog
collector via
panos_syslog_profile+panos_syslog_server. - Forward to an HTTP endpoint via
panos_http_profile+panos_http_server(for, e.g., Datadog or generic webhook receivers). - Forward to email via
panos_email_profile+panos_email_server. - Forward to SNMPv2c/v3 trap receivers via
panos_snmp_profile+panos_snmp_v2c_server/panos_snmp_v3_server.
The Ansible side configures the firewall's log-forwarding plumbing; the SIEM side is whatever you have.
9.6 Decryption broker / TLS inspection ecosystem
The panos_decryption_rule module manages decryption policy. Pair with
the EDA rulebook at extensions/eda/rulebooks/decryption.yaml for
event-driven decryption tuning.
9.7 Panorama → managed devices
Panorama is itself an integration point. The collection writes to Panorama
via device_group / template / template_stack parameters, then pushes
to managed devices via panos_commit_push. Push styles supported (per
panos_commit_push.style):
device grouptemplatetemplate stacklog collector groupwildfire appliancewildfire cluster
9.8 First-boot / day-zero pipelines
For VM-Series and PA-7000 first-boot bootstrapping, chain:
panos_admpwd(sets initial admin password via SSH;panos_cert_gen_sshgenerates a cert via SSH).panos_lic(license install).panos_dynamic_updates(initial content install).panos_loadcfg(load a baseline config from a saved file).panos_commit_firewall.panos_check(wait for ready).
9.9 Test data / sandboxing
Use tests/integration/setup.yml as a model for spinning up a test
firewall. The pattern: wait for the PAN-OS device to be ready (panos_check),
save a copy of the current config as blank.xml on the device, then run
your tests with the ability to reset back to blank between test cases.
9.10 Compliance / hardening references
The collection pairs cleanly with Palo Alto Networks' IronSkillet configuration framework:
- IronSkillet provides the security-baseline configuration objects.
- Translate IronSkillet's XML into Ansible tasks using
panos_address_object,panos_security_rule,panos_zone, etc.
Or use panos_config_element to push raw XML chunks if you need 1:1
parity with IronSkillet snippets while you migrate.