Most engineers think…
Most engineers think automating a firewall just means 'SSH into the box with Ansible and paste the CLI commands' — so they reach for the network_cli connection like they would for a Cisco switch.
Wrong — and it will fail on day one. The Palo Alto and Fortinet firewall collections do not drive the CLI. paloaltonetworks.panos talks to PAN-OS over its XML API (via a provider / httpapi connection), and fortinet.fortios talks to FortiGate over its REST API (an httpapi connection with an access_token). You manage objects and policy declaratively, and on Palo Alto nothing is live until you commit — a step that has no equivalent on a switch and trips up almost everyone the first time.
① Firewall-as-code — killing drift, GUI clicks and the mystery rules
Meet Sneha, an L2 engineer at Flipkart. A change ticket lands: "Allow the new payment service (203.0.113.40) to reach the app on tcp/8443." She opens the Palo Alto GUI, clicks Policies → Security, fills a form, clicks Commit. Same week, a teammate does the same change on the FortiGate by hand. Six months later an auditor asks "why does rule 47 exist and who approved it?" — and nobody knows. That gap is the disease this lesson cures.
Three things go wrong with click-by-hand firewalls. First, configuration drift: the running rulebase no longer matches any document. Second, no review: a fat-fingered any-any rule goes live with nobody checking. Third, no rollback: undoing a bad change means remembering what it used to be. Multiply that by two vendors with different GUIs and you have the classic outage waiting to happen.
Rules as code flips it. The firewall's objects and policies are described in an Ansible playbook kept in git. A change is now a pull request a colleague reviews; a wrong change is a git revert; and 'why does this rule exist?' is answered by the commit message. The firewall stops being a black box you poke and becomes a file you reason about.
Here's the part that surprises people coming from switch automation. Ansible does not SSH into these firewalls and type CLI. paloaltonetworks.panos drives PAN-OS over its XML API using a provider dict (or an httpapi connection). fortinet.fortios drives FortiGate over its REST API using the httpapi connection. So the connection is httpapi or local — never network_cli. The modules speak the firewall's native API, with structured idempotency baked in.
The four wins of rules-as-code, one tap each
Tap each card — these are the lines you'll repeat to a manager who asks 'why bother automating the firewall?'
Every change is a git commit with author, time and diff. So: 'who changed rule 47 and why' is answered, not guessed.
A change is a PR a second engineer approves before merge. So: a fat-fingered any-any rule gets caught off the firewall.
Undo is git revert + re-run, not 'remember the old value'. So: a bad rule is gone in minutes, cleanly.
One inventory drives Palo Alto and Fortinet from the same source of truth. So: no more divergent hand-config per box.
Manual firewall clicks are like a security guard who lets visitors in on a nod and never writes it down — a month later nobody knows who entered or why. Rules-as-code is the society gate-pass register: every entry is logged with who, when and approved-by, the watchman follows a written list, and if a wrong name got in you can see exactly which entry to strike off. The firewall rulebase is your building; git is the register.
Pause & Predict
Predict: you point Ansible at a Palo Alto firewall using the network_cli connection (the one you'd use for a Cisco switch). What goes wrong? Type your guess.
Aditya at Wipro asks: "What's the single biggest reason to move firewall changes from the GUI into Ansible playbooks in git?"
② Palo Alto with panos — objects, security rules, and the commit step
Let's automate the Flipkart change properly on Palo Alto. Two ingredients: objects (named things like an address) and a security rule that references them. With panos_address_object you create the address partner-pay-host = 203.0.113.40, and with panos_security_rule you write the allow rule. The object comes first so the rule can point at it by name.
Connection is via the provider dict — ip_address, username, and a password or api_key (the modern path is an httpapi connection, but provider is what you'll see most). Key rule fields map straight to the GUI you already know: source_zone / destination_zone (e.g. untrust → dmz), source_ip / destination_ip, application (App-ID, e.g. web-browsing or ssl), service (defaults to application-default), action (allow/deny/drop), tag_name, and crucially location (top/bottom/before/after) which controls where in the rulebase it lands.
Now the gotcha that catches every newcomer. Your panos_security_rule task writes to the candidate config, not the running config. Nothing changes for users until a commit copies candidate to running. So a panos playbook is incomplete without panos_commit_firewall at the end. The inline commit: true parameter on the rule module is deprecated — use the dedicated commit module instead.
If you're driving Panorama instead of a single firewall, two more things change. You target a device_group (and optionally a template for network config), and committing is two steps: panos_commit_panorama commits Panorama, then panos_commit_push pushes it down to the firewalls. Miss the push and your rule lives on Panorama but never reaches the box enforcing traffic.
- name: Flipkart payment allow on PAN-OS
hosts: paloalto
connection: local
gather_facts: false
vars:
provider:
ip_address: "{{ pan_ip }}"
username: "{{ pan_user }}"
api_key: "{{ pan_api_key }}" # from vault — next lesson
tasks:
- name: 1) Address object FIRST
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
name: "partner-pay-host"
value: "203.0.113.40"
description: "Partner payment service"
- name: 2) Security rule that references it
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
rule_name: "allow-partner-pay"
source_zone: ["untrust"]
destination_zone: ["dmz"]
source_ip: ["partner-pay-host"]
destination_ip: ["app-pay-server"]
application: ["ssl"]
action: "allow"
tag_name: ["change-CHG10422"]
location: "bottom"
- name: 3) COMMIT — candidate -> running (or it never enforces)
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"PLAY [Flipkart payment allow on PAN-OS] **************************** TASK [1) Address object FIRST] ************************ changed: [pa-fw-01] TASK [2) Security rule that references it] *********** changed: [pa-fw-01] TASK [3) COMMIT - candidate -> running] ************** changed: [pa-fw-01] PLAY RECAP ********** pa-fw-01 : ok=3 changed=3 unreachable=0 failed=0
Symptom: ansible-playbook reports changed for your panos_security_rule task, but traffic is still blocked and the GUI shows an uncommitted change. Cause: you wrote candidate config but never committed it to running. Fix: add a panos_commit_firewall task (single firewall) or panos_commit_panorama + panos_commit_push (Panorama). Real-world twist from the field: a known bug after upgrading Panorama to 11.x made panos_commit_panorama report success while the config stayed on candidate (it worked on 10.2.8) — so always verify the running config, not just the task result.
Neha at HCL automates a Palo Alto allow rule. The play finishes with ok=2 changed=2 and no errors, but users still can't reach the app. The GUI shows 'changes pending'. What's missing from her playbook?
Pause & Predict
Predict: on Panorama you run panos_security_rule into a device_group and panos_commit_panorama, but skip panos_commit_push. Will the managed firewalls enforce the rule? Type your guess.
③ Fortinet with fortios — REST, VDOMs, tokens and object order
Same change, now on the FortiGate. The fortinet.fortios collection talks to the box over its REST API using the httpapi connection plugin (set ansible_connection: httpapi). The old fortiosapi Python library path is deprecated — httpapi is the supported way now. So, like Palo Alto, you're driving an API, not SSH-ing to the CLI.
Auth is a REST API token. You generate it on the box under System → Administrators → Create New → REST API Admin, then pass it to tasks as access_token (kept in Vault, of course). Every fortios task also names a vdom — default root — so your objects land in the right virtual firewall. Put an object in the wrong vdom and the policy in another vdom can't see it.
The FortiGate version of the object-first rule is strict and bites people constantly. A fortios_firewall_policy references its source/destination by the names of fortios_firewall_address objects. If the policy task runs before those address objects exist, FortiOS rejects it with entry not found in datasource. Ansible runs tasks top-down, so the fix is just ordering: address tasks above the policy task.
One more FortiGate-specific wrinkle: policy position and idempotency. A policy has a numeric policyid and an order in the list (FortiGate evaluates top-down, first match wins). Creating/updating a policy by id is idempotent — run it twice, the second run is ok not changed. But moving a policy's position is a separate action: move (before/after) operation; if you rely on creation order alone, re-running may not re-assert position, so be explicit when order matters to avoid rule shadowing.
▶ Watch one FortiGate change land over REST
Sneha's same payment-allow change, now on the FortiGate. Follow it from the Ansible control node through the httpapi REST session into vdom root. Press Play for the healthy path, then Break it to see the failure.
- name: Flipkart payment allow on FortiGate
hosts: fortigate
connection: httpapi
gather_facts: false
vars:
vdom: "root"
ansible_httpapi_use_ssl: true
ansible_httpapi_validate_certs: false
tasks:
- name: 1) Address objects FIRST
fortinet.fortios.fortios_firewall_address:
access_token: "{{ forti_token }}" # from vault
vdom: "{{ vdom }}"
state: present
firewall_address:
name: "partner-pay-host"
type: ipmask
subnet: "203.0.113.40 255.255.255.255"
- name: 2) Policy that references the object by name
fortinet.fortios.fortios_firewall_policy:
access_token: "{{ forti_token }}"
vdom: "{{ vdom }}"
state: present
firewall_policy:
policyid: 12
name: "allow-partner-pay"
srcintf: [{ name: "port2" }]
dstintf: [{ name: "port1" }]
srcaddr: [{ name: "partner-pay-host" }]
dstaddr: [{ name: "pay-app" }]
service: [{ name: "HTTPS" }]
schedule: "always"
action: "accept"
status: "enable"PLAY [Flipkart payment allow on FortiGate] ************************ TASK [1) Address objects FIRST] ********************** changed: [fg-01] TASK [2) Policy that references the object by name] ** changed: [fg-01] PLAY RECAP ********** fg-01 : ok=2 changed=2 unreachable=0 failed=0 # re-run -> ok=2 changed=0 (idempotent: state already matches)
After the play, confirm the object and policy exist in the right vdom rather than trusting the task result. Quick check on the box: config vdom → edit root → show firewall address partner-pay-host and show firewall policy 12 should both return your config. Better still, re-run the playbook with --check: if everything reports ok (no changed), reality already matches your code — that's idempotency proving the change stuck.
Arjun at ICICI faces this
Arjun, an L1 engineer, runs the FortiGate firewall playbook and the policy task fails immediately with 'entry not found in datasource' on srcaddr. The address task and the policy task are both in the file.
Task order. The policy task is listed ABOVE the fortios_firewall_address tasks, so Ansible runs the policy first — and it references object names that FortiOS hasn't created yet. (Or the objects were created in a different vdom than the policy.)
He reads the play top-to-bottom like Ansible does, confirms the policy is above the address objects, and checks that both tasks use the same vdom (root).
playbook tasks: list fortios_firewall_address ABOVE fortios_firewall_policy; verify vdom matches on both (System → VDOM on the GUI)Reorder so the two fortios_firewall_address tasks come first, then the fortios_firewall_policy, all in vdom root. Re-run the play.
Play now reports changed for the address objects and the policy; show firewall policy 12 on the box lists srcaddr partner-pay-host, and traffic from 203.0.113.40 reaches the app.
Meera at Airtel writes a FortiGate policy referencing srcaddr 'branch-net' and runs it. It fails with 'entry not found in datasource'. The play has both an address task and the policy task. Most likely cause?
Pause & Predict
Predict: you run the FortiGate playbook twice in a row without changing anything. What does the PLAY RECAP show on the second run, and what does that tell you? Type your guess.
④ Safe rollout — git, --check, gated commit, both vendors from one run
You can write rules in code now. The discipline that makes it safe is the rollout flow: playbooks live in git, every change is a pull request a colleague reviews, the play is dry-run with --check, and the commit/push is a deliberate gated step — never an accident. Manual GUI clicking had none of these gates; this is where the payoff is.
Check mode is your preview. ansible-playbook site.yml --check --diff reports which rules would be created or changed, and the diff shows before/after — all without touching the firewall. On Palo Alto you get a second safety net for free: even a real run leaves edits in candidate config until you run the commit task, so you can stage changes and commit only after a human says go. That commit becomes your gate.
Now the headline trick: one inventory, both vendors. You group your boxes — a paloalto group and a fortigate group — and keep the intent once in group_vars (allow 203.0.113.40 → the payment app on HTTPS). One play targets the paloalto group with panos modules (+ commit); another targets the fortigate group with fortios modules. The same business change is translated into each vendor's object + rule syntax from a single source of truth.
arjun@netauto:~/firewall-iac$ git checkout -b chg10422-allow-pay arjun@netauto:~/firewall-iac$ ansible-playbook -i inventory.ini site.yml --check --diff arjun@netauto:~/firewall-iac$ git commit -am "CHG10422 allow 203.0.113.40 -> pay-app (HTTPS)" arjun@netauto:~/firewall-iac$ git push origin chg10422-allow-pay # open PR, get review # --- after a teammate approves the PR --- arjun@netauto:~/firewall-iac$ ansible-playbook -i inventory.ini site.yml
PLAY [Palo Alto] ** changed: address-object, security-rule, COMMIT (candidate->running) PLAY [FortiGate] ** changed: firewall_address x2, firewall_policy (REST, live) PLAY RECAP ********** pa-fw-01 : ok=3 changed=3 failed=0 fg-01 : ok=3 changed=3 failed=0
Why bother with all this rigour? Because the perimeter is a live target. In 2024, PAN-OS CVE-2024-3400 (GlobalProtect command injection, CVSS 10.0) was actively exploited in the wild, and CVE-2024-0012 let attackers bypass the management web interface auth. When the next advisory drops, the team that can review, dry-run and roll out a hardening rule across every firewall from code in minutes — with an audit trail — is in a very different place from the team clicking 40 GUIs by hand at 2 a.m. Rules-as-code isn't just tidiness; it's how you respond fast and prove what you did.
Your single change ('allow the partner host to the payment app') is one tiffin order. The group_vars is the address written once on the lid. The Mumbai dabbawala system reads that one address and routes it through whichever line is right — the panos line commits at the destination gate (the PAN-OS commit), the fortios line drops it straight in (live REST). One order, written once, delivered correctly to two different buildings by two different routes. That's one inventory driving both vendors.
Cold, in 30 seconds: name the panos modules (panos_address_object → panos_security_rule → panos_commit_firewall) and the fortios modules (fortios_firewall_address → fortios_firewall_policy); say the connection model for each (panos = provider/httpapi XML API; fortios = httpapi REST + access_token; never network_cli); state the two big gotchas (PAN forgot-commit; Forti object-before-policy); and describe the safe rollout (git PR → --check → gated commit). If you can do that without notes, you're ready for the next lesson and for the automation questions in PCNSE/NSE and RHCE.
An interviewer asks Sneha: "Give me the single most important reason to put firewall changes behind a git PR + --check + a gated commit, instead of pushing playbooks straight from your laptop." Best answer?
🤖 Ask the AI Tutor
Tap any question — instant, scoped to this lesson. No login, no waiting.
Pre-curated from Ansible docs + community Q&A, scoped to this lesson. For a live prod issue, paste your export into chat.techclick.in.
📝 Wrap-up assessment — six more
You've answered 4 inline. Six left. 70% (7 of 10) marks the lesson complete on your profile. Tap Submit all answers at the end.
🧠 In your own words
Type one line: In one line, why is a Palo Alto rule created by Ansible not actually enforcing traffic until a separate step runs? Then compare to the expert version.
panos_commit_firewall) copies candidate to the running config — so a panos playbook is incomplete without that commit task.🗣 Teach a friend
Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary.
📖 Glossary
- Rules as code
- Defining firewall objects and policies in version-controlled files (playbooks) instead of clicking a GUI, so changes are reviewed, audited and repeatable.
- paloaltonetworks.panos
- The official Ansible collection for PAN-OS firewalls and Panorama; drives the device over its XML API, not the CLI.
- fortinet.fortios
- The official Ansible collection for FortiGate/FortiOS; drives the device over its REST API using the httpapi connection.
- Candidate vs running config
- On PAN-OS, candidate holds your edits; running is what actually enforces traffic. A commit copies candidate to running.
- panos_commit_firewall
- The panos module that commits a firewall's candidate config to running, making earlier rule/object tasks take effect.
- provider (panos)
- A dict of PAN-OS connection details (ip_address, username, password or api_key) passed to panos tasks; the modern alternative is an httpapi connection.
- httpapi connection
- An Ansible connection plugin that talks to a device's HTTP API. Both panos (optionally) and fortios use it instead of network_cli/SSH.
- access_token (fortios)
- A REST API token generated on FortiGate (System → Administrators → REST API Admin) that authenticates fortios tasks; safer than a reused admin password.
- VDOM
- Virtual Domain — a logical firewall instance inside one FortiGate. fortios tasks target a vdom (default 'root') so objects land in the right partition.
- device_group (Panorama)
- On Panorama, the container that holds shared objects and rules pushed to a set of managed firewalls; panos tasks set device_group to target it.
- Object-before-policy
- The ordering rule: an address/service object must exist before a policy can reference it by name, or the policy task errors out.
- --check mode
- Ansible dry-run: reports which tasks would change without writing anything, so you preview a firewall change before it is real.
- Idempotency
- Running the same playbook twice leaves the firewall in the same state — the second run reports 'ok', not 'changed', because the rule already matches.
📚 Sources
- paloaltonetworks.panos.panos_security_rule module — parameters (provider, device_group, rule_name, source_zone/destination_zone, source_ip/destination_ip, application, service, action, tag_name, location) and the deprecated inline 'commit'. paloaltonetworks.github.io/pan-os-ansible/modules/panos_security_rule_module.html
- paloaltonetworks.panos.panos_commit_firewall / panos_commit_panorama / panos_commit_push — committing candidate to running; Panorama commit then push to managed firewalls. paloaltonetworks.github.io/pan-os-ansible/modules/panos_commit_firewall_module.html
- fortinet.fortios.fortios_firewall_policy and fortios_firewall_address modules — vdom, access_token, srcintf/dstintf/srcaddr/dstaddr/service/action/schedule, httpapi connection; legacy fortiosapi deprecated in favour of httpapi. docs.ansible.com/.../fortinet/fortios/fortios_firewall_policy_module.html
- PaloAltoNetworks/pan-os-ansible GitHub issue #569 — real gotcha: after upgrading Panorama to 11.x, panos_commit_panorama reports success but the config stays on candidate and isn't pushed (was fine on 10.2.8). github.com/PaloAltoNetworks/pan-os-ansible/issues/569
- OneUpTime engineering blog — 'How to Use Ansible to Manage FortiGate Firewalls' (Feb 2026): generating a REST API Admin token under System → Administrators, ansible_httpapi setup, address-then-policy ordering. oneuptime.com/blog/post/2026-02-21-how-to-use-ansible-to-manage-fortigate-firewalls/view
- PAN-OS CVE-2024-3400 (GlobalProtect command injection, CVSS 10.0, actively exploited) and CVE-2024-0012 (mgmt-web auth bypass) — why fast, repeatable, auditable firewall change/patching via code matters. security.paloaltonetworks.com/CVE-2024-3400 · cisa.gov alert 2024/04/12
- Red Hat RHCE EX294 objectives (manage Ansible content, write playbooks with tasks/variables, use roles and collections, encrypt with Ansible Vault) + Palo Alto PCNSE / Fortinet NSE automation angle. redhat.com/en/services/training/ex294-red-hat-certified-engineer-rhce-exam-red-hat-enterprise-linux-8-exam
What's next?
You just put firewall passwords and API tokens into variables — now stop them sitting in plaintext. Next we encrypt them with Ansible Vault so your rules-as-code repo is safe to share.