NetOps Helper
The current release is netops-helper/v0.3.8 (2026-10-06), which pins netops-core==0.2.6 and vendors src/netops_core inside its own release archive. The repository release table links the current release of every component.
Install from the release assets. The installation guide downloads the source archives of this component and of the pinned netops-core from their release pages and verifies them against the release SHA256SUMS and its Sigstore bundle; the server runs as a container image built from that archive, or loaded from the published OCI archive, on a separate runner host. netops-helper has a separate PyPI publication step after GitHub; check exact-version index availability before choosing that installation channel. The package is prepared as the client side only: once uploaded, installing the package with pip would put only the stdio proxy netops-helper-proxy that an MCP client launches on the host, with the pinned netops-core, never the server.
NetOps Helper provides bounded MCP troubleshooting reads, optional measured FortiOS configuration-path reads and separately enrolled finite diagnostic recipes. Ordinary named queries expose no arbitrary CLI or configuration export. schema_read reads a full snapshot in memory and returns only enrolled, redacted attributes. fortios_diagnostics can change temporary diagnostic options/debug state and must verify cleanup; it does not change persistent configuration. Both extensions require explicit grants, exact live identity and independent server-owned schema binding.
Operators address explicitly enrolled devices by name. A local stdio proxy validates the device's helper section of the shared inventory, injects that one device's credential after the MCP client boundary, transports the request over SSH to a host-key-pinned runner, and invokes an isolated container there. Device output keeps identifiers needed for correlation while recognized secrets are removed on a best-effort basis.
This is a self-hosted community project for experienced operators and security reviewers. It is not an enterprise orchestrator, a replacement for device-side authorization, or proof that a diagnostic conclusion is correct.
New in 0.3.8
- Optional
schema_readand boundedfortios_diagnostics, with separate path/address/interface/context grants. - FTPS exact leaf pinning on both control and data channels before listing bytes are read; an aggregate FTP control-response budget.
- Fail-closed proxy response handling/redaction, bounded pending requests and deadlines, and correct application MCP version.
- Egress bundle schema 4 with a host INPUT guard; regenerate older bundles and measure it on the deployment host.
- Python 3.14.8 and locked PyJWT 2.15.0; archive installation stops on failed signature or checksum verification.
See the issue resolution table, tool reference, security model, candidate validation and known vulnerabilities. Known OpenSSH and remaining Python findings are disclosed, not claimed fixed.
Architecture
dedicated read-only agent/session
-> local stdio proxy: device discovery, section/egress checks, credential injection
-> pinned SSH transport (`ssh -T`; runner password or key from the credential store)
-> fixed `docker exec -i netops-helper python -m netops_helper.server`
-> isolated phase-1 read-only MCP server
-> device account with externally enforced read-only permissions
The required order of controls is:
- read-only accounts enforced by each target platform;
- an exact
helpersection per device for named queries, inventories, metadata/listing roots, and network egress; - host-side egress rules applied before the container starts;
- best-effort response redaction and explicit byte pagination;
- per-device rate limiting and bounded SSH continuation caching;
- a server without persistent configuration-write tools; optional diagnostic recipes have bounded temporary runtime effects and verified cleanup.
Capabilities
The remote FastMCP server registers exactly 12 tools: two control-plane tools and ten device tools. The local proxy adds target_scope, so a client sees exactly 13 tools: three control-plane tools and ten device tools.
- Device discovery through
helper_statusand enrolled-scope inspection throughtarget_scope. - DNS, TCP, ICMP, and certificate-verifying TLS diagnostics.
- Named SSH troubleshooting queries for FortiOS, Extreme Switch Engine, Cisco IOS, IOS-XE and NX-OS, Arista EOS, Junos, Linux, and Ruckus Unleashed.
- Opt-in ARP/neighbor, MAC/FDB, and LLDP/CDP queries where a reviewed platform command exists.
- Typed parameters selected from per-device interface, service, address, software-switch, VLAN, managed-switch and certificate inventories.
- Opt-in VLAN detail, DHCP snooping, certificate metadata and managed-switch status, PoE, MAC, stacking and LLDP through the FortiGate controller; see measured limits.
- SNMPv2c GET with a dedicated community record that is never the device secret.
- SFTP metadata under per-device non-root paths; no remote file body download.
- FTPS directory listing and explicitly acknowledged read-only plain FTP listing.
- Explicit pagination metadata and a stable content digest for long SSH output.
target_scope does not return the device address, the login, any credential name, the secret, the community, or the host key pin. It intentionally returns enrolled inventories, SFTP metadata/listing roots, and egress addresses; this can reveal target addressing and other operational topology. Treat it as credential-free but environment-sensitive data.
See Tool reference, Read-only accounts, Configuration, Onboarding, and Installation.
Deliberate non-capabilities
The ordinary SSH catalogue has no configuration-export query. Optional schema_read collects a full snapshot in memory and returns only explicitly granted measured paths, with known credential attributes redacted. There is no whole-snapshot export, generic HTTP body or remote file-content tool. Persistent configuration writes, uploads, deletion, restart/reboot, software installation, arbitrary shell, raw CLI input and autonomous target expansion remain outside the interface. Bounded diagnostics may set temporary options/debug state; explicit grants and verified cleanup are part of that separate boundary.
There is no general device-log browser. The only deliberately log-oriented query is the fixed, opt-in, one-hour Linux service journal query. Some fixed status/history diagnostics may contain event-like output, but the client cannot select arbitrary device logs, time ranges, filters, or files.
The measured extension is described under Measured FortiOS configuration objects. Its explicit grants do not enable a whole-snapshot export or a generic body-read escape hatch.
Security properties
- The helper exposes no listening port; MCP uses SSH-tunneled stdio.
- The container runs non-root with a read-only root filesystem, no Linux capabilities,
no-new-privileges, resource limits, and no Docker socket. - The Compose file mounts
/tmpand/runnoexec, so the image installs a packaged askpass program at a dedicated executable path (/usr/local/bin/netops-askpass, outside those mounts) and pointsNETOPS_ASKPASS_PROGRAMat it; without it, password authentication would have nowhere it is allowed to execute an askpass helper. - Every device must declare
account_role: "read-only"in its helper section; verify actual device-side permissions over the same access path. Snapshot collection and temporary diagnostic controls require separate permission checks before enrollment; the declaration is not proof of those permissions. - Every request is checked against the exact helper section and per-tool egress scope before a credential is forwarded.
- No platform in the query catalogue sends a paging preamble any more;
ssh_readsends exactly the one reviewed command and nothing else. FortiOS sessions still require preverifiedoutput standard, because FortiOS itself pages and the helper never writes into device configuration to turn that off. - SSH-family reads inherit
netops-core's bounded receive: a device that keeps sending past the capture budget is killed and the call is refused with nothing of what it sent returned. This runs ahead of, and independently from, the helper's own later 2 MB snapshot cap on the decoded output. - SSH and SFTP refuse the
ssh-rsahost key algorithm and SHA-1 key exchange for every device. A device which offers onlyssh-rsaneeds the named per-device exceptionlegacy_ssh: "rsa-sha1"in its inventory entry, and a classic Cisco IOS device which also offers only SHA-1 key exchange needslegacy_ssh: "rsa-sha1-dh14"; there is no global switch and no algorithm list in configuration. See Legacy SSH algorithms. - Host key trust is a pin in the inventory, verified on the server before any credential is used, and the runner is pinned the same way; there is no
known_hostsfile and no first-use acceptance. - The client supplies query names and typed parameters, never raw commands.
- Inventory-bound slots prevent device output from becoming a new command argument or expanding target scope.
- IP, IPv6, MAC, hostname, username, email, and serial values remain visible because troubleshooting requires correlation.
- Injected credentials and recognized secret forms are redacted on a best-effort basis; policy and remote permissions must keep secret-bearing data out of scope.
- Every device response is marked as untrusted data and must run in a dedicated read-only agent/session.
- Proxy and transport failures are reported by a fixed classified category (for example
ssh_host_key,auth_material,rate_limit) and a fixed public message, never the device's or the SSH client's own words; raw stderr is sanitized before use and never relayed. - Recognized CLI refusals of FortiOS, EXOS, Cisco IOS, IOS-XE and NX-OS, Arista EOS and Junos return
ok: falsewithdevice_cli_error, even when SSH returns zero; valid EXOS output with status 250 remains available. See SSH results. - Mandatory audit writes a durable
startedrecord before a device operation and a terminal record afterward; interrupted attempts can remain visibly incomplete. - Audit JSONL contains allowlisted metadata only and rotates into five 2 MB segments.
Read Security model, Egress control, Security policy, and the release-specific known vulnerability findings before deployment.
This release ships Debian trixie's openssh-client with one Critical finding Debian marks wont-fix (CVE-2026-60002; fixed upstream in OpenSSH 10.4, which trixie does not ship), the only entry the release gate ignores in this image. It is a reviewed, dated exception, not a fix; the findings document says what it exposes, what contains it, what to do if that is not acceptable, and why two further openssh-client entries the scanner reports as High are sshd code this image does not carry.
Requirements
- A Linux ARM64 runner with Docker Engine and Compose v2.
- A local MCP client host with Python 3.13+, OpenSSH, and the host key fingerprints of the runner and of every device.
- Dedicated device identities whose persistent configuration-write restrictions are enforced on the targets; validate each optional schema/diagnostic permission separately.
- A local inventory, credential store, egress policy, and runner file that are never shipped with the source or container image.
- A dedicated agent/session without shell, write-capable file, deployment, or mutating MCP tools.
- An out-of-band recovery path while applying host firewall rules.
Quick start
This sequence deliberately creates the Compose network and container in a stopped state. Do not start the helper until the generated egress contract has been reviewed, applied, and checked.
-
Download and verify the same release on the proxy host and the runner, as in Installation.
-
Create dedicated target accounts and independently test both allowed reads and denied configuration, export, maintenance, and shell actions. Follow Read-only accounts.
-
Create the four operator files:
vault.jsonwith mode600,inventory.jsonwith one entry per device,egress-policy.json, andrunner.json. Follow the four operator files and credentials and protocol use. Name a separatesnmp_credentialonly for devices that need SNMP. Write the host key fingerprint of the runner and of every device into those files. Then runpython3 scripts/check_operator_config.py, a read-only preflight validator that reads only those four files and never contacts a device or opens a network connection, to validate all four before continuing - see Onboarding for the guided walkthrough of this whole sequence and for migrating an older configuration. -
On the runner, build the image and create the network and container without starting the service:
docker compose build --pull=false docker compose up --no-start --no-build docker compose ps --all docker network inspect netops-helper
-
On the proxy host, generate a mode-
600, secret-free but topology-sensitive bundle from file paths:python3 scripts/generate_egress_rules.py \ --inventory /path/to/inventory.json \ --policy /path/to/egress-policy.json \ --output /restricted/path/netops-helper-egress.json
-
Transfer the bundle through a host-key-verified channel if proxy and runner differ. On the runner, compare its SHA-256 digest with the sender, inspect its manifest and rules, retain out-of-band recovery, then explicitly apply and check it:
sudo python3 scripts/apply_egress_rules.py \ --bundle /restricted/path/netops-helper-egress.json --apply sudo python3 scripts/check_egress_rules.py \ --expected /restricted/path/netops-helper-egress.json
Continue only after
egress_apply=okandegress_check=ok. The bundle also installs the host INPUT guard; before relying on it, run the live checks in Egress control, among them that the runner's own services time out from the container through every runner address. -
Start the already-created service and confirm its state:
docker compose start docker compose ps
-
Configure the proxy as a stdio MCP server in a dedicated read-only profile of any compatible client: the command
netops-helper-proxywhen the component is installed, orscripts/remote_mcp_proxy.pywhen it is run from an unpacked archive. The proxy needsnetops_coreandnetops_helperon its path; see Installation. Start a fresh session, callhelper_status, inspecttarget_scopefor one listed device, and test one harmless enrolled query against a controlled test target.
A device credential reuses its login and secret across SSH, SFTP, FTPS, and plain FTP; plain FTP transmits them without encryption. SNMPv2c sends its separate community in plaintext at the protocol layer. The stock image validates public trust; tls_probe and system-trust FTPS will normally reject private-CA or self-signed devices until a private image contains an independently verified trust anchor and the device certificate has a matching SAN. Follow the credential and private CA and FTPS pin procedures; verification must not be disabled.
The Compose network has internal: false so diagnostics can reach targets. Bundle schema 4 installs one IPv4 iptables ruleset: a DOCKER-USER chain for traffic forwarded from the bridge, and a guard as the first INPUT rule that accepts only RELATED,ESTABLISHED packets from the bridge and drops every new connection to the runner itself. A schema 3 bundle of 0.3.7 or earlier, which has no INPUT guard, is refused as bundle_schema_outdated; regenerate it. IPv6 is disabled on this Docker network with enable_ipv6: false; no ip6tables protection is claimed. DNS that the Docker daemon forwards for the container leaves from the host's own network stack, which neither chain sees. Treat egress as constrained only after the live checks in Egress control.
Development
Use Python 3.14.8 to match the shipped container and Helper CI, install the locked dependencies and pytest in a maintained development environment, then run:
python -m pytest -q
PYTHONPATH=src:../netops-core/src python tests/run_tests.py
python scripts/check_public_release.py
The base image is digest-pinned and runtime dependencies are hash-locked, but the image is not fully reproducible: the one distribution package it installs, openssh-client, is deliberately left unpinned so a rebuild keeps receiving its security updates - see Reproducibility of the image. Release metadata includes CycloneDX SBOM data. A clean test run is necessary but not sufficient; review the source diff, effective target permissions, complete vulnerability report, license inventory, egress behavior on the actual ARM64 runner, and residual risks.
License
MIT. See LICENSE.
Measured FortiOS configuration objects
The optional schema_read tool reads one measured configuration path. Its show view renders the selected object's configured attributes; its get view returns structured attributes from the live full-configuration snapshot. Neither view infers defaults or collects runtime state outside that snapshot. VDOM names and all parent table keys remain separate; optional vdom and owners selectors narrow the result. Device data remains explicitly untrusted and paginated.
Enable it only for a target enrolled with an externally enforced read-only account. Add exact paths to helper.read_inventory.schema_paths (up to 4096 unique paths, at most 128 characters each) and enable system_status. The client proxy checks that grant. The server also requires NETOPS_SCHEMA_REGISTRY, pointing to an operator-managed format-1 JSON file:
{"format":1,"targets":{"example-device":{"schema":"libraries/example.json","sha256":"REPLACE_WITH_SHA256","host_key_fingerprint":"REPLACE_WITH_HOST_KEY_PIN"}}}
Schema filenames are relative to the registry and cannot traverse to a parent directory. Keep the registry and libraries read-only in the server deployment. This is additional operator-managed configuration; the default deployment has no schema grants. The registry pins both the library content and the target SSH host key. Each fresh read verifies get system status against the library's exact hardware model, version and build before reading the configuration. A mismatch, absent grant, invalid registry or truncated snapshot refuses the operation.
Known credential attributes and authentication secrets are redacted. The tool reads a complete snapshot in memory but returns only the enrolled path's own attributes, excluding child objects. It does not persist the snapshot. Pagination uses the existing bounded short-lived memory cache and audit preflight/completion.
Optional bounded FortiOS diagnostics
fortios_diagnostics provides separately enrolled ping, traceroute, ICMP header capture, filtered sessions and flow recipes with time/count/output limits and verified cleanup. It requires exact measured VM hardware/build, a server-owned schema registry and independent scope grants. See tool reference for the VDOM check, shared debug timer and remaining measurement limits.
Metadata
Release files for netops-helper 0.3.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| netops_helper-0.3.8.tar.gz | 226.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| netops_helper-0.3.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 314.1 kB
Release files / netops_helper-0.3.8.tar.gz
| Download URL | netops_helper-0.3.8.tar.gz |
|---|---|
| Size | 226.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
63f02c3f45ae2528ae21fb6c88682c11763b54a981bb646c4b6b031692b09c9d
|
|
BLAKE2b-256 checksum How to use checksums |
29ec8bc3501020442cd87b6467a044ed29d1e9117adae7e668c72d8103954dd2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / netops_helper-0.3.8-py3-none-any.whl
| Download URL | netops_helper-0.3.8-py3-none-any.whl |
|---|---|
| Size | 87.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0154aa82338429fe1ae3998ec52eca5fdbdc836efa98f20a5f8f8c426bcd94a4
|
|
BLAKE2b-256 checksum How to use checksums |
fd615ec2508d6ecc07503e3b101f53ce1b4fe160c7e471bfa4ffe102d7d760e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log