T Techclick ← Palo Alto hub
Palo Alto · PAN-OS · Security policy · Interactive lesson

PAN-OS Security policy — first match, then prove it

The ticket says “I added deny-mysql.” The Traffic log still shows allow-dmz-any. PAN-OS did not ignore you — a higher rule already matched. This lesson is the model, the click path, and the log fields that close that ticket.

18 min read · L2 primary · Quiz at end

After this page you can

Quick answer (say this out loud)

PAN-OS evaluates Security rules left to right, top to bottom. The first rule whose zones, addresses, user, application, and service all match wins — later rules are not evaluated. Use App-ID for what the session is and Service = application-default so that app only runs on its standard ports. Attach a Security profile group on Allow (profiles do not scan Deny). Prove it in Monitor → Logs → Traffic: Rule Name, Application, Action, Source/Destination Zone. If a new deny never hits, look for a shadow above it — not a broken App-ID.

1. Why first-match matters

A port ACL can look “correct” and still be dead. PAN-OS does not walk the whole rulebase and pick the tightest row. It stops at the first full match.

That is why a specific deny mysql at position 12 never fires if position 6 is trust → dmz, application any, service any, allow. The session already matched. Policy Analyzer calls that a shadow: a lower rule that cannot be hit because a higher rule covers the same traffic with a different action.

Hero · who talks to whom
Trust laptop to NGFW first-match rule stack to Untrust
Notice: the session hits the NGFW once. One rule wins. The rest of the stack is not a second opinion.

Two more reasons this is not “just an ACL”:

Hard words before the runbook

Zone — a label on an interface. The Security rule matches Source Zone and Destination Zone, not the interface name.

App-ID — classification of the application (signatures, decoders, heuristics), not the TCP port.

Service — Layer 4 TCP/UDP port. application-default means “only the standard ports for the App-IDs on this rule.”

Shadow — a lower rule that never hits because a higher rule already matches the same traffic.

2. Mental model

Hold three parts. Interviews fail when people mix them.

1. Match

Source/dest zone, address, user, application, service, (optional) URL category. All specified fields must match. First rule that does, wins.

2. Action + inspect

Allow, Deny, Drop, Reset client/server/both. Profiles and Log Forwarding attach here. Profiles run only on Allow.

3. Proof

Traffic log Rule Name is the rule that won — not the rule you meant. If the name is wrong, the match is wrong. Do not start by changing App-ID.

Say this out loud

Zones decide the door. App-ID decides what walked through it. Service decides which port that app may use. The first full match is the whole decision. The Traffic log Rule Name is the evidence.

Two NAT facts you must not invert (official Security Policy Rules table):

Get those backwards and a perfectly typed App-ID rule never matches. The log then shows interzone-default, not your new name.

3. Match-order flowchart

Read this before you click Policies → Security. The packet does not “find the best rule.” It walks down until one rule says yes.

Path · first match vs shadow
Packet walking a rule stack; first match highlighted, lower rules shadowed
Path A is the first full match. Everything below that row is unread — including the deny you just committed.
Flow 1 · first match, then stop
Session arrives · walk the rulebase top → bottom New session zones + 5-tuple Next rule zone · addr · user · app · svc All fields match? YES STOP this rule wins NO More user rules below? YES · next row NO Default rules intrazone-default allow interzone-default deny Shadow trap Broad allow above a later deny = later rule never read Fix order — do not add a second deny at the bottom If action = Allow → then scan with the Security profile group Antivirus · Anti-Spyware · Vulnerability Protection · URL Filtering · File Blocking · WildFire Analysis Profiles are not match criteria. They do not run on Deny / Drop / Reset.

Read left → right, then down. Diamond = decision. Green STOP is the only rule that writes Rule Name in the Traffic log for this session.

4. How to choose Application vs Service

Application and Service are two different columns. Mixing them is the classic “app-default broke my custom port” ticket.

You needApplicationServiceWhy
Production allow of a known app on its normal port Named App-ID (or group / filter) application-default Official best practice. DNS matches TCP/UDP 53, not 5353. Stops port-hopping evasion.
Same app on a non-standard port (in-house HTTPS on 8443) Keep the App-ID Custom Service object (TCP 8443) application-default will not match 8443 for ssl / web-browsing. Pin the port; do not set Service = any.
Temporary discovery / unknown app mix Application filter or any (short window) application-default still preferred Then use Policy Optimizer / Traffic logs to replace any with named App-IDs.
Port-based “allow TCP 443” Avoid as the permanent design service-https or any Any application can sit on 443. You lose App-ID control and open an evasion path.
Block a specific app that was allowed by a broad rule above Named App-ID, action Deny application-default or any The deny must sit above the broad allow, or it is a shadow.

Source: Security Policy Rules and Create a Security Policy Rule — “always use application-based security rules… and always set the Service to application-default unless you’re using a more restrictive list of ports.”

Do not do this

Do not set Application = mysql and Service = application-default, then wonder why a DBA tool on TCP 3307 never matches. Default ports are in Applipedia / Objects → Applications. If the app is legitimately off-port, create Objects → Services and attach that Service. Service = any on an App-ID rule is the other failure: the app is allowed on every port.

5. Runbook · Side A objects, Side B rule, Side C log

Lab story (placeholder IPs only): users in zone trust (10.1.8.0/24) need DNS and sanctioned web to zone untrust. You will not use the factory rule1 (trust → untrust, any). You will build objects, one allow rule with a profile group, commit, then prove the hit.

Primary source for clicks: Set Up a Basic Security Policy (PAN-OS).

Side A — objects first

  1. Confirm zones exist

    Network → Zones. You need trust and untrust (or your site names) already attached to interfaces. A Security rule cannot invent a zone. If the zone is missing, stop — this is a network build, not a policy ticket.

  2. Address object for the user subnet

    Objects → Addresses → Add. Name net-users-trust. Type IP Netmask. Address 10.1.8.0/24. Official guidance: use address objects as Destination (and Source) instead of typing raw IPs, especially for commonly exploited services.

  3. Security profile group

    Objects → Security Profile Groups → Add. Name it pg-outbound (or name it exactly default if you want every new rule to inherit it). Add Antivirus, Anti-Spyware, Vulnerability Protection, URL Filtering, File Blocking, WildFire Analysis — start from the predefined profiles (default / strict / basic file blocking) documented in the basic-policy guide, then tighten later.

https://fw.lab.example.com/#objects/security-profile-groups Training mock · not live

Objects → Security Profile Groups → Add

Security Profile Group

pg-outbound
cleared (this vsys only)
default
strict
strict
default
basic file blocking
default
Cancel OK

Click next: OK, then Policies → Security → Add. Naming the group default auto-attaches it to new rules. Source: Objects → Security Profile Groups.

Side B — the Security rule

  1. Add the rule in the right place

    Policies → Security → Add. Put this allow below any explicit denies for the same zones, and above leftover any allows and the two default rules. First match is position, not “tightest match.”

  2. General

    Name allow-users-web-dns. Rule Type leave universal unless you have a real reason for intrazone-only or interzone-only. Changing a working universal rule to intrazone is a classic way to send trust→untrust into interzone-default.

  3. Source

    Source Zone = trust. Source Address = net-users-trust (or any). Source User = any unless User-ID is already on that zone.

  4. Destination

    Destination Zone = untrust (post-NAT zone if you NAT to the internet). Destination Address = any for general web, or a specific address object for infrastructure (DNS, SMTP) — the basic-policy guide calls that out as a best practice.

  5. Application + Service

    Applications tab: Add dns, ssl, web-browsing (and later the real SaaS App-IDs you see in logs). Check Depends on on each App-ID in Objects → Applications — some apps implicitly include ssl / web-browsing; others you must add.

    Service/URL Category tab: Service = application-default. Leave URL Category as any unless you intend only web traffic to that category to match.

  6. Actions

    Action Setting = Allow. Profile Type = Group, Group Profile = pg-outbound (or Profile Type = Profiles and pick them one by one). Verify Log at Session End is enabled (default). Do not enable Disable Server Response Inspection. Click OK, then Commit.

https://fw.lab.example.com/#policies/security Training mock · not live

Policies → Security → Add

Security Policy Rule

General Source Destination Application Service/URL Category Actions
allow-users-web-dns
universal
trust
untrust · post-NAT
dns, ssl, web-browsing
application-default
Allow
Group · pg-outbound
Log at Session End · checked · Log Forwarding = none (lab)
Cancel OK · then Commit

Click next: OK, Commit, generate DNS + HTTPS from a host in 10.1.8.0/24, then open Monitor → Logs → Traffic. Source: Policies → Security (PAN-OS & Panorama).

Side C — prove it in the Traffic log

  1. Optional pre-check: Security Policy Match

    Device → Troubleshooting → Select Test = Security Policy Match. Enter Source and Destination IPs, Protocol (6 = TCP), Application if you know it, Execute. The output is the best matching rule for those values. It is a config test — not a live packet.

  2. Generate traffic, then open the log

    From a host in net-users-trust, resolve DNS and fetch an HTTPS site. On the firewall: Monitor → Logs → Traffic. Filter on the source IP or (rule eq 'allow-users-web-dns').

  3. Quote these fields in the ticket

    Rule Name = allow-users-web-dns. Application = dns / ssl / the identified App-ID (not still incomplete at session end). Action = allow. Source Zone / Destination Zone = trust / untrust. Session End Reason is tcp-fin or tcp-rst — not policy-deny. Type/subtype is end (or start if you also logged start), not drop/deny.

https://fw.lab.example.com/#monitor/logs/traffic Training mock · not live

Monitor → Logs → Traffic

Traffic · filter (addr.src in 10.1.8.0/24)

13:04:12 src=10.1.8.41 dst=203.0.113.53 app=dns from=trust to=untrust rule=allow-users-web-dns action=allow type=end
13:04:18 src=10.1.8.41 dst=203.0.113.80 app=ssl from=trust to=untrust rule=allow-users-web-dns action=allow sport=51722 dport=443 type=end
13:04:02 src=10.1.8.41 dst=203.0.113.80 app=incomplete rule=allow-users-web-dns action=allow type=start · ignore until end
Export CSV Add to filter · rule eq allow-users-web-dns

Green close: Rule Name is yours, Application is a real App-ID at session end, Action = allow, zones are the ones on the rule. Source: Monitor → Logs (Traffic log fields: Rule Name, Application, Action, zones).

Green success = these fields

Monitor → Logs → Traffic: rule=allow-users-web-dns · app=dns or ssl (not stuck on incomplete at end) · action=allow · from=trust · to=untrust · Threat log may be quiet if nothing was blocked — that is still a successful allow-and-scan. Device → Troubleshooting → Security Policy Match returns the same rule name.

CLI equivalent (same test as Device → Troubleshooting)
test security-policy-match from trust to untrust source 10.1.8.41 destination 203.0.113.53 protocol 17 destination-port 53 application dns

6. Runtime path after commit

After go-live the firewall does not re-read your intent. It does this.

Flow 2 · session after commit
One session · match, identify, maybe rematch, then log 1 · Ingress interface → zone pre-NAT addresses 2 · First lookup top-down first match post-NAT dest zone 3 · App-ID signatures / decode on allowed traffic 4 · Rematch if App-ID changes new first-match 5 · Allow + scan profile group on Allow only then Log at Session End or Deny / Drop no profile scan deny vs drop in Type Traffic log is written if the matching rule has logging on (default: session end) Rule Name = the rule that won after any rematch · Application = final App-ID · Action = allow / deny / drop interzone-default is silent until you Override it and enable Log at Session End

App-ID overview: traffic is matched against policy first, then signatures run on allowed traffic. If the application later changes, the firewall looks up policy again — a broad ssl allow above a youtube deny is why the page “starts then dies.”

Two log subtypes you will quote (official Traffic log / Monitor → Logs help):

7. Traps + proof checklist

Proof · close the ticket with a named field
Operations desk monitor showing a green health check and a highlighted log row
Notice: proof is a named field on a timestamp, not a screenshot of a spinning tab.
TrapWhat you seeWhat is true
Shadow New deny never hits; old allow still in the log A higher rule already matched. Move the specific rule up, or tighten the broad allow. Policy Analyzer: Shadows.
Service = any on an App-ID allow “It works on every port” You allowed that app off its standard ports. Use application-default unless you deliberately pin a tighter Service.
application-default + custom port App-ID is right, session misses the rule Standard ports only. Create Objects → Services for 8443 (etc.) and attach it.
Profiles on Deny No Threat logs, “AV is broken” Security profiles evaluate only on Allow. Deny does not scan.
Zone / NAT swap Hits interzone-default Destination Zone is post-NAT. Addresses are pre-NAT. Fix the rule, do not add any-any.
Universal → intrazone “to tighten” trust→untrust now denied Intrazone rules do not match between zones. Leave universal unless you mean same-zone only.
Silent default deny User blocked, empty Traffic log Override interzone-default, enable Log at Session End. Defaults are read-only until Override.
Incomplete at session start Panic that App-ID failed Start logs often show incomplete/ssl. Read the end log Application. That is why default is Log at Session End.
Pilot checklist before you call it done

Knowledge check

Six judgment items. Same facts as the runbook. Check answers, then reset if you missed the shadow or the profile-on-allow rule.

Q1

You commit deny-mysql at position 12 (trust → dmz, application mysql). Position 6 is allow-dmz-any (trust → dmz, application any, service any, allow). After commit, mysql still reaches 10.50.5.10. What is the first fix?

Correct: c. First match wins. Position 6 already matched application any. Position 12 is a shadow. Profiles on a deny never run. Re-read match-order + traps.
Q2

You are writing the production allow for dns and smtp from users to IT infrastructure. What do you set on the Service/URL Category tab unless the apps truly use non-standard ports?

Correct: a. Official create-rule guidance: application-based rules and Service = application-default unless you are using a more restrictive port list. Re-read How to choose.
Q3

A deny rule has Profile Type = Group and pg-outbound attached. Users are blocked (good) but the SOC sees no Threat log for that session. Why?

Correct: b. Official Components of a Security Rule: Security profiles are evaluated only for allow. Naming a group default only auto-attaches it to new rules. Re-read mental model + Side B Actions.
Q4

Outbound internet use source NAT. The Security allow still never hits; Traffic (after you override the default) shows interzone-default. Which zone/address pairing is the one PAN-OS documents?

Correct: d. Security Policy Rules table: Destination Zone is post-NAT; addresses stay the original (pre-NAT) IPs. Re-read mental model.
Q5

How do you prove allow-users-web-dns is the rule that actually fired for 10.1.8.41 → 203.0.113.53 UDP/53?

Correct: b. The Traffic log Rule Name is the rule that won. Incomplete on a start log is expected. Re-read Side C + green success.
Q6

You need every implicit interzone deny in the Traffic log. interzone-default has logging off. What does PAN-OS tell you to do?

Correct: c. Defaults are read-only until Override. You can change tags, action, log settings, and security profiles — not the match. Re-read Why it matters + traps.

Sources

Related: PAN-OS session factory · Traffic not passing — 7-step ladder · NAT deep dive · Logging and reporting · Palo Alto interview hub · Palo Alto course