Repository navigation
Releases: mechubsec/rustjunosmcp
Release list
v0.27.5
rust-junosmcp v0.27.4
Added
- Official MCP Registry listing. The image now carries
io.modelcontextprotocol.server.name="io.github.mechubsec/rustjunosmcp",
which the registry uses to verify image ownership, and the repo ships a
server.jsondescribing the documented stdio container invocation.
Changed
- Dependency and CI updates: distroless
cc-debian13base image digest,
cargo-minor-patchgroup, and pinnedmecmcpreusable workflows. - Docs: stdio container invocation and fwconfigsanitizer references.
rust-junosmcp v0.27.3
rust-junosmcp v0.27.3
v0.27.2
v0.27.1
rustjunosmcp v0.27.0
Security
- Redact device secrets from tool output.
get_junos_configand
junos_config_diffreturned raw device configuration, including Junos
$9$-style reversibly-encrypted values, PSKs, SNMP communities, and
RADIUS/TACACS secrets, straight to the calling model (the README also
wrongly called these values "hashed" — they are symmetrically encrypted
and recoverable with the device's master key). Redaction is now applied
at the two tool-specific call sites and, as a last-mile safety net, in a
singlecall_toolpost-processor covering every tool response, so a
newly added tool cannot silently skip it. Tool descriptions and the
README now say output is redacted (MEC-14).
Added
confirm_commit— new write tool that sends the confirming commit for
a commit-confirmed window opened byload_and_commit_config,
rollback_config, orrender_and_apply_j2_template, cancelling the
pending auto-rollback (MEC-45).
Changed
- Behaviour change: commit-confirmed is on by default.
load_and_commit_config,rollback_config(commit=true),
render_and_apply_j2_template, andapply_junos_change_setnow issue
commit confirmed <window>unless the caller passes
confirm_timeout_mins: 0. Previously these committed directly with no
auto-rollback net unless the caller opted in. The window defaults to the
new--commit-confirm-default-minsflag (default 10 minutes); a per-call
confirm_timeout_minsoverrides it. The opt-out is recorded in the audit
event ascommit_confirmed=false. The response and change-set status both
reportrollback_deadline_unix(MEC-45). --ssh-accept-new-host-keysnow gives real TOFU for NETCONF SSH, not
no-verification-at-all. Previously the flag pinned scp's known_hosts
entries on first contact (HostKeyVerification::AcceptNew) but set NETCONF
SSH toHostKeyVerification::AcceptAll— an operator reading the flag name
had no reason to expect that NETCONF connections were left completely
unverified. NETCONF now usesAcceptNewtoo: an unknown host's key is
pinned on first contact, and a host that later presents a different key
is refused, on both the scp and NETCONF paths, against the same
known_hostsfile (MEC-44).
Behaviour change for anyone relying on the old flag for key rotation:
a device that rotates its host key (reimage, RE swap) will now be refused
on reconnect instead of being silently re-trusted. Re-run
scripts/scan-known-hosts.sh, or delete the stale line from
known_hosts, after a legitimate rotation.- New
--ssh-insecure-accept-any-host-keyflag, lab-only, carries the
old blanketHostKeyVerification::AcceptAllbehavior for NETCONF SSH under
an honestly-named opt-in. Mutually exclusive with
--ssh-accept-new-host-keys. Logged loudly at startup and recorded as an
audit event. - Container images now publish to
ghcr.io/mechubsec/rustjunosmcp—
the repo moved to the mechubsec organization, and images are renamed to
match. Older tags were copied from the previous name.
Fixed
--ssh-insecure-accept-any-host-keynow also disables host-key
verification for scp (transfer_file/upgrade_junos), not just
NETCONF SSH. The MEC-44 landing (above) left scp onAcceptNew(TOFU)
under this flag, so a device presenting a changed key was still refused
over scp despite the flag's name promising to accept any key. Both
transports now share oneSshHostKeyModeend-to-end
(Strict/AcceptNew/AcceptAll), so--ssh-insecure-accept-any-host-key
means the same thing on both paths.srx_flow_sessionsfails closed on two more summary-parsing gaps
(MEC-745): a node reporting no error but also no recognised session-count
element no longer has its missing count silently papered over by the
other nodes' totals, and an early warning-severityrpc-erroron a node
can no longer mask a later error-severity one on the same node. Both
previously risked understating the true session count.
Full changes: v0.26.0...v0.27.0
Backfilled GitHub release for an existing tag (2026-10-07).
rustjunosmcp v0.26.0
Added
-
--allow-direct-commit, off by default.load_and_commit_config, a
committingrender_and_apply_j2_template,rollback_config, and
upgrade_junosstage, validate, and commit a device change in one call with
no change set and no second-principal review. Without the flag, all four
are refused before the device is touched, identically over stdio and HTTP.
With it, the server logs loudly at startup and every call is audited
(direct_commit_allowed=true; a refusal is audited too). See
--allow-direct-commitin the README. -
approve_junos_change_setnow requires a human approver. The mecmcp
dependency'sChangesetCoordinator::approve_change_setgained an
approver_actor_typeargument and refuses anything but
mecmcp_audit::ActorType::Human— an agent or an unattributed caller could
already never propose and approve the same change set, but nothing
previously stopped it from standing in as the second principal. -
get_junos_configgains aformatparameter (textdefault,set,
xml, orjson), rendered device-side via the matching Junos
| display <format>CLI modifier — the same mechanism
execute_junos_commandalready passes through untouched. Output flows
through the same policy check, XML-wrapper stripping, and output-cap
pipeline as the existingtextformat; there is no new unredacted path.
An unrecognizedformatis rejected before any device connection. -
The load tools gain a
modeparameter (mergedefault,replace, or
override) controlling the wireactionfor<load-configuration>:
load_and_commit_config,render_and_apply_j2_template, and the
change-set payload spec (create_junos_change_set'sactions[].payload).
overridereplaces the entire candidate configuration — the highest
blast-radius operation this server exposes — and is refused outright on
load_and_commit_configandrender_and_apply_j2_template, which commit
directly with no second-principal review. It is permitted only through
create_junos_change_set→approve_junos_change_set→
apply_junos_change_set, which requires approval by a principal distinct
from the creator before anything commits (MEC-12).config_format=set
(configuration-set payloads) has no wire-leveloverrideaction in Junos;
that combination is rejected before any RPC is sent regardless of path. -
Raised MSRV to 1.89 (family-wide decision; enables mecmcp to drop aes pin)
-
Two new read-only SRX tools:
srx_flow_sessions,srx_policy_match.
srx_flow_sessionsqueries active flow sessions with hard-capped result
limits and node-aware summaries;srx_policy_matchperforms deterministic
5-tuple policy matching against both zone-pair and global policies,
returning the device's own verdict. Tool count: 37 → 39. -
Four new read-only SRX tools:
srx_list_policies,srx_resolve_address,
srx_resolve_application,srx_list_nat_rules. Security policies by
zone pair (incl. global policies and optional hit counts), address-book and
application/application-set resolution (includingjunos-*predefined
defaults, with recursive nested-set resolution and explicit cycle
rejection), and source/destination/static NAT rules. Address-book and
application resolution are configuration-sourced via a hand-built
subtree-filteredget-configurationRPC. Names on policies and NAT rules
are returned unresolved by design — usesrx_resolve_address/
srx_resolve_applicationto expand them. Tool count: 39 → 43. -
Release image and tarball are now signed keylessly with cosign via
GitHub Actions OIDC (no key pair, ever). TheRelease imageworkflow signs
the pushed image by digest; a newSign release tarballworkflow signs the
LXC tarball once it is attached to a published GitHub release. See the
README's "Verifying the image signature" and "Downloading a prebuilt release
tarball instead" sections forcosign verify/verify-blobrecipes.
Fixed
- cSRX devices now work. rustez 0.18 tolerates cSRX rejecting
<get-route-engine-information/>during fact gathering
(mechubsec/rustez#54). Previously every tool call on a cSRX device failed
with[OperationFailed] syntax error.
Full changes: v0.25.0...v0.26.0
Backfilled GitHub release for an existing tag (2026-10-07).
rustjunosmcp v0.25.0
Added
execute(operation, arguments)is an additive 37th MCP tool over the
existing 36 concrete Junos and SRX operations. It accepts only an exact
concrete operation name; it never repairs names, translates argument keys,
or supplies defaults. The selected concrete handler receives the original
argument object, so its existing typed validation remains authoritative.
Direct calls to all original 36 tools remain supported.
Changed
-
Facade authorization is deliberately dual-scoped. An authenticated HTTP
token must explicitly grant bothexecuteand the selected concrete tool;
wildcard scope does not grantexecute. The same preflight also applies the
caller's device scope to every nested device selector before dispatch. -
executeis conservatively write-capable. It is excluded from wildcard
tool scope, because it can dispatch a write-capable concrete operation. -
Facade audit records retain both layers of attribution. HTTP transport
records the outerexecuterequest, while the concrete handler emits the
correlated operation record with the same request id; malformed and rejected
facade calls remain bounded and do not log raw nested arguments.
Full changes: v0.24.1...v0.25.0
Backfilled GitHub release for an existing tag (2026-10-07).
rustjunosmcp v0.24.1
Changed
- Shipped systemd unit now carries the fleet seccomp posture (mecmcp#354).
Added a comment documenting whySystemCallErrorNumber=EPERMis load-bearing:
without it, systemd's default applies and a denied syscall raises SIGSYS, killing
the process mid-request (mecmcp#351). The comment also clarifies that EPERM denials
are silent at the systemd layer — seccomp return actions have a precedence order
and ERRNO outranks LOG, so such denials can only become visible if the application
stops discarding the errno.
Fixed
- Flaky test race in
timeout_budget_tests. Both tests mutated the same
process-globalCLEANUP_TIMEOUT_SECSand cargo ran them in parallel, so whichever
test lost the race would read the value the other had just reset, causing spurious
failures. Added a static mutex that both tests hold for their entire duration to
serialize them.
Full changes: v0.24.0...v0.24.1
Backfilled GitHub release for an existing tag (2026-10-07).
rustjunosmcp v0.24.0
Added
-
A crashed commit is now re-probed against the device at startup (#370).
commit_operationpersists the caller's attribution -- includingrequest_id-- immediately before
the commit RPC is sent, andformat_attributionputs that same id into the Junos commit comment as
request.id=<uuid>. A process that dies between sending the commit and recording its result therefore
leaves behind a record naming something the device can be asked about. On startup the server now asks:
for every operationChangesetCoordinator::loadleftIndeterminatethat carries an attribution, it
reads the device's commit log and settles the recordCommittedif the id is there.The selection is
Indeterminateplus an attribution, notCommitting.loadrewrites every
non-terminal operation toIndeterminatebefore any sweep could observe it, and attribution is written
nowhere butcommit_operation-- so its presence is what separates a crash during the commit from a
crash during staging, which reaches the same state with no attribution.Only a hit settles anything. Junos keeps a bounded commit history, so an entry can age out, and a
device can simply be unreachable at boot. Neither is evidence the commit did not happen, so both leave
the recordIndeterminateforstate resolve. The sweep never writesFailed.The commit-log hit settles the commit; the lock flag is cleared only when lock freedom was proven.
Finding the id in the log proves the commit landed, but not that the candidate lock was released — the
process may have died between the commit and the unlock. So after finding a hit, the sweep probes lock
freedom by taking and releasing the lock, matching the abandon path.config_lock_heldis cleared only
when that probe succeeds; if the lock cannot be taken, cannot be confirmed returned, or the probe times
out or errors, the record is still settledCommitted(the commit is proven) but the flag remains set
and thedetailsnote that the lock state could not be verified.A confirmed commit is not settled either. Finding the id proves the provisional commit entered the
log, not that its rollback was ever cancelled; settling itCommittedwould make the record terminal,
freeing the device for an apply whose commit would cancel a rollback still armed, and would report as
permanently live a change the device may yet revert. Junos marks these on the entry's header
(commit confirmed, rollback in Nmins), so the match groups the log into entries rather than searching
it flat, and a deadline already on the record is honoured even when the log is ambiguous.Known gap: a settle writes the record directly and emits no SSDF
result_receipt, so a crash after the
apply intent was spooled leaves that evidence chain unterminated even once the device has confirmed the
outcome. Tracked separately.The sweep is detached rather than awaited: a candidate on an unreachable device costs 20s, longer than
the readiness budget the test harness and package smoke allow, so blocking startup on it would turn a
recoverable record into a server killed and retried on every boot. Nothing is lost by serving first --
a non-terminal operation already blocks a new one on its device, so these records keep gating writes
until the sweep settles them. Probing is bounded-concurrent (4 at a time) and the candidate order is
rotated each run, so a prefix of unreachable devices in the coordinator's stable map order cannot
starve a reachable candidate behind them -- either within one sweep or across every restart. 20s per
device, 120s for the sweep. A provisional finding is reported but deliberately not written back: doing so
would mean writingIndeterminateand the old deadline over a record aconfirm_junos_change_setmay
have settled in between, un-confirming a commit the operator did confirm, and mecmcp has no
compare-and-update for operations to make that check atomic. No rollback deadline is invented either: reconstructing one from the device's clock would be
the same kind of unobserved claim the rest of this refuses to make. Because the sweep now runs alongside live traffic, the settle
path re-reads the record and skips it if anything settled it while the probe was in flight; that path
is safe from the remaining window by construction, sincecommittedis only reached for a record that
carried no deadline and so writes exactly what a concurrent confirm would have written. The candidate
rotation is seeded per process rather than from the clock, because a wall-clock modulo is periodic --
60 records restarted daily lands on86400 % 60 == 0and would reselect the same slow prefix forever.Verified on hardware (vSRX 24.4R1.9), not just offline: a real change-set commit was confirmed to stamp
the full untruncatedrequest.id=into the device's commit log; a record flipped toIndeterminatewas
settledCommittedfrom that log after a restart; an id the device had never seen was left untouched;
and an unreachable device timed out at 20s with the record untouched and the server still starting.
Fixed
-
get_junos_change_set_statusnow distinguishes a live commit from one the device reverted (#384).
apply_junos_change_setwithconfirm_timeout_minsissues a Junos confirmed commit, which the
device rolls back unlessconfirm_junos_change_setarrives before the deadline. Status reported
appliedeither way. The cause is whereAppliedis written:apply_change_setsets it as soon as
staging succeeds, before the commit is issued, and nothing rewrites it afterwards -- so the
change-set state is settled before the device has been asked to do anything.The verdict now comes from the linked operation record, reached through
ChangeSetRecord::operation_id:
awaiting_confirmationwhile a rollback deadline is still in the future,presumed_auto_reverted
once it passes with no confirmation recorded,committedonce the operation reachesCommitted, and
discardedwhen one was reconciled bystate resolve.A recorded deadline is the only evidence this server holds that a confirmed commit was issued, so it
decides whether a verdict is owed at all, and it outlives a manual resolve.resolve_persisted_operation
settles the state but leavesrollback_deadline_unixin place, andstate resolve ... AS COMMITTED
does not stop the device: reportingcommittedthere would hide a rollback still counting down.
AS DISCARDEDis an operator saying the change is not on the device, which a timer only reinforces.
AnAS COMMITTEDrecord whose window has since closed reportsindeterminaterather than guessing:
state resolveneither stops the device nor records when it ran, so that shape is either a confirm
made out of band and then reconciled -- the remedy for a confirming commit whose durable write failed
-- or a reconciliation made early on a timer that then fired.committedwould hide a revert and
presumed_auto_revertedwould contradict a human who looked at the device, so the tool reports the
uncertainty and sends the reader to the device.
An operation with no deadline that is notCommittedgets no verdict at all -- an ordinary failed
apply terminalizes asDiscarded, and that is not a confirmed-commit outcome.
The response also carriesoperation_id,operation_stateandrollback_deadline_unixso a reader
can see the basis. All additions are additive -- no existing field or argument name changed.presumed_auto_revertedis a presumption, not an observation. It is the absence of a recorded
confirmation, and it is wrong if a confirming commit was issued out of band on the device, if the
server and device clocks disagree, or ifconfirm_junos_change_setreached the device but its
durable write failed. Only the device settles it. Likewisecommittedcannot separate a confirmed
commit from a plain one, because confirming clears the deadline -- both mean the configuration is
live, which is the distinction that matters.The derivation keys on the deadline rather than on
LifecycleState::Committing, because a restart
rewrites every non-terminal operation toIndeterminatewhile leaving the deadline intact. Since the
Junos rollback belongs to the device rather than the session, that window is still open and still
confirmable; keying onCommittingdropped the deadline from the response at exactly the moment an
operator needed it.
Full changes: v0.23.0...v0.24.0
Backfilled GitHub release for an existing tag (2026-10-07).