palo-alto-bgp-ipsec
# palo-alto-bgp-ipsec
An [MCP](https://modelcontextprotocol.io) server for provisioning **IPsec
tunnels and BGP-over-IPsec peers** on Palo Alto **Panorama**-managed firewalls,
plus a curated set of read/write tools for day-to-day PAN-OS operations.
The provisioning flow is **candidate-only and human-committed**: an LLM builds
and validates the candidate configuration; a human inspects it in the Panorama
UI and performs the commit. A deployment can be **single** (1 tunnel + 1 BGP
peer) or **dual** (2 tunnels + 2 peers for redundancy).
## Highlights
- **Any Panorama.** The server connects to whatever Panorama is configured
via `PANOS_HOST` + `PANOS_API_KEY` (env vars, server-side). No Panorama is
hard-coded; every call works against the connected target.
- **Guided workflow.** Read `panorama_ipsec_bgp_guide` first — it returns a
static playbook (hard rules, tool order, naming conventions) so an LLM
learns the flow without trial-and-error.
- **Minimal human input.** The LLM asks a human only for values it cannot
derive — deployment label, template, peer IP(s), PSK(s), per-peer remote
AS(es), and BGP policy intent. Tunnel units and link-local /30s can be
suggested from a limited live-candidate scope, but the result is not a
global collision guarantee and must be checked against merged/effective
configuration. Serial, virtual router, WAN loopback, and the global BGP
local-as / router-id are resolved from Panorama. See
[`docs/ipsec-bgp-intake-form.md`](docs/ipsec-bgp-intake-form.md).
- **Two transports.** Runs over **stdio** (primary, for local MCP clients) or
**HTTP** (Streamable HTTP + legacy SSE). Both expose an identical tool set.
- **Secret-safe.** PSKs and API keys never appear in a plan, snapshot, hash,
log, URL, or result string. Structured logs are deep-redacted; only slot
names (e.g. `tunnel-1-psk`) are ever logged.
## Install
Requires Node.js **>=20** (`undici@6` matches that runtime); the package
manifest enforces the same minimum.
```bash
git clone https://github.com/yaylaerkan/palo-alto-bgp-ipsec.git
cd palo-alto-bgp-ipsec
npm install
npm run build
```
Then copy `.env.example` to `.env` and fill in `PANOS_HOST` / `PANOS_API_KEY`.
## Configuration
Set the target firewall/Panorama via environment variables (a `.env` file is
loaded automatically):
| Variable | Required | Description |
|----------|----------|-------------|
| `PANOS_HOST` | ✅ | Panorama/firewall IP or hostname (no `https://`) |
| `PANOS_API_KEY` | ✅ | PAN-OS XML API key |
| `PANOS_FIREWALLS_CONFIG` | — | Path to a multi-firewall JSON config (overrides single-host env) |
| `PANOS_PROXY` | — | SOCKS proxy URL for API calls |
| `PANOS_VERSION` | — | Target PAN-OS version hint (default assumes 11.1.x) |
| `PANORAMA_PLAN_TTL_MINUTES` | — | In-memory plan and operation snapshot TTL, integer 1–1440 (default `15`) |
| `LOG_LEVEL` / `PALO_ALTO_BGP_IPSEC_LOG_LEVEL` | — | Winston log level (default `info`) |
| `LOG_DIR` / `PALO_ALTO_BGP_IPSEC_LOG_DIR` | — | Directory for rotating JSON logs (default `logs/`) |
| `PORT` | — | HTTP listen port (default `3001`) |
| `HOST` | — | HTTP bind address (default `0.0.0.0`) |
| `MCP_ALLOWED_HOSTS` | — | Comma-separated `Host` header allowlist for DNS-rebinding protection |
| `CONF_DIR` | — | Directory of mounted JSON config (default `/app/conf`, `/conf`, `./conf`) |
API keys supplied via config are migrated into the OS keychain when available
(`@napi-rs/keyring`); otherwise they stay in the config file.
Binding to a loopback address enables DNS-rebinding protection automatically.
When binding to `0.0.0.0` (the default, and what a container needs), set
`MCP_ALLOWED_HOSTS` to restrict which `Host` headers are accepted.
### Container config mounts
Kubernetes Secrets and ConfigMaps, Docker secrets, and most PaaS config
providers expose their payload as files in a mounted directory rather than as
environment variables. Any of these filenames found in `CONF_DIR` is read as a
flat JSON object and loaded into the environment:
| File | Typical contents |
|------|------------------|
| `secrets.json` | `PANOS_HOST`, `PANOS_API_KEY` |
| `configs.json` | `PORT`, `PANOS_PROXY`, `PANORAMA_PLAN_TTL_MINUTES` |
| `secret.json` | Same shape as `secrets.json`, for providers that use the singular name |
```json
{
"PANOS_HOST": "panorama.example.local",
"PANOS_API_KEY": "..."
}
```
The process loads those files into env at startup **without overwriting**
existing values, so a local `.env` still wins. Mount diagnostics log file
*presence* only (`secrets.json=yes`); secret values are never logged.
## Run
### stdio (primary)
```bash
npm run start:stdio
# or, after global install / npx:
palo-alto-bgp-ipsec
```
Register with a stdio MCP client (e.g. Claude Desktop) by pointing it at the
`palo-alto-bgp-ipsec` binary (`build/stdio.js`).
### HTTP
```bash
npm start # listens on PORT (default 3001)
```
| Endpoint | Transport | Use case |
|----------|-----------|----------|
| `/mcp` | Streamable HTTP | Modern MCP clients, OpenAI Codex |
| `/sse` + `/messages` | Legacy SSE | Cursor, existing integrations |
| `/health` | — | Liveness/readiness probe |
| `/metrics` | — | Prometheus metrics (`mcp_tool_request_count` + default process metrics) |
Each HTTP session gets its own server instance, so one client disconnecting
never tears down another's session.
```bash
codex mcp add palo-alto-bgp-ipsec --url http://localhost:3001/mcp
```
### Repository MCP client
`scripts/mcp-call.mjs` drives the server as a real MCP client over HTTP or
stdio. It can list full schemas, read arguments from a file/stdin, and inject
PSKs from a no-echo TTY prompt or secret stdin without putting secret values in
the process argument list.
> **Multi-step provisioning over stdio.** Plans and operation snapshots are
> process-local and in-memory — each `scripts/mcp-call.mjs --transport stdio`
> invocation spawns a fresh server process, so a plan created in one invocation
> is not visible to a later one (you will get `STALE_PLAN`). Either run the
> whole plan → apply → validate sequence inside a single node process that
> imports the build modules directly (see
> [`scripts/example-provision.mjs`](scripts/example-provision.mjs) for a working
> end-to-end example), or use the HTTP transport with a long-running server so
> the plan persists across calls.
>
> For multi-step provisioning, the HTTP transport (`npm start`, port 3001) is
> the practical choice.
```bash
node scripts/mcp-call.mjs __list__ --schemas
node scripts/mcp-call.mjs --transport stdio __list__ --schemas
node scripts/mcp-call.mjs panorama_preflight_template_network --args-stdin
node scripts/mcp-call.mjs panorama_apply_ipsec_candidate \
--args-file plan.json --secret tunnel-1-psk
printf '%s\n' "$PSK_FROM_SECRET_STORE" | \
node scripts/mcp-call.mjs panorama_apply_ipsec_candidate \
--args-file plan.json --secret-stdin tunnel-1-psk
```
Do not put a `psks` object in positional JSON, `--args`, `--args-file`, or
`--args-stdin`; the repository client rejects inline PSKs. Supply every PSK
slot with `--secret` or `--secret-stdin`. `--args-stdin` and `--secret-stdin`
cannot be combined because both consume stdin. Apply output is sanitized even
when full output is requested.
## Provisioning workflow
Single and dual deployments follow the same sequence — supply 1 or 2
tunnels/peers (the counts must match). See
[`src/tools/panorama-guide.ts`](src/tools/panorama-guide.ts) for the full
playbook (hard rules, naming).
1. `panorama_ipsec_bgp_guide` — read the playbook (static, no API call).
2. `panorama_preflight_template_network` — verify template/serial/VR/network.
Resolution joins direct template and template-stack bindings with live
device state, ignores disconnected candidates, and returns structured
`choices` for `AMBIGUOUS_SERIAL` / `AMBIGUOUS_VIRTUAL_ROUTER` instead of
guessing.
3. Optionally run `panorama_suggest_ipsec_bgp_resources` to propose bare tunnel
units, non-overlapping link-local /30 pairs within the inspected scope, and
the WAN loopback. The result deliberately reports `collisionsChecked: false`,
`scopedCollisionsChecked: true`, `fullyChecked: false`, and a
`collisionScope`; a human must verify merged/effective configuration before
accepting the suggestion.
4. `panorama_plan_ipsec` → `panorama_apply_ipsec_candidate` →
`panorama_validate_ipsec_candidate_v2` (preferred; takes `plan_id` +
`plan_hash`). The legacy `panorama_validate_ipsec_candidate` remains
available and takes the full spec.
> **Note:** if the apply → validate_v2 flow returns `STALE_PLAN`, that is
> expected drift detection, not a bug. Fall back to the legacy
> `panorama_validate_ipsec_candidate`, which takes the full spec.
5. `panorama_plan_bgp` → `panorama_apply_bgp_candidate` →
`panorama_validate_bgp_candidate` (validate takes plan_id + plan_hash).
6. Use `panorama_verify_candidate_operation` with an apply `operationId` to
collect sanitized candidate evidence (`admin`, `dirtyId`, `time`) without
returning configuration XML or key material. `require_dirty` defaults to
`true`; missing, unreadable, or clean expected resources return
`CANDIDATE_EVIDENCE_INCOMPLETE` with `success: false` and `verified: false`.
7. **Human commits** in Panorama, then pushes to devices. Provisioning tools
never commit or push automatically.
8. Optional after commit+push: `panorama_verify_ipsec_runtime` and/or
`panorama_verify_bgp_runtime`.
Key rules: IPsec before BGP; candidate-only (no auto-commit); PSK is a secret
(never in plan/log/hash); named deployment objects use the `${deployment}-`
prefix. PAN-OS requires tunnel interfaces to remain bare `tunnel.<unit>`, so
their ownership boundary is the exact comment marker
`mcp-owner:<deployment>`. Markerless or foreign-marker interfaces are
conflicts and are not updated or removed. BGP global local-as / router-id are
reused from the live device; each BGP peer carries a local-address binding.
Candidate read failures are fail-closed (`CANDIDATE_READ_FAILED`) and do not
become create/update assumptions. APPLY and remove-APPLY reject any plan with
conflicts using `PLAN_HAS_CONFLICTS` before mutation begins.
Plans and operation snapshots are bound to the configured Panorama target by
an API-key-independent SHA-256 identity derived from normalized host, firewall
selector, and TLS verification policy. Reusing an ID against another target
fails with `TARGET_MISMATCH` before candidate reads or writes.
For BGP policy, set `policyMode` explicitly. The core planner recognizes the
explicit stale-owned-rule reconciliation flags `pruneOwnedRules` and its alias
`reconcileOwnedRules`, and the registered `panorama_plan_bgp` MCP schema exposes
both. Either flag requires `policyMode: "none" | "explicit"` and deletes only
stale import/export rules owned by the same deployment. With neither flag,
stale owned rules remain a plan conflict and APPLY is rejected.
Undo before commit: `panorama_remove_bgp_candidate` →
`panorama_remove_ipsec_candidate` (both two-phase: `mode="plan"` then
`mode="apply"`), or `panorama_restore_candidate_operation` with an earlier
`operationId`.
## Tools
**Panorama IPsec/BGP provisioning**
`panorama_ipsec_bgp_guide`, `panorama_preflight_template_network`,
`panorama_detect_ipsec_loopback`, `panorama_suggest_ipsec_bgp_resources`,
`panorama_plan_ipsec`,
`panorama_apply_ipsec_candidate`, `panorama_validate_ipsec_candidate`,
`panorama_validate_ipsec_candidate_v2`,
`panorama_remove_ipsec_candidate`, `panorama_verify_ipsec_runtime`,
`panorama_plan_bgp`, `panorama_apply_bgp_candidate`,
`panorama_validate_bgp_candidate`, `panorama_remove_bgp_candidate`,
`panorama_verify_bgp_runtime`, `panorama_restore_candidate_operation`.
**Provisioning lifecycle status** — `panorama_get_plan_status`,
`panorama_list_active_plans`, `panorama_get_operation_status`,
`panorama_list_active_operations`. These return metadata-only lifecycle views;
they exclude plan/candidate hashes, XPath values, mutation/restore XML, and
secret slot names. Passing `firewall` filters/binds the lookup to that target.
**Commit / push** (explicit, human-driven — not part of the provisioning flow)
`panorama_commit`, `panorama_push_to_devices`.
**System** — `list_firewalls`, `get_firewall_info`, `get_system_resources`,
`get_ha_status`, `get_active_sessions`. `list_firewalls` returns only target
name, host, and TLS verification mode; it never returns API keys.
**Network** — `get_interfaces`, `get_zones`, `get_vlans`, `get_routing_table`,
`get_static_routes`, `delete_static_route`, `get_arp_table`,
`get_dhcp_leases`, `get_dns_proxy`.
**Objects** — address objects/groups, service objects/groups, tags (get and
delete); application filters (get).
**Config** — `get_config`, `panorama_verify_candidate_operation`,
`set_config`, `delete_config`. `get_config` supports parsed JSON or sanitized
XML plus a `metadata_only` mode; PAN-OS `key` and `pre-shared-key` values are
always redacted.
**Logs** — `get_traffic_logs`, `get_threat_logs`, `get_system_logs`,
`get_config_logs`, `get_url_filter_logs`.
## Security model
- **Candidate-only.** No tool commits or pushes automatically; the human owns
the commit. (`panorama_commit` / `panorama_push_to_devices` exist but are
explicit, human-driven actions.)
- **No-touch production.** Named objects use the deployment prefix. Bare
tunnel interfaces use `mcp-owner:<deployment>` in their comment as the
ownership boundary. Markerless/foreign interfaces conflict. Shared
infrastructure (WAN loopback, global BGP router-id/local-as) is read-only and
reused verbatim so its plan step is a no-op.
- **PSK as secret.** Plans carry `PSK_PLACEHOLDER` slots; PSKs are materialized
only at apply time and transit the API as redacted secrets. They are never
logged, hashed, or returned.
Plans and operation snapshots are process-local and expire after
`PANORAMA_PLAN_TTL_MINUTES` (default 15 minutes); they are lost on restart.
Use the four lifecycle status tools to inspect safe active metadata and use
`panorama_verify_candidate_operation` for candidate evidence while the
operation snapshot is active. Status and evidence lookups can return
`TARGET_MISMATCH` when an ID is presented to a different configured target.
## Logging
Structured Winston logging writes to **stderr** (so it never corrupts the
stdio MCP channel) and to a rotating JSON file under `LOG_DIR`
(`palo-alto-bgp-ipsec-YYYY-MM-DD.log`). All records pass through deep redaction that drops
`psk`/`apiKey`/`password`/`secret` keys and keeps PSK slot names only.
## Development
```bash
npm run build # tsc → build/
npm test # vitest
npm run typecheck # tsc --noEmit
```
### Docker
```bash
docker build -t palo-alto-bgp-ipsec .
docker run --rm -p 3001:3001 --env-file .env palo-alto-bgp-ipsec
```
## Disclaimer
This project writes to the **candidate** configuration of the Panorama you point
it at. It never commits or pushes on its own, but `panorama_commit` and
`panorama_push_to_devices` exist as explicit tools — review any candidate in the
Panorama UI before committing. Test against a lab device first. No affiliation
with or endorsement by Palo Alto Networks; PAN-OS and Panorama are trademarks of
Palo Alto Networks, Inc.
## License
[MIT](LICENSE)
TDQS
Scored across 63 tools
Most tools target distinct resources or lifecycle phases, and descriptions are detailed, but several pairs overlap conceptually: validate_ipsec_candidate and _v2, plan_status vs operation_status, and generic set_config/delete_config vs typed resource tools. An agent could misselect without careful reading.
There are two broad conventions: panorama_* lifecycle tools and generic get_/delete_/commit-style tools. Within each group naming is mostly consistent, but mixing remove vs delete, list vs get, commit vs panorama_commit, and _v2 suffixes makes the overall pattern inconsistent.
63 tools is far beyond the 50+ threshold for an extreme count mismatch. Many could be consolidated, especially the status/list/operation metadata helpers, log variants, and duplicate validation versions.
The core IPsec+BGP-over-Panorama lifecycle is well covered: preflight, suggest, plan_ipsec, apply, validate, plan_bgp, apply_bgp, validate, runtime verify, remove, commit, and push. Minor gaps exist for object updates and the duplicate v1/v2 validation path, but generic set_config and delete/add provide workarounds.