Skip to main content

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_read and bounded fortios_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:

  1. read-only accounts enforced by each target platform;
  2. an exact helper section per device for named queries, inventories, metadata/listing roots, and network egress;
  3. host-side egress rules applied before the container starts;
  4. best-effort response redaction and explicit byte pagination;
  5. per-device rate limiting and bounded SSH continuation caching;
  6. 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_status and enrolled-scope inspection through target_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 /tmp and /run noexec, so the image installs a packaged askpass program at a dedicated executable path (/usr/local/bin/netops-askpass, outside those mounts) and points NETOPS_ASKPASS_PROGRAM at 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_read sends exactly the one reviewed command and nothing else. FortiOS sessions still require preverified output 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-rsa host key algorithm and SHA-1 key exchange for every device. A device which offers only ssh-rsa needs the named per-device exception legacy_ssh: "rsa-sha1" in its inventory entry, and a classic Cisco IOS device which also offers only SHA-1 key exchange needs legacy_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_hosts file 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: false with device_cli_error, even when SSH returns zero; valid EXOS output with status 250 remains available. See SSH results.
  • Mandatory audit writes a durable started record 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.

  1. Download and verify the same release on the proxy host and the runner, as in Installation.

  2. Create dedicated target accounts and independently test both allowed reads and denied configuration, export, maintenance, and shell actions. Follow Read-only accounts.

  3. Create the four operator files: vault.json with mode 600, inventory.json with one entry per device, egress-policy.json, and runner.json. Follow the four operator files and credentials and protocol use. Name a separate snmp_credential only for devices that need SNMP. Write the host key fingerprint of the runner and of every device into those files. Then run python3 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.

  4. 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
    
  5. 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
    
  6. 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=ok and egress_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.

  7. Start the already-created service and confirm its state:

    docker compose start
    docker compose ps
    
  8. Configure the proxy as a stdio MCP server in a dedicated read-only profile of any compatible client: the command netops-helper-proxy when the component is installed, or scripts/remote_mcp_proxy.py when it is run from an unpacked archive. The proxy needs netops_core and netops_helper on its path; see Installation. Start a fresh session, call helper_status, inspect target_scope for 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)

Source distribution for netops-helper 0.3.8
File Size Uploaded
netops_helper-0.3.8.tar.gz 226.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for netops-helper 0.3.8
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.3.8 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page