All posts Security

remote_access_policy: A Remote-Access Ceiling the Cloud Can't Raise

SECURITY 9 MIN READ NETDEFENSE TEAM

Signing, sequencing, and a fixed operation vocabulary protect the command channel: a device only acts on instructions that carry a valid control-plane signature, in the right order, exactly once. That’s a real guarantee, and it holds even against a compromised dispatch tier. What it does not do is protect against a valid credential doing something that credential is allowed to do. CONNECT — opening a remote session — is one of NDAgent’s ten fixed operations, the same as PING or SYNC. And a remote session is a shell.

So a stolen write-scoped token, a compromised control plane, or a prompt-injected AI agent holding valid credentials can produce a perfectly signed, perfectly sequenced CONNECT — and every layer described on our security page has no basis to object, because nothing about that request is malformed. That isn’t a flaw in the signing design; it’s the honest edge of what a signed channel can promise. Signing tells you a command is authentic. It says nothing about whether the credential behind it should have been allowed to ask.

NDAgent v1.15.0 adds a layer that answers a different question. Not “was this request authentic,” but “should this device ever open a session like this at all” — and it answers that question locally, on the device, regardless of what arrives over the signed channel.

TL;DR

  • NDAgent v1.15.0 adds remote_access_policy, a device-local ceiling on remote sessions: full (default), readonly (web UI only, no shell), disabled (no session opens at all).
  • The control plane can request a session at or below the ceiling, never above it — and it can’t raise the ceiling either, because the value lives in the device’s own configuration and configuration sync has no writer for that node.
  • Enforced twice: once before a relay session is even dialed, and again at the chokepoint every stream — interactive shell and the MCP exec stream alike — has to pass through.
  • Existing devices default to full on upgrade. Nothing changes until you opt in.
  • The limit is explicit: this closes NetDefense-mediated remote sessions, not configuration sync. Package installation still runs post-install scripts as root.

Three values, and what each one actually forecloses

remote_access_policy is a single setting in NDAgent’s local configuration, with three values:

  • full (the default) — unchanged behavior. Any remote session type the platform requests is honored: interactive shell, the web admin tunnel, the MCP device console’s console_exec.
  • readonly — only the read-only web admin tunnel is ever served. No shell, no SSH, no exec stream — regardless of what the incoming request asks for. If a CONNECT arrives requesting an interactive session, the device does not partially honor it and hope the caller behaves; it serves, at most, the read-only view.
  • disabled — no session opens at all. Not a read-only one, not a web admin one. CONNECT is refused outright, before anything is dialed.

The important property is the direction of the ceiling: the control plane may request a session at or below what the device allows, never above it. A full device can still be asked for a read-only session. A readonly device asked for an interactive shell gets, at most, the read-only tunnel it’s configured to allow — never the shell.

Why it’s device-final

The setting lives in the device’s own OPNsense configuration, not in anything NetDefense’s control plane stores or pushes. That has a concrete consequence: NDAgent’s OPNsense API client has no writer for this configuration node. Not “a writer gated behind a permission check” — no writer at all.

That means:

  • An operator account, however privileged, can’t raise it remotely through any API call. There’s no endpoint that reaches this value.
  • The platform can’t raise it on your behalf during a configuration sync, because sync can only touch the OPNsense nodes NDAgent’s writer supports — and this isn’t one of them, on purpose.
  • An AI agent driving the MCP tools can’t raise it either, for the identical reason. There’s no tool call, confirm: true or not, that reaches this value, because the value has no remote path to reach.

Why the device, not the platform, enforces this

This is the same principle behind reject_dangerous_snippets: a guarantee that has to hold even when the control plane is compromised can’t depend on the control plane choosing to honor it. It has to live somewhere the control plane structurally cannot reach. For remote_access_policy, that somewhere is the device’s own configuration, behind a writer that simply doesn’t exist for this node.

To change it, you set it on the firewall itself: Services → NetDefense → Settings, under Advanced, then Apply — the same place you’d flip reject_dangerous_snippets. Applying reloads the agent’s configuration for you; there’s no separate restart step. It’s a deliberate, on-device decision, every time.

Enforced twice, on purpose

remote_access_policy is checked in two places, not one:

  1. In the CONNECT handler, before any relay session is dialed. On a disabled device the request is refused outright — nothing is dialed, and NDPathFinder is never contacted for a session that was never going to be allowed. On a readonly device the request isn’t refused; it’s clamped before the session starts. A CONNECT asking for an interactive session is downgraded to a read-only one there and then, rather than being started as requested and policed afterwards.
  2. Again at the per-stream chokepoint every stream has to traverse, regardless of what the CONNECT handler decided. This is why the ceiling covers both the interactive PTY (ndcli device connect) and the MCP exec stream (console_open / console_exec) identically — both funnel through the same point before a single byte of shell output leaves the device.

The reason for two gates instead of one is simple: the guarantee shouldn’t depend on any single caller, code path, or future feature having applied the check correctly. Two independent enforcement points that would both have to fail is a meaningfully different bar than one check that a bug, a refactor, or a new client integration could someday bypass.

The middle setting is the interesting one

full and disabled are easy to reason about. readonly is the one worth sitting with, because it’s the setting most MSPs and homelab operators will actually reach for.

We already ship read-only remote WebAdmin sessions — a session type an operator or MSSP customer can choose per connection, enforced on the device once that flag is set. remote_access_policy=readonly is a different, stronger claim: it isn’t a flag the caller sets per session, it’s a ceiling the device holds regardless of what any caller — operator, automation token, or compromised platform — asks for. Set it on a client’s firewall and every future session to that device is structurally denied a shell, permanently, until someone changes the setting locally on that box.

That’s the practical case for most readers: a client site where support staff legitimately need to glance at interface status or confirm a rule during an outage, but where nobody — including you, including your own automation, including an incident you haven’t had yet — should ever be able to get a shell on that box through NetDefense. readonly makes that structural instead of procedural.

Upgrade behavior

Existing devices default to full on upgrade. Nothing changes in behavior for a fleet that upgrades to v1.15.0 and does nothing else — the same pattern we used when reject_dangerous_snippets shipped. Opt in per device, whenever it fits your change process, from Services → NetDefense → Settings on that firewall. There’s no forced migration and no silent behavior change.

No action required to stay exactly as you are

If you don’t set remote_access_policy, your devices keep working precisely as they do today. This is a ceiling you opt into, not a default that changes under you.

The limits, stated plainly

This ceiling governs NetDefense-mediated remote sessions onlyCONNECT and the streams that flow from it. It is not a general lockdown of the device, and it’s worth being explicit about what it leaves open:

  • Configuration sync is a separate surface entirely. remote_access_policy has no opinion on what a sync can push, and a sync doesn’t go through the CONNECT path at all.
  • Package installation is a root path this ceiling does not close. A plugin install runs the package’s post-install scripts as root, same as it always has — remote_access_policy=disabled does not change that.
  • reject_dangerous_snippets — the device-side guardrail we shipped for sync content — covers the privileged-user, SSH-key, and service-account vectors inside a configuration push. It does not cover package installation either. The two features close different doors; neither claims to close all of them.

If you want the honest full picture of what’s covered and what isn’t, the security page is the place we keep it current.

If an AI agent drives your fleet, do both

If an LLM-powered client is driving your devices through the MCP server — reviewing diagnostics, running syncs, opening consoles — the actionable advice here is belt and braces, not either-or:

  • Scope the token. A read-only Personal Access Token can’t request a write-scoped CONNECT in the first place. Give automation exactly the access it needs and nothing past it.
  • Set the ceiling. A scoped token protects you from that token being misused. It doesn’t protect you from every other credential in your organization, or from the platform itself in the worst case. remote_access_policy does, for the devices where you set it.

Neither one substitutes for the other. A read-only token on a device left at full is still one leaked write-scoped credential away from a shell on that box. A device set to readonly still benefits from a scoped token, because scoping limits what an agent can even attempt, not just what a device would allow if attempted. Do both, and the two failure modes stop overlapping.

Join the discussion in our Community

Related reading

Security Secure by Default: Config Pushes That Grant Privileges Are Now Rejected Out of the Box
Guide How to Manage Multiple OPNsense Firewalls: Your Options in 2026
Feature Read-Only Remote Access to Your Firewall's WebAdmin

Enjoyed this post? Explore more in the documentation.