Skip to content
Back to skills

Pfsense Api Inspection

ASecurity

Use when: troubleshooting network/routing/DNS/VPN issues that may involve pfSense rules, NAT port forwards, active states, or dropped packets. Covers read-only inspection of firewall config and logs via the pfSense REST API v2. All operations are GET-only and non-mutating.

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 22, 2026
toolspythongoshellbashrailsapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 22, 2026

npx -y skills add jimmybish/homelab --skill pfsense-api-inspection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pfsense Api Inspection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pfsense Api Inspection
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jimmybish-pfsense-api-inspection/badge)](https://www.skillsdirectory.com/skills/jimmybish-pfsense-api-inspection)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pfsense-api-inspection
description: 'Use when: troubleshooting network/routing/DNS/VPN issues that may involve pfSense rules, NAT port forwards, active states, or dropped packets. Covers read-only inspection of firewall config and logs via the pfSense REST API v2. All operations are GET-only and non-mutating.'
---

# pfSense API Inspection (read-only)

Read-only pfSense investigation via its REST API v2. Use this when troubleshooting connectivity problems where you need to see the current firewall rules, NAT port forwards, active state table, or firewall log without touching the config.

## Access & Guardrails

- **Only GET requests are permitted in this inspection workflow.** Do not assume the API key itself is read-only; other repository automation uses it for mutating DNS operations.
- Do **not** attempt POST/PUT/PATCH/DELETE. If a mutating change is genuinely required, ask the user to make it in the pfSense UI or delegate it to the appropriate managed automation.
- Never log the API key. Never commit it. Use `pfsense_api_key` from Ansible Vault only.
- **Base URL:** `https://{{ groups['pfsense_host'][0] }}` — resolves to `router` in inventory.
- **Auth header:** `X-API-Key: {{ pfsense_api_key }}`
- The LAN interface uses a self-signed certificate, so the request task sets `validate_certs: false`.

## Secure Request Workflow

Keep the decrypted key inside Ansible and suppress the authenticated request task's output. Create this temporary one-shot playbook:

```yaml
---
- hosts: localhost
  gather_facts: false
  tasks:
    - name: Validate that the requested operation is read-only
      ansible.builtin.assert:
        that:
          - pfsense_endpoint is defined
          - pfsense_endpoint | length > 0
          - pfsense_method | default('GET') == 'GET'
        fail_msg: "Only GET requests are allowed by this inspection workflow"

    - name: Query the pfSense API without exposing credentials
      ansible.builtin.uri:
        url: "https://{{ groups['pfsense_host'][0] }}/api/v2/{{ pfsense_endpoint }}"
        method: GET
        headers:
          X-API-Key: "{{ pfsense_api_key }}"
        return_content: true
        validate_certs: false
      register: pfsense_response
      no_log: true

    - name: Display the API response
      ansible.builtin.debug:
        var: pfsense_response.json
```

Save it as `/tmp/pfsense_get.yaml`, then pass only the relative endpoint:

```bash
cd ansible
ansible-playbook -i inventory.yaml /tmp/pfsense_get.yaml \
  --vault-password-file ~/ansible_key \
  -e 'pfsense_endpoint=firewall/rules'
```

Delete `/tmp/pfsense_get.yaml` after the investigation. Never use Ansible `debug` to print `pfsense_api_key`, export it to the shell, place it on a command line, or write it to a temporary file.

## Useful GET Endpoints

All paths are relative to `https://router/api/v2/`.

### Firewall Rules

```bash
ansible-playbook -i inventory.yaml /tmp/pfsense_get.yaml --vault-password-file ~/ansible_key -e 'pfsense_endpoint=firewall/rules'
```

Returns every rule across all interfaces. Fields worth inspecting per rule:
- `interface` — string or list (e.g. `lan`, `wan`, `openvpn`, `opt3`, `opt4`)
- `type` — `pass`, `block`, `reject`
- `disabled` — filter these out for the active picture
- `ipprotocol` — `inet` (IPv4), `inet6` (IPv6), `inet46` (both). Missing v6 rules are a common gotcha.
- `protocol`, `source`, `destination`, `destination_port`, `descr`

### NAT Port Forwards

```bash
ansible-playbook -i inventory.yaml /tmp/pfsense_get.yaml --vault-password-file ~/ansible_key -e 'pfsense_endpoint=firewall/nat/port_forwards'
```

Key fields per entry:
- `interface`, `ipprotocol`, `protocol`
- `destination`, `destination_port` — what the outside sees
- `target`, `local_port` — where the traffic is DNAT'd to
- `disabled`, `descr`

### Active State Table

```bash
ansible-playbook -i inventory.yaml /tmp/pfsense_get.yaml --vault-password-file ~/ansible_key -e 'pfsense_endpoint=firewall/states?limit=5000'
```

Each entry represents a live flow through pfSense. Fields:
- `interface`, `direction`, `protocol`
- `source`, `destination` (each `ip:port`)
- `state` — e.g. `ESTABLISHED:ESTABLISHED`, `SINGLE:NO_TRAFFIC`, `NO_TRAFFIC:SINGLE`
- `packets_in`/`packets_out`, `bytes_in`/`bytes_out`, `age`, `expires_in`

The state table is gold for confirming whether traffic is reaching pfSense at all. If there's no state for the flow you expect, traffic isn't arriving.

### Firewall Logs

```bash
ansible-playbook -i inventory.yaml /tmp/pfsense_get.yaml --vault-password-file ~/ansible_key -e 'pfsense_endpoint=status/logs/firewall?limit=2000'
```

Returns raw pf log lines. Only **default-deny** and **rules with logging enabled** appear here. A missing log entry doesn't necessarily mean no drop — but combined with an absent state entry it does.

Log line format (pf CSV):

```
Aug 21 18:11:44 router filterlog[3904]: 1,6,,1000000103,igc0,match,block,in,4,0x0,,235,36374,0,none,6,tcp,40,<src_ip>,<dst_ip>,<sport>,<dport>,0,...
```

Key positions:
- Field 5: interface (e.g. `igc0` = WAN, `igc1` = LAN)
- Field 7: action (`block`, `pass`)
- Field 8: direction (`in`, `out`)
- Field 17: protocol (`tcp`, `udp`, `icmp`)
- Fields 19,20: src IP, dst IP
- Fields 21,22: src port, dst port

## Typical Investigation Patterns

### "Is external traffic reaching a service behind pfSense?"

1. Fetch `firewall/nat/port_forwards`, confirm the WAN forward exists and is not disabled
2. Fetch `firewall/states`, grep for the destination port or target IP
   - **State exists, bytes bidirectional** → traffic is flowing, problem is downstream (host, container, app)
   - **State exists, `NO_TRAFFIC` on out side** → DNAT worked, target isn't replying
   - **No state at all** → traffic isn't arriving at pfSense
3. Fetch `status/logs/firewall`, grep for the destination port or source IP → look for explicit blocks

### "Is a client's DNS query being blocked?"

1. Check `firewall/rules` for the source interface — look for `dst=127.0.0.1 dport=53` (the standard DNS redirect NAT) and confirm it's not disabled
2. Check `status/logs/firewall` for `block` entries mentioning port 53 and the client's IP

### "Does the WAN accept IPv6 for a given port?"

pfSense NAT is IPv4-only. IPv6 must be handled by a separate pass rule on WAN with `ipprotocol: inet6`. Grep the rules dump:

```python
import json, sys
d = json.load(sys.stdin)
for r in d['data']:
    if r.get('disabled'): continue
    if r.get('ipprotocol') == 'inet6' and 'wan' in str(r.get('interface')):
        print(r.get('protocol'), r.get('destination_port'), r.get('descr'))
```

If nothing prints, no v6 traffic will get through the WAN.

### JSON Field Gotchas

- `interface` can be a **string** or a **list** — always normalise
- `protocol` and `destination_port` are frequently `None` — guard formatters against it
- Prefer `str(...)` when printing rule fields for consistent output

## When to Escalate

If read-only inspection reveals a rule needs to be added or changed:

1. Report the finding clearly to the user (which rule/NAT, which fields, why)
2. Ask them to make the change in the pfSense UI, or elevate the API key (which is a separate manual step)
3. Do not attempt to bypass the read-only restriction

## Related

- `known-issues.md` covers concrete scenarios where this skill has been used (e.g. wg-easy handshake failure over carrier NAT64).
- The Ansible role tasks `tasks/pfsense_dns_*.yaml` are the only place where write access to the API is (currently) exercised — those use a different code path and don't count as ad-hoc troubleshooting.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.