RINGby Ringwood

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: LocalSubnet alone is not enough if the plant subnet is also "local" — bind the rule to the office NIC with -InterfaceAlias; the installer requires both -LanCidr and -InterfaceAlias together on a multi-homed box and asserts the rule's interface filter matches. Profile: the rule is Domain,Private only — if the NIC's Get-NetConnectionProfile category 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 -NoFirewall instead, 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): ringadminFullAdministrator; 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 offlinedns 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 8635DA433B6DF8194746E88ED9D2589566C20E38BFC2A80E431A348B7C765841matches 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/allowedOrigin rendering 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/.DESCRIPTION and 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/_notes blocks 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).

Generated from the docs/manual/reference book in the Ring repository — the markdown there is the source of truth. Paths shown in code like this point into the Ring source repository, which is private to Ringwood — they are not links.