9. Ring Remote View reference#
Who this is for. Plant IT, integrators adding a site, a security reviewer checking the package's claims against its code, and a future developer who needs an exact value without re-reading every script.
Not a walkthrough. For the guided install see installer/07 — Ring Remote View (v1, LAN) and installer/07a — off-site (v2, Cloudflare Tunnel). For what an operator sees, see operator/10 — Remote viewing; for where this sits in the plant network, see reference/02 §2.4.
Ring Remote View is a separate, optional package — a pinned, hardened
MeshCentral server plus a portable
Node runtime, installed into C:\Ring\RemoteView\. It mirrors the one live
Ring HMI session; it never starts a second Ring, never opens a second PLC
connection, and no Ring file is touched by anything in it. Every table below
comes from the package's own scripts and config template, the two runbooks,
and a bench verification pass run 2026-09-04 — where that pass found
something a runbook got wrong, or left unproven, this chapter says so.
9.1 Pinned constants#
| Item | Value |
|---|---|
| Product display name | Ring Remote View (a wrapper name — MeshCentral itself is not rebranded) |
| MeshCentral version | 1.2.4 (Apache-2.0, engines.node >= 20) |
| Node runtime | Node 24 LTS ("Krypton"), win-x64, portable — bundled, never installed machine-wide. Node 20 left maintenance 2026-04-30; the build refuses a line already past end-of-life. Node 24's own EOL: 2028-04-30. |
| Install root | C:\Ring\RemoteView\ |
| HTTPS port (web UI) | 8443 |
| MPS port | 4433 — Intel AMT presence-server port, bound to 127.0.0.1, no inbound rule. See §9.2. |
| HTTP redirect port | disabled (redirPort: 0 — no plaintext listener exists) |
| Server service — display name | MeshCentral |
| Server service — real service name | meshcentral.exe, a node-windows daemon at <root>\meshcentral\WinService\daemon\meshcentral.exe. The SCM service name is the exe name, not "MeshCentral." Bench-confirmed 2026-09-04. |
| Agent service — display name | Mesh Agent |
| Agent service — binary | C:\Program Files\Mesh Agent\MeshAgent.exe |
| Device group | Plant HMI |
| Device group features | 4 (Record Sessions) |
| Device group consent | 9 (1 desktop notify + 8 desktop prompt) |
| Control mask | 12586504 |
| View-only mask | 12586760 |
test\Verify-RemoteViewStatic.ps1 asserts every value above across every
script and the config template — see §9.7.
v2 pinned constants (Cloudflare Tunnel, optional)#
| Item | Value |
|---|---|
| cloudflared version (pinned) | 2026.7.3 |
| cloudflared asset | cloudflared-windows-amd64.exe |
| cloudflared SHA256 (pinned) | 8635DA433B6DF8194746E88ED9D2589566C20E38BFC2A80E431A348B7C765841 |
| cloudflared licence | Apache-2.0 |
| Windows service | cloudflared, Automatic start, --no-autoupdate pinned into the ImagePath |
| Binary location | C:\Ring\RemoteView\cloudflared\cloudflared.exe |
| Outbound ports | TCP 443 (required), UDP 7844 (QUIC, optional) |
| Inbound ports added | none, on any NIC — asserted by the installer |
9.2 Listeners and firewall#
What actually listens (bench-observed, 2026-09-04)#
| Port | Observed |
|---|---|
| 8443 | Listening on 0.0.0.0:8443 and [::]:8443, TLS 1.3. Plain HTTP gets no response at all. |
| 4433 (MPS) | No socket at all, not merely "bound to loopback." MeshCentral 1.2.4's mpsserver.js line 42: if ((args.lanonly != true) && (args.mpsport !== 0)) — in LAN-only mode (always on here) the AMT MPS listener never starts. The runbooks' "bound to 127.0.0.1" is conservative; the bench truth is "not listening." |
| Plant/PLC NIC | Nothing inbound, ever. |
Inbound firewall rules#
| Rule | Created by | Scope | Purpose |
|---|---|---|---|
Ring Remote View (HTTPS 8443) |
New-RemoteViewFirewallRule.ps1 (installer calls it unless -NoFirewall) |
TCP 8443 inbound, RemoteAddress = LocalSubnet, Profile = Domain,Private (never Public/Any), optionally one NIC via -InterfaceAlias |
The only inbound surface this package adds on purpose |
Mesh Agent WebRTC Traffic |
The Mesh Agent's own installer (meshagent64 -fullinstall) — not created or documented by this package or either runbook. Bench finding, 2026-09-04. |
Allow, UDP, any port, program MeshAgent.exe, profiles Domain+Private+Public |
Unused (webRTC: false always), but present on every install with the local agent. Disable with Disable-NetFirewallRule -DisplayName 'Mesh Agent WebRTC Traffic', or accept it explicitly. Removed automatically by MeshAgent.exe -fulluninstall — bench-confirmed gone after uninstall. |
No rule is ever created for MPS 4433.
Dual-homed:
LocalSubnetalone is not enough if the plant subnet is also "local" — bind the rule to the office NIC with-InterfaceAlias; the installer requires both-LanCidrand-InterfaceAliastogether on a multi-homed box and asserts the rule's interface filter matches. Profile: the rule isDomain,Privateonly — if the NIC'sGet-NetConnectionProfilecategory is Public, the rule does nothing and 8443 is unreachable with no error (the installer warns in its summary). The 2026-09-04 bench run hit exactly this and used-NoFirewallinstead, since the lab's only NIC was Public.
v2 is outbound-only. cloudflared dials out on TCP 443 (and UDP 7844 for
QUIC). The v2 installer creates no inbound rule and asserts that none exists
referencing cloudflared.
9.3 Files and folders on the HMI PC#
C:\Ring\RemoteView\
node\ portable Node 24 LTS "Krypton" (not machine-wide)
meshcentral\ MeshCentral 1.2.4 (agents\ pruned to Windows x64)
meshcentral-data\ JUNCTION -> C:\Ring\RemoteView\meshcentral-data\
meshcentral-recordings\ JUNCTION -> C:\Ring\RemoteView\meshcentral-recordings\
meshcentral-data\ real folder: config.json, certificates, database
meshcentral-recordings\ real folder: session .mcrec recordings
manifest.json the payload SBOM
payload-inventory.json path + size + SHA256 of every payload file
cloudflared\ v2 only — see below
The junctions are load-bearing — do not delete them. MeshCentral resolves
its data folder relative to the package location, and winservice.js
forwards only user/port/aliasport/mpsport/mpsaliasport/redirport/exactport/debug
to the Windows service — not --datapath. Deletes in this package are
junction-aware: a link is unlinked first, so a recursive delete can never
follow it into real data.
| Item | Detail |
|---|---|
| Certificates | meshcentral-data\ holds root / webserver / agentserver / codesign / mps cert+key pairs, generated by MeshCentral itself on first start (no synchronous --cert step). webserver-cert-public.crt's subject CN is the rendered certificate name — see §9.5 — not the product name and not the bare NetBIOS name. |
| Recordings | Capped 90 days / 500 files / 20,480 MB, scoped to Plant HMI only, indexed, protocol 2 (desktop) only. Evidence — fold into the plant's backup/retention policy rather than pruning ad hoc. |
| Shortcuts | ...\Start Menu\Programs\Ring Remote View.url and C:\Users\Public\Desktop\Ring Remote View.url, both to https://<hostname>:8443/. |
v2: cloudflared\ |
cloudflared.exe (pinned, SHA256-verified) and install-state.json (version, SHA256, hostname, patched-key list, edge evidence, a non-reversible token fingerprint — never the token itself). |
| v2: config backups | meshcentral-data\config.json.bak-<yyyyMMddHHmmss>, written before every v2 patch. Never pruned. |
Rollback deny-list. C:\Ring\Icons, C:\Ring\Pictures, C:\Ring\App and
C:\Ring itself can never be deleted by a rollback — only artefacts this
run created (install root, junctions, config.json, both services, the
firewall rule, the shortcuts, and any accounts it created) are ever undone.
9.4 Accounts and rights#
The rule the design depends on: a MeshCentral site administrator
bypasses every device-group rights mask entirely. So every operator is
created with --rights none — zero server rights — and access is
granted only on the Plant HMI device group, per account, by numeric mask.
The admin account stays with plant management, never handed to an operator.
| Account type | Server rights | Device-group mask | Can drive? | Terminal/files/registry/software |
|---|---|---|---|---|
| Site administrator | Full (siteadmin = 4294967295, i.e. 0xFFFFFFFF) |
n/a — bypasses masks | yes | yes — exactly why this never goes to an operator |
| Operator, control | none |
12586504 | yes | denied |
| Operator, view-only (default) | none |
12586760 | no — input discarded | denied |
8 REMOTECONTROL
512 NOTERMINAL
1024 NOFILES
2048 NOAMT
4194304 NOREGISTRY
8388608 NOSOFTWARE
--------
12586504 control mask
+256 REMOTEVIEWONLY
--------
12586760 view-only mask
Bit values are the MESHRIGHT_* constants in meshuser.js @ 1.2.4,
lines 40–64. MeshCtrl flag names map to the same bits in meshctrl.js
@ 1.2.4, AddUserToDeviceGroup, lines 1648–1665: control =
--remotecontrol --noterminal --nofiles --noamt --noregistry --nosoftware;
view-only = the same plus --desktopviewonly (the flag for the 256 bit
— --viewonly is not a real flag).
Erratum, corrected on purpose. An earlier brief quoted 12587016 /
12587272 — the same flags, mis-added by exactly 512. This package ships
the correct masks above, and the static gate fails the build if either
superseded literal reappears anywhere.
What MeshCtrl actually shows (ListUsersOfDeviceGroup, bench-observed):
ringadmin → FullAdministrator; jbaker (control) →
RemoteControl, NoTerminal, NoFiles, NoAMT; shiftlead (view-only) →
RemoteControl, RemoteViewOnly, NoTerminal, NoFiles, NoAMT. Note the
gap: MeshCtrl's text view never names the NoRegistry/NoSoftware bits —
only the numeric mask read-back (12586504/12586760) confirms them.
Enforced server-side, not just a hidden tab. The 2026-09-04 pen-test pass (§9.7) confirmed the relay itself refuses to route a restricted account to the agent for terminal, file and command requests.
2FA is domain-wide, not per-account. force2factor: true applies to
every account — MeshCentral 1.2.4 has no per-account 2FA setting. It is
enforced post-login: a successful password login is signalled to force
2FA enrolment afterward, not refused outright (webserver.js, feature flag
0x00040000). skip2factor is deliberately absent.
9.5 Configuration keys as rendered by the installer#
Rendered by Install-RemoteView.ps1 from config-template.json into
meshcentral-data\config.json. <TOKEN> = a placeholder.
settings.*#
| Key | Rendered value | Why |
|---|---|---|
cert |
<HOSTNAME> — lowercase, WITH A DOT: the FQDN if DNS gives one, else <hostname>.local |
MeshCentral 1.2.4 only offers 2FA enrolment in the UI when certificates.CommonName contains a dot (webserver.js, feature flag 0x00001000). A bare NetBIOS name never has one, so nobody could ever enrol while force2factor blocked every feature until they did — a dead lock. Fixed on branch docs/remote-access-guide; bench-rendered as laptop-t8d4hur8.local. |
npmpath |
absolute path to the bundled node\node_modules\npm\bin\npm-cli.js |
Belt-and-suspenders: if MeshCentral ever falls to its startup module-install path it must use the bundled npm, never a missing machine-wide one. |
LANonly |
true |
No tunnel, no internet exposure by default. Also makes the served UI build WebSocket URLs from window.location.hostname, not the cert CN — load-bearing for v2, not just a v1 default. |
port / redirPort |
<HTTPS_PORT> (8443) / 0 |
The web UI port; no plaintext listener exists at all. |
mpsPort / mpsPortBind |
4433 / "127.0.0.1" |
See §9.1/§9.2 — never reachable off-box; in LAN-only mode never even opens a socket. |
exactPorts |
true |
Refuses to silently walk to another port on a collision — loud failure, not a surprise listener. Also required by v2: cloudflared's origin hard-codes 127.0.0.1:8443. |
userAllowedIP / agentAllowedIP |
<LAN_CIDR>,127.0.0.1 |
The source-IP gate — set again under domains[""] below; both are independent and must move together. |
selfUpdate / noAgentUpdate |
false / 1 |
A validated HMI PC does not silently take new server or agent code. |
allowLoginToken |
false |
No login-token bypass of password/2FA. |
allowFraming |
false |
The UI is never framed by another page. |
webRTC |
false |
This is why the "Mesh Agent WebRTC Traffic" rule (§9.2) is unused even though it exists. |
plugins.enabled |
false |
No plugin system. |
maxInvalidLogin / maxInvalid2fa |
{time:10, count:5, coolofftime:10} each |
Brute-force throttle on password and 2FA attempts. |
domains[""].*#
| Key | Rendered value | Why |
|---|---|---|
newAccounts |
false |
Self-registration off. Bench-confirmed: /createaccount, /register, /signup all HTTP 404, no form. |
newAccountsRights |
notools, nonewgroups, nonewdevices, locksettings |
Dormant while newAccounts is false — documents intent if ever flipped. |
guestDeviceSharing, agentSelfGuestSharing, allowSavingDeviceCredentials, ipkvm, mstsc, novnc, ssh, myServer, agentInviteCodes, localSessionRecording |
all false |
Every unused feature is off, not merely unused. |
userAllowedIP / agentAllowedIP |
<LAN_CIDR>,127.0.0.1 |
The second, independent copy — webserver.js evaluates the global and per-domain lists separately. |
allowedOrigin |
<ALLOWED_ORIGINS> — lowercase array: bare hostname, certificate name, localhost, 127.0.0.1, primary LAN IPv4 |
MeshCentral closes /control.ashx with cause invalidorigin unless the browser's Origin hostname is listed here — or, with no list, unless it equals the certificate CommonName case-sensitively, which a lowercase browser Origin never matches on an uppercase Windows machine name. This was a real, bench-observed blocking defect before the fix — see §9.8. |
userSessionIdleTimeout / logoutOnIdleSessionTimeout |
30 / true |
Idle browser sessions log out. |
clipboardGet / clipboardSet |
false / false |
No clipboard bridging between browser and HMI desktop. |
passwordRequirements.min/upper/lower/numeric/nonalpha |
12/1/1/1/1 |
Password complexity floor. |
passwordRequirements.banCommonPasswords / oldPasswordBan |
true / 5 |
Common-password ban (triggers the bundled wildleek module); no reusing the last five. |
passwordRequirements.force2factor |
true |
Domain-wide, post-login — see §9.4. |
passwordRequirements.otp2factor / backupcode2factor |
true / true |
The only two 2FA methods enabled — a plant LAN has no SMTP/SMS/Duo reachability, so any other method would offer enrolment paths that can never complete (all left false). |
passwordRequirements.skip2factor |
deliberately absent | Its presence would let an IP range bypass 2FA entirely. |
desktop.viewonly / disableconnectall |
false / true |
Control is granted per account via the rights mask, never domain-wide; no bulk "connect to all" action. |
terminal.sshConnect / files.sftpConnect |
false / false |
No SSH/SFTP bridging on top of the already-denied tabs. |
userConsentFlags.desktopnotify / desktopprompt |
true / true |
Matches the device-group numeric consent value 9 (1 + 8). |
consentMessages.Desktop |
"{0} ({1}) is asking to view or take control of this Ring HMI. Allow?" |
The exact text shown at the panel. |
consentMessages.Terminal / Files |
"...This should never appear - deny it and report it." |
If either ever appears, the rights model has failed — an incident, not a click-through. |
consentMessages.consentTimeout |
60 (seconds) |
How long the panel has to answer. |
consentMessages.autoAcceptOnTimeout / autoAcceptIfNoUser / autoAcceptIfLocked |
all false |
An unattended panel denies the connection. Bench-confirmed: after 61 s unanswered, the session returned to Disconnected with no screen ever shown. |
agentConfig |
["disableUpdate=1", "noUpdateCoreModule=1"] |
The local agent is frozen — no silent code change on a validated HMI PC. |
sessionRecording.* |
scoped true, indexed, protocols:[2], 90 days / 500 files / 20480 MB |
Desktop only, Plant HMI only, capped as shown. |
limits.* |
MaxDevices 8, MaxUserAccounts 25, MaxUserSessions 16, MaxAgentSessions 8 |
Sane ceilings for a single-HMI deployment. |
v2 — the five keys it patches, and why#
Each is justified line-by-line against the MeshCentral 1.2.4 source in
REMOTE_ACCESS_V2_RUNBOOK.md §4A; this is the one-line summary.
| # | Key | Rendered value | Reason |
|---|---|---|---|
| 1 | settings.trustedProxy |
"127.0.0.1,::1" |
The only key that makes MeshCentral trust cloudflared's forwarded cf-connecting-ip, so the audit log and login-throttle bucket use the real client IP instead of 127.0.0.1 for every tunnel login. |
| 2 | settings.cookieIpCheck |
"lax" |
Explicit (matches the 1.2.4 default) so the behaviour is documented, not discovered — accepts a /24 move once trustedProxy is on, forces re-login on a bigger one. |
| 3 | settings.userAllowedIP |
Widened with approved off-site CIDRs, or removed via -NoUserIpAllowList |
A deliberate Section-2D decision — see §9.8. Widened, the LAN CIDR and 127.0.0.1 are preserved, never replaced. |
| 4 | domains[""].userAllowedIP |
Same change, again | Independent of #3 — both must move together or logins die before authentication runs. |
| 5 | domains[""].allowedOrigin |
Widened: public FQDN + this machine's names + loopback | Without the public hostname here, /control.ashx — every desktop session depends on it — closes with invalidorigin. |
| — | _ringRemoteViewV2 |
Documentation-only array | Underscore-prefixed top-level keys are ignored by MeshCentral; a review aid only. |
v2 — must NOT set (the installer refuses to proceed if any is present)#
| Key | What it would do |
|---|---|
settings.tlsOffload (any value) |
Breaks v2 outright — MeshCentral serves plain HTTP on 8443, cloudflared's https:// origin fails; also drops the cookie's Secure flag. |
settings.trustedProxy = "CloudFlare" |
Wrong for a same-machine tunnel — matches the TCP peer against Cloudflare's ranges, but the peer here is 127.0.0.1, so cf-connecting-ip is silently ignored. |
settings.trustedProxy = true |
Trusts forwarded headers from every peer — any LAN host could forge X-Forwarded-For past userAllowedIP. Also out of schema (typed string). |
settings.aliasPort (e.g. 443) |
Poisons LAN agent discovery — the scanner tells the local agent to connect to 443, where nothing listens. |
domains[""].certUrl |
Solves a non-problem; causes outbound cert fetches and agent churn on every Cloudflare edge-cert rotation. |
ignoreAgentHashCheck (either scope) |
Disables the agent's TLS pinning — "for debugging only" per the schema's own text. |
domains[""].dns = <public fqdn> |
On the default domain this takes the whole server offline — dns is for extra sub-domains, never "". |
settings.relayDNS = <public fqdn> |
Replaces the MeshCentral UI with the app web-relay for exactly the hostname you meant to serve. |
settings.LANonly=false, settings.WANonly=true |
Breaks the tunnel; WANonly also kills local-relay and the LAN discovery scanner. |
settings.strictTransportSecurity |
Not in the 1.2.4 schema at all, and locks LAN users out of the self-signed cert's click-through. |
allowFraming variants |
Unnecessary (Access uses a full-page redirect, not an iframe) and widens the CSP frame-ancestors surface. |
domains[""].allowedOrigin = true |
Disables the origin gate entirely instead of enumerating hostnames. |
settings.cookieIpCheck = "none" |
Unbinds cookies from the client IP just as trustedProxy makes that IP meaningful. |
agentPort/agentAliasPort/agentAliasDNS |
Opens a second agent-only listener — there is exactly one, local agent, and it already reaches 8443. |
9.6 Scripts and parameters#
Build-RemoteViewPackage.ps1 — packager-side payload builder#
| Parameter | Default | Meaning |
|---|---|---|
-OutDir |
<script dir>\dist |
Build output directory. |
-NodeMajor |
24 |
Node LTS major to stage. Fails if past end-of-life. |
-ProbeOnly |
off | Resolve + HEAD-probe URLs, then stop. Downloads nothing. |
-SkipZip |
off | Stage dist\payload + manifest, skip the .zip. |
-Force |
off | Rebuild from scratch. |
Install-RemoteView.ps1 — target-side installer#
| Parameter | Default | Meaning |
|---|---|---|
-AdminUser |
ringadmin |
Site administrator account id. |
-AdminPassword |
generated | SecureString; omitted, a 20-char password is shown once. |
-AdminPasswordFile |
— | UTF-8 file, first line = password; deleted after reading. |
-LanCidr |
auto-detected | Allowed LAN range. Required with -InterfaceAlias on a multi-homed box. |
-InterfaceAlias |
— | Office NIC the firewall rule binds to; required on a multi-homed box. |
-HttpsPort |
8443 |
Pinned. |
-Operators |
@() |
@{User='...'; Pass='...'; ViewOnly=$true/$false} array. |
-OperatorSpec |
— | Flat form: "jbaker:control,shiftlead:view" (defaults view-only). |
-PayloadZip |
<script dir>\dist\RingRemoteView-payload.zip |
Never expanded with Expand-Archive — extracted entry by entry and proven against the archive's own listing and payload-inventory.json before anything is copied. |
-PayloadDir |
— | Alternative: an already-expanded directory. |
-InstallRoot |
C:\Ring\RemoteView |
Pinned. |
-NoFirewall |
off | Skip the inbound rule. |
-NoAgent |
off | Skip the local Mesh Agent. |
-Reinstall |
off | Adopt-and-replace an existing service instead of aborting. |
-TempExpandRoot |
$env:TEMP |
Payload expansion directory. |
-VerifyPayloadHashes |
off | Re-hash every extracted file (adds minutes under on-access AV). |
-HealthTimeoutSeconds |
180 |
Wait for https://127.0.0.1:<port> to answer. |
Provision-RemoteView.ps1 — device group + operator accounts#
| Parameter | Default | Meaning |
|---|---|---|
-InstallRoot / -HttpsPort |
C:\Ring\RemoteView / 8443 |
|
-AdminUser / -AdminPassword |
mandatory | Site administrator id / SecureString. |
-Operators |
@() |
Same shape as the installer's. |
-GroupName |
Plant HMI |
Pinned. |
-EmitInviteLink |
off | Time-bounded background-agent invite link — off by default (a never-expiring link is a standing credential). |
-InviteLinkHours |
24 |
Must be > 0. |
Uninstall-RemoteView.ps1#
| Parameter | Default | Meaning |
|---|---|---|
-InstallRoot / -HttpsPort |
C:\Ring\RemoteView / 8443 |
Port is used to find the firewall rule by name. |
-KeepData |
off | Preserve meshcentral-data\ and meshcentral-recordings\. |
-Force |
off | No confirmation prompt. |
New-RemoteViewFirewallRule.ps1#
| Parameter | Default | Meaning |
|---|---|---|
-Port |
8443 |
Pinned. |
-InterfaceAlias |
— | Restrict to one NIC. |
-Remove |
off | Remove instead of create. |
Install-RemoteViewTunnel.ps1 — v2 companion#
| Parameter | Default | Meaning |
|---|---|---|
-TunnelToken |
— | Tunnel token (String/SecureString). Required unless -TunnelTokenFile or -Uninstall. Stored in the service ImagePath — an accepted residual. |
-TunnelTokenFile |
— | UTF-8 file, first line = token; deleted after reading. |
-Hostname |
— | Public FQDN the tunnel publishes — no scheme/port/path. |
-RemoteAllowedIP |
@() |
Approved off-site egress CIDRs, added to the v1 allow-list. Choose this or -NoUserIpAllowList — the installer refuses to guess. |
-NoUserIpAllowList |
off | Removes userAllowedIP entirely; Access becomes the whole perimeter, LAN users lose the source-IP gate too. |
-ExtraAllowedOrigin |
@() |
Extra bare hostnames for allowedOrigin. |
-CloudflaredVersion |
2026.7.3 |
Changing it requires -ExpectedSha256. |
-ExpectedSha256 |
pinned hash | No way to skip verification. |
-PayloadPath |
— | Already-downloaded binary (air-gapped) — still hash-verified. |
-InstallRoot / -HttpsPort |
C:\Ring\RemoteView / 8443 |
v1 root and port. |
-EdgeWaitSeconds |
120 |
Bounded wait for evidence of a real edge connection. |
-AllowUnconfirmedEdge |
off | Downgrades "no edge evidence" to a warning — never suppresses an observed failure. |
-HealthTimeoutSeconds |
180 |
Matches v1. |
-Uninstall / -Force |
off / off | Remove the service, revert only the keys v2 added / no confirmation prompt. |
Test-RemoteViewTunnelSmoke.ps1 — v2 staged smoke#
| Parameter | Default | Meaning |
|---|---|---|
-CloudflaredExe |
staged path, else PATH | Path to cloudflared.exe. |
-HttpsPort |
8443 |
Origin is always https://127.0.0.1:<port>. |
-BudgetSeconds |
240 |
Hard ceiling on the exposure window (raised from 120 after a bench-found defect — §9.7). |
-TunnelStartTimeoutSeconds |
45 |
Wait for cloudflared to print its ephemeral hostname. |
-EvidenceDir |
<script dir>\dist |
Transcript location (git-ignored). |
-NoDelay |
off | Skip the 5 s abort window on the exposure banner. |
Verify-RemoteViewStatic.ps1 — static gate#
param() — no parameters.
powershell -NoProfile -ExecutionPolicy Bypass -File .\test\Verify-RemoteViewStatic.ps1
Runs entirely offline: installs nothing, starts no service, touches no firewall. See §9.7 for the current pass result.
9.7 Verification evidence#
Everything here was observed, in order, on a bench pass 2026-09-04 on the
developer laptop (Windows 11 Home, single NIC classified Public), source
tree main @ fd4e5173. Items not exercised are marked NOT VERIFIED —
that phrasing is deliberate; do not read it as a pass.
| Check | Result |
|---|---|
Static gate, before the certificate/allowedOrigin fix |
PASS, 0 failures |
Static gate, after the fix (branch docs/remote-access-guide) |
PASS, 0 failures — see remote-view/test/Verify-RemoteViewStatic.ps1 output for the current check count |
Build-RemoteViewPackage.ps1 -ProbeOnly |
PASS — MeshCentral pin 1.2.4 confirmed at npm; Node resolved to v24.20.0 (EOL 2028-04-30); nodejs.org URLs HEAD 200 |
Existing payload RingRemoteView-payload.zip (built 2026-07-31) |
115,737,143 bytes, SHA-256 be01f3ca00c84ce18df9e802bda82f03ab7a7a3774104aa4ebcdaa929bf2191f, matches manifest.json (Node v24.18.1, MeshCentral 1.2.4, four extra modules) |
Fresh install with the fixed package (authoritative).
Install-RemoteView.ps1 (-NoFirewall, with the local Mesh Agent), branch
docs/remote-access-guide: exit 0, 175 s (an earlier run before the
cert/origin fix took 203 s under the same conditions — expect longer on an
HMI PC with on-access AV scanning the ~394 MB payload). Printed: certificate
name laptop-t8d4hur8.local; allowedOrigin = laptop-t8d4hur8, laptop-t8d4hur8.local, localhost, 127.0.0.1, 192.168.1.206; server cert
subject=CN=laptop-t8d4hur8.local; agent service Running; shortcuts for
https://laptop-t8d4hur8:8443/.
Browser walkthrough as operator jbaker (control mask) — observed and screenshotted#
| Step | Observed |
|---|---|
| Login page | Title "Ring Remote View - Login"; footer credits MeshCentral 1.2.4 (Apache-2.0). |
| First login, one-time password | "Password change requested," two password fields, then the main UI loads. |
| 2FA enrolment | My Account → "Account security" → "Manage authenticator app": QR code, secret, 6-digit token field. A valid code: "Authenticator app activation successful." |
| Next login | A second page asks for the 6-digit token. |
| Device list | Group Plant HMI, device LAPTOP-T8D4HUR8, "Agent, Powered." Operator tabs: General, Desktop, Events only. |
| Desktop → Connect | "Connected"; area reads "Waiting for user to grant access..." At the panel: "jbaker (jbaker) is asking to view or take control of this Ring HMI. Allow?" (Allow / Deny, plus an "auto-accept next 5 minutes" checkbox). |
| Nobody answers | After 61 s, status returns to Disconnected; no screen content was ever shown. PASS — fails closed. |
Not exercised: clicking Allow, view-only input-discard, registry/software tabs, off-subnet. The auto-accept checkbox is stock MeshCentral — tell operators to leave it unticked unless the shift agrees.
Uninstall — verified.
Uninstall-RemoteView.ps1 -Force: exit 0, 50 s. Removed the Mesh
Agent (-fulluninstall) and MeshCentral service, printed "No Ring Remote
View firewall rules found" (this run used -NoFirewall), "UNINSTALL
COMPLETE." Confirmed afterward: no services, install root gone,
C:\Program Files\Mesh Agent gone, the "Mesh Agent WebRTC Traffic" rule
gone, no 8443 listener, shortcuts gone.
Penetration-test items — REMOTE_ACCESS_RUNBOOK.md §8, observed via MeshCtrl#
| # | Attack | Account | Observed | Verdict |
|---|---|---|---|---|
| 2 | File transfer (Upload/Download) | jbaker (control) | Connecting... then Unable to route; nothing transferred |
DENIED |
| 2 (control case) | same | ringadmin (admin) | Connected, download completed | allowed, as expected |
| 3 | RunCommand --run whoami |
jbaker / shiftlead | Access denied (both) |
DENIED |
| 3 (control case) | same | ringadmin | OK |
allowed, as expected |
| 4 | Shell (interactive terminal) |
jbaker | Connecting... then Unable to route; no shell in 20 s |
DENIED |
| server rights | ListUsers |
jbaker | Access denied (--rights none) |
DENIED |
| 9 | Self-registration | unauthenticated | HTTP 404 each, no form | DENIED |
| 10 | MPS exposure | — | no listener on 4433 | PASS |
| 7 | Consent bypass / unattended panel | — | confirmed in the walkthrough: 61 s timeout, Disconnected, no content shown | PASS — fails closed |
| 1 | Raw terminal relay (p=1) |
— | not run raw; test #4 exercises the same relay gate | partially covered |
| 5 | Registry / software tabs | — | NOT VERIFIED — no headless probe available | NOT VERIFIED |
| 6 | View-only sends input | — | NOT VERIFIED headlessly | NOT VERIFIED |
| 8 | Off-subnet reach | — | NOT possible on a single machine | NOT VERIFIED |
Rights masks are enforced server-side — the relay itself refuses to route a restricted account, not merely a hidden tab. The full §8 table on a clean VM with a second machine is still owed for items 1, 5, 6 and 8.
v2 — what was verified:
| Item | Result |
|---|---|
cloudflared-windows-amd64.exe 2026.7.3 |
54,213,360 bytes, SHA-256 8635DA433B6DF8194746E88ED9D2589566C20E38BFC2A80E431A348B7C765841 — matches the pin; --version confirms the tag. |
Test-RemoteViewTunnelSmoke.ps1 (after the fix below) |
PASS — every gated probe passed, 59 s. Hostname resolved after 30 s; GET / HTTP 200; CSP connect-src named the tunnel's own hostname (Host header survives the hop); /control.ashx, /meshrelay.ashx, /2fahold.ashx, /echo.ashx upgrades all PASS; the Origin gate closed an unknown Origin with invalidorigin; tunnel process confirmed gone at the end. |
Install-RemoteViewTunnel.ps1 itself |
NOT run — needs a real Zero Trust tunnel token and domain. |
| Cloudflare Access policy, full off-site smoke, v2 penetration test | OUTSTANDING. |
Defect found and fixed in the smoke script. As shipped, it probed the new
hostname before DNS had propagated and every probe failed with an opaque
exception. Fixed on branch docs/remote-access-guide: waits up to 90 s for
DNS, retries the login-page GET for up to 60 s through transient Cloudflare
530/1033 errors, unwraps the real inner exception, and raised the default
-BudgetSeconds from 120 to 240. The PASS above is the re-run.
Other verified facts.
The Inno Setup GUI wrapper (RingRemoteView.iss) has never been
compiled — no ISCC installed, no RingRemoteView-Setup.exe anywhere in the
tree; the PowerShell installer is the only proven install path.
Multi-homed detection ignores loopback and 169.254.* APIPA addresses.
IPv6 caveat: browsing the bare hostname from the laptop itself answered
HTTP 401 before any login page, because the name resolved to IPv6
addresses first and userAllowedIP lists only the IPv4 CIDR and
127.0.0.1 — make sure the HMI name resolves to IPv4 for LAN clients, or
browse the IPv4 address directly (allowedOrigin permits that). A
PowerShell trap, not MeshCentral's: Invoke-WebRequest/ClientWebSocket
with a ServerCertificateValidationCallback scriptblock fails
non-interactively ("There is no Runspace available to run scripts in this
thread") — use curl.exe -k https://127.0.0.1:8443/ or a real browser for a
hand check instead.
9.8 Security posture and residual risks#
Defence in depth, any one gate stopping an attacker on its own: (1) the
network perimeter — LAN-only (v1), or Cloudflare Access authenticating at
the edge (v2); (2) MeshCentral's own accounts — login, force2factor, the
panel consent prompt, session recording; (3) the rights mask, enforced
server-side (confirmed in §9.7).
Fixed defects (bench 2026-09-04 — read before trusting an unmodified main build):
| Defect | Root cause | Fix |
|---|---|---|
| Login succeeded, then "Invalid origin in HTTP request, click to reconnect." | settings.cert was $env:COMPUTERNAME (always uppercase); the Origin check is case-sensitive and browsers always lowercase the Origin. |
Cert name rendered lowercase; allowedOrigin added (hostname, cert name, localhost, 127.0.0.1, LAN IPv4). |
2FA enrolment was impossible while force2factor made it mandatory. |
MeshCentral only shows "Account Security" when the cert CommonName contains a dot; a bare NetBIOS name never does. | Cert name rendered with a dot (FQDN, or <hostname>.local). |
A failed install sometimes printed ROLLBACK INCOMPLETE for folders that had actually been removed. |
The rollback closures for meshcentral-data/meshcentral-recordings used .GetNewClosure() blocks that couldn't see the script's own Remove-TreeRobust function. |
The function is now captured into the closure. |
All three are fixed on branch docs/remote-access-guide (the static gate
gained one new assertion for the fix). Verify which branch a given build
came from before
assuming 2FA enrolment or a plain-hostname browser session will work.
Residual risks — v1.
MeshCtrl takes credentials on its command line during provisioning, briefly
visible in the process list (console-only, install-time only); the Inno
wrapper passes -AdminPassword as an argument — same residual; a site
administrator can do everything, and the control is procedural — that
credential stays with plant management.
Residual risks — v2 (only where the tunnel is installed).
The tunnel token is a standing credential in the service ImagePath
(registry) — rotate it in the Zero Trust dashboard if the box is suspect.
trustedProxy: "127.0.0.1,::1" means any local process can forge a
source IP — accepted only because cloudflared is the sole local client of
8443 on a single-purpose HMI, and force2factor still stands behind it.
Cloudflare sits in the trust path (edge-decrypt, re-encrypt to origin) — the
Tailscale alternative avoids this if policy forbids a third party. Double
authentication is intentional: MeshCentral has no key consuming an Access
identity header, so Access is a perimeter, not single sign-on.
cookieIpCheck: "lax" forces re-login on a roaming IP move outside its
/24 — correct behaviour, not a fault to chase. The v2 install restarts
MeshCentral, dropping any live LAN session; Ring and the PLC are untouched.
Still outstanding — a verification gap, not a residual risk. Pen-test items 1 (partial), 5, 6, 8 on a clean two-machine VM; the entire v2 off-site chain (Access policy, off-site smoke, v2 penetration test); whether the Inno Setup wrapper compiles at all.
9.9 Patch policy and licences#
| Component | Pin mechanism | To move it |
|---|---|---|
| MeshCentral | Pinned in Build-RemoteViewPackage.ps1; selfUpdate:false; agent frozen (agentConfig, noAgentUpdate:1) |
Watch advisories. On a security release: bump the pin, rebuild, re-run the static gate, re-run the penetration test, redeploy in a window. Never npm update in place. |
| Node runtime | The build refuses a line already past EOL | Bump -NodeMajor as the pinned line nears EOL. |
cloudflared (v2) |
Pinned version + SHA256 in the installer; auto-update off in the service ImagePath | -Uninstall, reinstall with new -CloudflaredVersion and -ExpectedSha256 (refuses a bump without it); re-run the staged and relevant off-site smoke lines. |
| Access policy (v2) | A live policy, not a pinned artefact | Review allowed emails/groups, session duration and MFA on the same cadence as user off-boarding — a drifted policy is a silent hole. |
AV/EDR. MeshCentral is widely flagged as a post-compromise RMM tool —
support an allow-list request with dist\manifest.json (the SBOM). Concrete
example: this laptop's Defender history shows MeshService.exe quarantined
2026-08-20 as RMM tooling; the 2026-09-04 install needed a Defender
exclusion for the scratch directory and C:\Program Files\Mesh Agent (removed
after). cloudflared.exe is legitimately signed but some EDR flags it by
category too — raise both with IT before install day.
| Licence file | Contents |
|---|---|
remote-view\NOTICE |
Attribution + Apache-2.0 statement of changes. |
remote-view\THIRD-PARTY-LICENSES.txt |
Apache-2.0 full text, Node.js MIT notice, cloudflared licence (also Apache-2.0). |
dist\manifest.json |
The SBOM — the artefact an AV/EDR allow-list request should cite. |
Sources verified#
remote-view\README.md— pinned constants, layout, security model, v2 summary, licence and patch-policy sections.remote-view\Install-RemoteView.ps1— parameter block; the certificate name/allowedOriginrendering logic and comments; the INSTALL OK summary; the rollback/catch-block structure.remote-view\Provision-RemoteView.ps1,remote-view\Uninstall-RemoteView.ps1,remote-view\New-RemoteViewFirewallRule.ps1— read in full: parameter blocks, mask constants, password generation, removal sequence.remote-view\Install-RemoteViewTunnel.ps1,remote-view\Test-RemoteViewTunnelSmoke.ps1,remote-view\Build-RemoteViewPackage.ps1,remote-view\test\Verify-RemoteViewStatic.ps1—.SYNOPSIS/.DESCRIPTIONand parameter/pinned-constants blocks of each, including the v2 cloudflared version/SHA256 pins.remote-view\config-template.json— every rendered key in §9.5 and the_keyProvenance/_notesblocks behind the "why" column.docs\production-readiness\REMOTE_ACCESS_RUNBOOK.md— v1 procedure, firewall table, account model and mask arithmetic, §8 penetration table, patch cadence.docs\production-readiness\REMOTE_ACCESS_V2_RUNBOOK.md— v2 procedure, the five patched keys with their MeshCentral evidence, the must-not-set table, staged/off-site smoke tables, v2 penetration test, residual risks.docs\production-readiness\REMOTE_ACCESS_V2_CLOUDFLARE.md— design rationale, the three-gate argument, the Tailscale alternative.- Bench verification facts sheet dated 2026-09-04 (developer laptop
LAPTOP-T8D4HUR8, source tree
main @ fd4e5173) — the source for every dated observation and pass/fail result in §9.2, §9.6–§9.8; where a later addendum in it superseded an earlier line, this chapter follows the addendum.
Not verified anywhere in this chapter: pen-test items 1 (fully), 5, 6 and 8
from REMOTE_ACCESS_RUNBOOK.md §8; the entire v2 off-site chain; and
whether the Inno Setup wrapper compiles at all (never built).