Node Access Control
Every noBGP node ships ready to be operated remotely: your AI assistant can run commands and read or write files on it, and the event bus can watch paths and dispatch work. That is the point of the product, and it is why an agent behind CGNAT (carrier-grade NAT) is useful at all.
It is also more reach than some machines should offer. Three settings on the node let its owner narrow that reach — from the box itself, in the agent's own config, where nothing on the network can override them.
These settings answer "what will this machine serve?" They do not answer "who is asking?" — that is the router's question, and for the one operation where it matters most, running as the superuser, the router asks it too. See Two gates, not one.
The three settings
| Setting | Governs | Default |
|---|---|---|
allow-tools | Which capability domains this node serves: fs, command | both |
allow-roots | Which filesystem roots the fs tools and the fs event source may touch — never the agent's own directory | / (everything else) |
allow-admin | Whether work may run as root / Administrator — the node half of two gates | true |
A fourth key, user, is not a veto but the other half of the same setting: it names the account unelevated work runs as. It matters here because a node with no usable user cannot run unelevated work at all — see Unelevated never means root.
Both user and allow-admin are set on the machine and only there. There is deliberately no tool to change them: altering the identity that gates a channel, through that channel, is using a tool to widen what the tool may do.
Set it with sudo nobgp config --user <account>, and clear it with sudo nobgp config --user "" — see Confirmed before it is written for what each of those changes costs.
user has one other on-machine source: the NOBGP_USER environment variable in the agent's own environment — the shape a container image or a task definition uses, and how a provisioned node arrives with an account already named. It outranks the config file for as long as it is set, so nobgp config --user on such a node says so rather than letting the value it just saved be silently overridden.
They are ANDed: any one of them saying no is a refusal. The values of allow-tools are domains, not tool names — fs covers fs_read, fs_write, file, fs_subscribe and friends; command covers command, command_subscribe and the shell behind a published terminal service.
# Read-only node: files may be read and watched, nothing may be executed
sudo nobgp config --allow-tools=fs
# Confine file access to one tree
sudo nobgp config --allow-roots=/var/log,/etc/myapp
# No root: work drops to the account named by --user
sudo nobgp config --allow-admin=false --user=deploy
# Read them back at any time
nobgp show
nobgp status # includes an `allow:` block
Changes take effect without a restart — the agent watches its own config.
A node that upgraded into a denied state would need someone physically at it, which on a NAT'd machine is not a conservative default — it is an unrecoverable one. So all three ship permissive, and tightening is always an explicit act by the node's owner.
Two gates, not one
allow-admin is the node owner's veto on superuser execution. It is not the only thing standing in the way of one, and it was never meant to be the only thing.
| Gate | Question | Answered by |
|---|---|---|
| Router | May this caller ask to be the superuser? | the caller's organization role — Owner or Admin, never a Member; or a node's manage grant on the local MCP server |
Node (allow-admin) | May anyone be the superuser on this machine? | the node's owner, here |
Both refuse rather than downgrade, and both have to say yes. An unelevated request — the default — needs neither.
The two refusals are distinguishable, which matters when you are reading an error rather than writing one: the router's is forbidden, and the node's — like every other veto on this page — is permission_denied, carrying the agent's own wording. Neither is retryable.
This matters because the defaults are permissive. allow-admin: true on a stock node says "root is available here", and until the router gained the caller-side gate, that was the only question anyone asked: whoever could reach the node could be root on it. Now the node owner's setting bounds the machine and the organization's roles bound the people, and neither substitutes for the other.
allow-tools and allow-admin bind everyone, elevated or not: a node that does not serve fs serves it to nobody, and a node that refuses the superuser refuses it to an org Owner exactly as it does to a Member.
allow-roots is different — since agent 0.4.37 it confines the node's configured account, and an elevated caller is not bound by it. On earlier agents it binds both identities. The reasoning is that a caller who has passed both gates is the superuser on that box, and a superuser confined to /var/log can undo the confinement with one command call anyway. The exception is the agent's own directory, refused at both identities and by no amount of elevation.
So allow-roots narrows what ordinary work reaches. If you want to bound what an administrator reaches, the setting for that is allow-admin: false.
What these settings govern
Every remote path onto the node — the MCP tools your AI assistant calls, the event-bus sources the router dispatches, and, since agent 0.4.33, published terminal services as well.
So on a node with allow-tools emptied:
| Access path | Result |
|---|---|
AI assistant calling the command tool | refused |
AI assistant reading files via fs_read | refused |
| Event-bus source (fs watch, dispatched command) | refused, with the reason |
| Browser terminal from a published service | refused |
The same goes for allow-admin: anything that would run as uid 0 is refused, whether it comes from an MCP caller, a dispatched command, a file operation, or a terminal service published with admin. It refuses regardless of who is asking — passing the router's caller gate does not make a node serve what its owner turned off. It also covers a node whose configured user is a superuser account: the gate asks what uid the work resolves to, never how the account is spelled.
These keys answer for the node's own filesystem. They say nothing about the shared drive or the node's storage area: those live in noBGP storage, and a write to either is served by the router without the agent seeing it, so no key here is consulted. That is exactly why a node's area is gated a step higher, at Owner or Admin in the organization that owns the node — see Roles & Permissions. Narrowing allow-roots still governs what the fs tools may touch on the machine, including the mount point where the shared drive appears.
This holds when the same tools address storage. From router 0.4.56 the fs tools can name a network's share or a node's storage area instead of a node's disk (how), and such a call never reaches the agent — the node can even be switched off — so a node's own vetoes cannot narrow it. Turning fs off in allow-tools takes away what callers may do to this machine's files, not what they may do to storage the organization owns.
Before agent 0.4.33 a published terminal service was not governed by these keys, on the reasoning that it kept a locked-down node recoverable without a trip to the machine.
That exemption also meant allow-admin: false could not do the one thing its name promises: a service published to run as root handed out root on a node whose owner had refused exactly that. The exemption is gone, and the recovery advice moved to the moment of the change — nobgp config now warns when a write leaves a node serving nothing, and asks before turning off superuser execution on a node where that removes remote execution entirely.
On a machine behind CGNAT, sudo nobgp config --allow-tools=fs (dropping command) closes the browser terminal too, and allow-admin: false with no usable user configured — a container, a bare-root install, a Windows node that captured no install account — refuses every session outright: the elevated path is vetoed and the unelevated one has nowhere to land. A Windows node that does have an account stays operable as that account from agent 0.4.46; on 0.4.44 and 0.4.45 it keeps its file tools and loses execution, since that identity covered files first. In those cases the only way back is console, RDP or SSH access to the box.
The usual answer is to keep the veto and give it somewhere to land: sudo nobgp config --allow-admin=false --user=deploy refuses the superuser while leaving the node fully operable as deploy. The --user account must already exist on the machine and must not be a superuser account — nobgp config refuses a name it cannot resolve, and since agent 0.4.41 refuses one that resolves to uid 0 (--user root is the shape that reads benign), rather than letting every later unelevated session fail to start.
Since agent 0.4.52 the change that leaves nothing behind — dropping command from allow-tools — is confirmed before it is written, and a non-interactive caller must pass --yes. See Confirmed before it is written.
Unelevated never means root
allow-admin bounds the elevated path. There is a second refusal on the other side of it, and it is the reason a node with no usable user is an off switch rather than a narrowing.
A node resolves what an unelevated call would run as — the account named by --user if there is one, its own ambient identity otherwise. If that comes out uid 0, it refuses the call rather than running the work as the superuser:
| Node state | admin: false | admin: true |
|---|---|---|
| An account is configured, resolvable, and not uid 0 | runs as that account, confined to allow-roots | ambient — gated by the caller's role and allow-admin |
| No account, or one that no longer resolves — on an agent running as root | refused | unchanged |
user: root, or any other uid-0 account — on any agent | refused; the config asked for the superuser, this call did not, and the call decides | unchanged |
| No account — on an agent that is already unprivileged | runs as that ambient identity | the same uid; no elevation exists there |
The refusal is failed_precondition, HTTP 403, and is not retryable — the request is not denied, it is incoherent, because "unelevated" would mean the superuser on that box. The message names both remedies:
# Give unelevated work somewhere to land
sudo nobgp config --user deploy
…or the caller re-runs with admin: true, if the superuser is genuinely what they meant.
Pick an account that can actually do the work. Setting --user nobody to satisfy the rule trades one clear refusal for a run of confusing permission failures further along.
You can read the answer off the box instead of discovering it. Since agent 0.4.42 nobgp status reports allow.unelevated — the account an unelevated call resolves to here — or allow.unelevated_refusal when there is none. See The fourth line is not a veto.
Windows needs no exception here. Its service runs as LocalSystem, which reports as uid 0, so a Windows node refuses unelevated work by the same predicate rather than by a platform rule of its own — with no account configured it is a node offering exactly one identity, and every call has to ask for it. Since agent 0.4.44 a configured user gives it a second — file operations first, and execution too from agent 0.4.46 — see below.
It is not a privilege reduction: a node with no account moves from everything implicitly root to only admin: true, explicitly root. The same privilege stays reachable by whoever could reach it before. What it buys is intent and auditability — an elevation appears in the record as one, and a caller weighing the risk of a command can see which identity it will get.
The old behaviour was to fall back to ambient root: silent, and in the escalating direction. That is the failure this closes — a terminal service whose owner had named an unprivileged account, running as root because the account could not be resolved and nothing said so.
The rule is the node's, read from its own config at the moment of the operation — the router forwards the flag and resolves nothing. Agent 0.4.41 is the release that introduced it.
A second identity on Windows
A Windows node with no account configured has one identity. Name one — the same user key as on every other platform — and it has a second, obtained by name: nothing prompts for a password, and no credential is stored on the machine.
# C:\ProgramData\nobgp\default.yml
user: alice
Since agent 0.4.44 the account is the whole of the switch, the same one-switch rule as Unix. Agent 0.4.43 shipped this behind a separate windows-user-identity key; that key is retired, and an upgrading agent removes it from the config file.
Since agent 0.4.46 that identity covers every surface — the same answer Unix gives:
| Operation | admin: false, agent 0.4.44–0.4.45 | admin: false, agent 0.4.46+ |
|---|---|---|
file and the fs_* tools | runs as user | runs as user |
command, terminal sessions, dispatched bus commands | refused, failed_precondition | runs as user |
In both cases the drop is genuine rather than an authorization check: a file written unelevated is owned by that account, and a process created unelevated is that account's, with its own %USERPROFILE%, %APPDATA% and HKCU.
Through 0.4.45 execution was refused rather than served because a Windows spawn still happened in the service's own context. Running it would have meant LocalSystem doing work the node had just said would run as alice — the silent elevation this whole rule exists to remove, and worse than a refusal because nothing in the record shows it. The refusal named the way out: send admin: true to run as LocalSystem deliberately.
Nothing in a client needs to change for the flip. The node you called is the authority, so a refusal means that node has not moved yet and its absence means it has. Do not build "Windows always needs admin: true" into a caller — a fleet upgrades one node at a time, and the claim is false on every node that has moved.
It still fails closed. If the node cannot obtain the account's token, load its profile or build its environment, the call is refused with failed_precondition — it is never quietly served in the service's own context.
A profile directory is created. Creating a process as an account needs its profile loaded, or the child would run as alice while %USERPROFILE% and HKCU still pointed at the service's own. For an account that has never signed in at that machine, the first unelevated command therefore creates C:\Users\<account> — logged as a warning, and left in place by nobgp uninstall. File operations need no profile and create nothing.
Network resources are not reachable as that account. The identity is obtained by name and holds no network credentials, so a command that reads a file share, or reaches anything else that authenticates it over the network, works with admin: true and fails with admin: false. This is inherent to obtaining an identity without a stored password — the same limit Windows Task Scheduler's "do not store password" option has — and is the trade for there being no credential on the box at all.
A Windows account in the local Administrators group receives an unfiltered admin token, so admin: false there is a drop from LocalSystem to Administrator and no further — a real reduction, but not isolation. It is the common shape, since the installer captures the interactive desktop user. It is deliberately not refused (unlike the Unix user: root case, where the two identities collapse to one uid): the two identities stay genuinely distinct here. What bounds such a node's file surface is allow-roots, which binds admin: false whatever the account turns out to be; execution is bounded by the account itself, or by dropping command from allow-tools.
You do not have to guess which shape a node is. network_directory reports it as info.user_is_admin (agent 0.4.46+ answers it, router 0.4.47+ reports it), and nobgp status on the box itself carries allow.unelevated_is_admin — see The fourth line is not a veto.
Agent 0.4.46 also made such an account's file access honour ACLs. An administrative account's token arrives from Windows carrying every privilege enabled, SeBackupPrivilege among them, and a read taken with backup privilege never consults the file's ACL. So on 0.4.44 and 0.4.45, on a node whose configured account was an administrator, an unelevated fs_read could read files that account's own permissions did not allow. Every token that stands behind the second identity is now stripped of its privileges before anything uses it, so the account reaches exactly what it is permitted to reach. A node whose account is an ordinary user was never affected.
allow-admin: false therefore reads differently on a node with an account than on one without: from agent 0.4.46 both file operations and execution drop to alice rather than being refused, exactly as on Unix, so the node stays operable with the superuser vetoed. On 0.4.44 and 0.4.45 it leaves file operations working and execution off.
A name the node cannot obtain a token for — an account since deleted, or a config copied off another machine — leaves it with one identity and logs why, rather than reporting an account it cannot become. That is the same rule Unix follows, and it is why network_directory reports the account a node resolved rather than the string in its config.
A Windows install captures whoever is signed in at the desktop and writes it to user — the same thing the Unix installer does with the account that ran sudo. Since agent 0.4.44 that name is the node's second identity from its first start; under 0.4.43 it was recorded but had no effect. A headless or unattended install captures nothing, which is the honest answer rather than a guess, and such a node keeps its single identity until someone names an account with nobgp config --user.
On upgrade from 0.4.43 a recorded name goes live. Nothing gains privilege: unelevated file calls that were refused now run as that unprivileged account, and admin: true is unchanged. On 0.4.44 and 0.4.45 execution still refused an unelevated call; from 0.4.46 it runs as that account too. The node starts reporting the account in network_directory, so it says what it will do.
The agent's own directory is never served
Since agent 0.4.34 the fs tools and fs event sources refuse every path inside the agent's own configuration directory, whatever the three settings say:
| Platform | Directory |
|---|---|
| Linux | /etc/nobgp |
| macOS | /usr/local/etc/nobgp |
| Windows | C:\ProgramData\nobgp |
allow-roots cannot widen it, allow-admin: true does not open it, and elevating does not reach it — the refusal is a property of the path rather than of the caller's privilege, and symlinks are resolved first, so a link pointing into that directory is refused the same way. The reason is what lives there: the node's network key and its JWT, and the config file these very settings are read from. Defaults are permissive, so on a stock node each of those was one fs_read away, and a caller holding fs could have edited away the veto meant to confine it.
The refusal names itself, so it is not mistaken for a narrow allow-roots and answered by widening one. And it bounds the file tools, not the machine: a command session running as an account with permission to read those files still can — closing that door is what allow-tools without command, or allow-admin: false, is for. allow-roots does not bound command either, and its workdir is deliberately not path-checked: the command string is unconstrained, so a confined working directory would refuse nothing that cd does not reach anyway.
Since agent 0.4.37 the tools that walk a tree — fs_list with recursive, fs_glob, fs_grep — ask the same question again at each directory they descend into, and prune the subtree when the answer is no. Before that only the root they were given was checked, so on a stock node (allow-roots defaulting to /) a recursive listing of /etc named the files in /etc/nobgp and a grep of /etc returned their contents, one call after fs_read refused those exact paths by name.
fs_list still names the refused directory itself before pruning it — a non-recursive listing of its parent already does, so hiding it would only make the two views disagree about what exists. What it does not do is show you anything inside.
Recovering a node you have locked down
If you drop command from allow-tools, or set allow-admin: false on a node with no unprivileged user to fall back to, remote execution is gone until you change the setting back — including from a published terminal service. nobgp config warns you at the moment of the change for exactly this reason, and asks for confirmation when the change turns remote execution off.
Confirmed before it is written
Since agent 0.4.52 the settings that can close the last door are checked before the file is saved, so a refusal or a declined prompt leaves the node exactly as it was.
| Change | Interactive | Non-interactive |
|---|---|---|
--allow-tools dropping command | asks | refused unless --yes |
--allow-admin=false with nothing to fall back to | asks | proceeds, warning in the log |
--user "" (clearing the account) | asks | proceeds, warning printed |
--user naming an account that does not exist, or one that is uid 0 | refused | refused |
Dropping command is the only one that stops a scripted caller, and the asymmetry is deliberate. allow-admin: false in its usual shape still leaves fs and command running as the unprivileged account; dropping command leaves nothing at all. It also fails at the worst possible moment: a command call is non-interactive by construction — there is no terminal on the far end — so the warning printed after the write was going out on the very session the write had just closed. --yes costs a deliberate caller one flag and refuses the accidental one.
Clearing the account is --user "". An empty value with no --user flag on the command line is read as "never set" and re-filled with the install account, so the empty string is how you say you mean it. On a node whose agent is the superuser, clearing it means every admin: false call is refused rather than narrowed — unelevated never means root — and the agent says so before saving. The clear survives restarts but not a reinstall: nobgp service install and registration re-capture the install account, since a fresh enrollment has no other way to learn one.
The allow-admin: false warning is repeated at every config load, and since agent 0.4.52 it names which of three states the node is in: sessions narrowed to a named account, remote execution off because there is nothing to narrow to, or no effect at all — on a node whose agent is not itself the superuser, an elevated request resolves to the same unprivileged identity and is served, so nothing is locked out. Earlier agents claimed a refusal there that the node was never going to make.
Getting back in
The way back in is on the machine itself (console, RDP, SSH, local keyboard):
sudo nobgp config --allow-tools=fs,command
sudo nobgp config --allow-admin=true
# Or keep the veto and name an account for unelevated work to run as
sudo nobgp config --user=deploy
The last one is the better fix on a node you meant to keep locked down: it leaves allow-admin: false in place and gives unelevated calls an identity that is not the superuser, which is what the node was refusing them for.
A published terminal service still reaches a node that kept command — narrowing allow-roots, or dropping only fs, leaves the terminal working.
Diagnostics keep answering either way: presence events are published by the router, and net_peers / net_metrics are served by the agent outside the allow-tools domains (which cover only fs and command) — so you can still see that the node is alive and connected while its tools are off.
Checking what a node will refuse
nobgp status reports the effective settings and warns when a node has narrowed its own remote access:
nobgp status
allow:
tools: [fs]
roots: [/]
admin: true
unelevated: deploy
warning: 'allow-tools excludes "command": remote execution is off on this node,
published terminal services included.'
A node that serves neither domain reports the stronger form instead — allow-tools is empty: this node serves no commands, terminal sessions or file access. Nothing remote reaches it — re-enable on-device.
From the calling side, a node that refuses a bus source says so rather than going quiet — the subscription reports refused with the reason, so "this node is not doing what I asked" is never something you have to infer from silence. A refused tool call answers the same way: permission_denied, marked not retryable, with the node's own wording naming which setting refused it. Since router 0.4.37 that holds wherever the refusal lands — a walk that is refused three directories in reads the same as one refused at the door.
The unelevated-never-means-root refusal answers with a different code on purpose: failed_precondition, not permission_denied. It is not a verdict on the caller — reading it as one would send an org Owner off to widen a role that was never the problem — and its fix is on the box (nobgp config --user) or in the next call (admin: true). Router 0.4.42 is the release that classifies it as such.
Agent 0.4.38 is the release where a node states that code itself rather than leaving the router to read it out of the message text. On an older agent the router still classifies each of the vetoes above from their wording; what upgrading buys is that a refusal phrased in some way that table does not anticipate stops arriving as a retryable internal.
Reading a message for its code cuts the other way too, and router 0.4.38 is where that was closed: an agent error quotes your own path or pattern back, so a fs_grep for allow-roots( — the grep you would write to audit exactly this page's settings — used to answer permission_denied, a veto no node ever made. The vetoes are matched on the node's full wording now, never on a token you can type, so a permission_denied on these settings is one the node actually issued.
The fourth line is not a veto
Since agent 0.4.42 the allow: block carries unelevated — what an admin: false call actually runs as on this node — or unelevated_refusal in its place when nothing can. Exactly one of the two is present.
allow:
tools: [fs, command]
roots: [/]
admin: true
unelevated_refusal: 'node "web-server-1" is not configured for unelevated
execution: no account is set, so it would run as the superuser. Configure an
account with `nobgp config --user <account>` on the node, or pass admin: true
to run as root deliberately'
That is the unelevated never means root state read off the box, rather than discovered by making a call — the text is the same refusal a caller would have received. Every operation on such a node needs admin: true, and were admin also false the node would serve nothing remotely at all.
On a Windows node with a second identity whose agent is 0.4.45 or earlier, read this line as the file answer: it names the account fs_* and file calls run as, while command and terminal sessions on the same node still refuse anything but LocalSystem. From agent 0.4.46 it is the answer for both.
A fifth key joins it on agent 0.4.46 — unelevated_is_admin, and only when it is true:
allow:
tools: [fs, command]
roots: [/]
admin: true
unelevated: alice
unelevated_is_admin: true
That says the account in unelevated is itself administrative, so admin: false on this node is a drop from LocalSystem to Administrator and no further. It is the on-box half of what network_directory reports fleet-wide as info.user_is_admin, and it is present only when true — the surprising answer is the one worth a line, and a false on every Unix node would be noise.
It is read from the gate, never from the user key, because the two differ exactly when it matters: an account that has since been deleted, or a user: root, reads fine in the config file and is refused at the call. nobgp show prints what was configured; this line prints what the node will do with it.
It exists because admin is only half the gate and the other half had no on-box answer. The router reports it fleet-wide through network_directory, but that tool is organization-scoped and therefore absent from a node's own local MCP surface — so an assistant standing on the machine could only learn it by making a call and reading the refusal. The local status tool reports the same block now, for that node only; a peer's identity is still network_directory's to report.
Related
- Interactive Terminals — the two access paths these settings govern
- Publishing Services — how terminal services are created and authorized
- CLI Reference — full flag reference
- MCP Reference → Execution identity — the caller-authority half of the superuser gate
- Organizations & Teams — which roles may ask to elevate