Skip to main content

Permissions and Risks

Read this before granting anything

A grant in mcp-sudo.yaml gives an AI agent root on your host - an agent that follows instructions found in the files, web pages and logs it reads. Several grants that look narrow are, in practice, full root: write access to /etc, services/manage, unrestricted sysctl writes. Grant the least a task needs, limit it with paths:, network: and sysctl:, and assume that anything an agent may do, someone who controls its input may make it do.

In stdio mode (mcpd stdio) none of this applies: there are no grants, and nothing runs as root.

This page is about the defaults - what a user can do with no grant, and what each kind of grant adds - and about the grants that are riskier than they look. The syntax of every option is on Sudo Privileges.

Who a call runs as​

Every tool call runs in a separate worker process, as the OS account with the same name as the MCP user (alice in users.yaml → the Linux account alice):

  • Without privileged: true - as that account, with its groups. The kernel decides what it may read, write or signal, exactly as for a shell of that account. mcp-sudo.yaml is not consulted, except for the network: limits below.
  • With privileged: true - as root, but only for a tool granted to that user in mcp-sudo.yaml, and only within the grant's paths:/sysctl: limits. Anything else is refused before a worker starts.

So the OS account is the first permission, and it matters as much as the grants:

  • An MCP user must not be a root account - it would be root on every call, without any grant. A users.yaml listing a user whose OS account has uid 0 is a config error: mcpd doesn't start with it, daemon/reload-config and linuxctl edit mcpd config users reject it, linuxctl create mcpd user won't create one, and a worker refuses to run as one.
  • Groups count. An account in docker, lxd or disk is root in all but name (it can start a privileged container, or read raw disks); adm and systemd-journal read every log. Create a dedicated account for each agent, in no such group: useradd --system --no-create-home agent-web.

The defaults​

What the user hasWhat the agent can do
A token, no entry in mcp-sudo.yamlCall every tool and resource as its own OS account: read what that account can read, change what it owns. No root. daemon/reload-config, every docker/* tool and the devices:///kernel:// resources are refused. network/curl and network/ping reach any address - see below.
allowed: true on a tool without a path (services/manage, processes/delete, kernel/system-control, logs/*, ...)That tool as root, with any arguments: any service, any process, any kernel parameter.
allowed: true on a path tool (files/*, disks/usage, disks/free)Nothing, and the config is rejected: a path tool must list its paths:. paths: ["/"] for the whole filesystem - written out, so whoever reads the config sees it.
allowed: true + paths: [/var/log]That tool as root inside /var/log only (/var/log/../../etc is judged as /etc and refused). Symlinks are not followed there, so one planted in /var/log can't lead out of it.
network: on network/curl/network/pingLimits every call of the tool - privileged or not - to the allowed destinations. Without it: any destination.
sysctl: write_keys: on kernel/system-controlRoot writes only to the listed keys. Without it: any key. Reads never need root.
resources:Reading those resources as root: "*" for fixed ones (devices://usb), a path prefix for file:// (/var/log), "" for all of a template (file:// → every file).

Examples​

A user with only a token:

# mcp-sudo.yaml: no entry for "reader"
CallResult
files/read /etc/os-release✅ world-readable
files/read /etc/shadow❌ permission denied - the kernel refuses the reader account
files/read /etc/shadow, privileged: true❌ user reader is not authorized to run files/read on path /etc/shadow as root
processes/delete another user's process, privileged: true❌ Permission denied. Hint: You are not authorized to use 'privileged: true' for this tool in mcp-sudo.yaml
services/manage restart nginx❌ refused by systemd: a non-root account may not manage system units
network/curl http://169.254.169.254/✅ allowed - no network: block

A log reader:

users:
logs:
privileged:
tools:
files/read:
allowed: true
paths: [/var/log]
files/list:
allowed: true
paths: [/var/log]
logs/journal-control:
allowed: true
CallResult
files/read /var/log/auth.log, privileged: true✅
files/read /var/log/../../etc/shadow, privileged: true❌ user logs is not authorized to run files/read on path /var/log/../../etc/shadow as root
files/read /var/log/link-to-shadow (a symlink), privileged: true❌ refusing to follow a symbolic link
files/update /var/log/x, privileged: true❌ not granted

Grants that amount to full root​

Each of these lets an agent turn its grant into unrestricted root - by writing a file root executes, by starting a program as root, or by reading a secret. Treat them as "root on this host", and grant them only where that is acceptable.

GrantWhy it is full root
files/create / files/update with paths covering /etc, /root, /usr, /var/spool/cron, /etc/systemd, or /Write /etc/sudoers.d/x, a cron job, /root/.ssh/authorized_keys, a systemd unit, a binary on PATH.
files/create / files/update on mcpd's own config directory (/etc/mcpd/configs)Rewrite mcp-sudo.yaml or users.yaml and grant itself everything at the next reload or restart. There is deliberately no MCP tool that edits them - don't hand one over through files/*.
files/chmod / files/chown on / or system directoriesMake a copy of a shell setuid root, or /etc/shadow world-readable.
files/read on /, or file:// with ""/etc/shadow, SSH host keys, mcpd's TLS private key (/etc/mcpd/configs/tls/mcpd.key), application secrets, other users' files.
services/manageStart, stop, restart, enable any unit - stop ssh or mcpd itself; together with any write to /etc/systemd, run any program as root.
kernel/system-control without sysctl.write_keyskernel.core_pattern and kernel.modprobe name programs the kernel runs as root; other keys can cut the host off the network.
processes/deleteSignal any process: sshd, mcpd, PID 1, a database mid-write.
docker/execA command as root inside the target container. Anything the container can reach, the agent can reach - and a container started --privileged, or with the host's filesystem or Docker socket mounted, is the host. Always give this one a containers: list.
docker/manage without containers:Stop, kill or remove any container on the host - including mcpd's own, and a database mid-write. containers: ["*"] means the same thing, written out.
Any docker/* grantTalking to the Engine API socket is the docker group, which is root - which is why every docker/* tool is root-or-nothing and invisible in tools/list without the grant. The containers: globs, matched against name and resolved ID, are the whole boundary.
MCP user whose OS account is in docker/lxd/diskRoot through the group, even with no grant at all (see above).

Also dangerous without any grant​

  • network/curl, network/ping connect from the server: to localhost services, the private network, the cloud metadata endpoint (169.254.169.254, which can hand out cloud credentials). A prompt-injected agent can be told to fetch an internal URL and pass the answer on. Give every user that doesn't need internal access a network: block with deny_private: true.
  • Reading logs and process lists shows command lines, environment-derived arguments and log lines - sometimes with passwords or tokens in them.
  • The token is the user. Whoever has it acts as that user. Keep TLS on (the default), give each agent its own user and token, and rotate a token (linuxctl update mcpd user NAME) whenever it may have leaked.

Crontabs​

A crontab is a list of commands the system runs as a user, on a schedule. Reading one is a read; writing one is scheduling code. How it works: your own crontab is free (like any other unprivileged call - the worker runs as your account, and crontab acts on it); another user's crontab needs privileged: true (meaning "act on another account", not root: the worker runs as that account) and a rule naming it, view or edit, in the cron/manage grant (rules); the privileged listing shows only the crontabs of accounts you have view on. Whichever way you use it, know these risks:

RiskWhat can happenWhat mcpd does, and what is left to you
PersistenceA job written by an agent outlives its session, its token and the token's rotation. Revoking an MCP user does not remove a job it already wrote.Every write is an audit-log line (caller, target, size, line count, content hash) and every crontab can be read back, so a job can be found and removed. Own-crontab edits are open by design; to forbid them for an account, list it in /etc/cron.deny (mcpd has no switch for it).
Code as another user, or as rootedit on a user's crontab is arbitrary commands as that user; on root's, as root.mcpd does not inspect the text it writes - the content is stored exactly as given, so a job can run any command, including one that downloads and runs something (`curl …
Argument injectionA user name such as -l becoming a crontab option.The name is validated by the daemon (allowed characters, must start with a letter or digit, must be an existing account). There is no crontab -u <name> at all: the worker is started as that account and runs a fixed crontab -l or crontab -; the content goes through stdin, never a shell.
Bypassing cron.denyroot can write the crontab of a user the administrator denied.Not possible through mcpd: the worker runs as the target account, so crontab applies /etc/cron.allow and /etc/cron.deny to it as usual.
Secrets in the textCrontabs and the journal's CMD lines hold whatever was typed into a command, including passwords.Reading needs view. Audit logs record size and hash, never the content. Keep secrets out of crontabs regardless.
Reading the spool as rootThe listing of users with a crontab reads a root-owned directory.File names are treated as untrusted: checked against the account database, no symlinks followed, anything that is not a regular file skipped. The list shows only users the rules allow view for.
Oversized or abusive contentA huge file, or a job every minute.A size cap on content; cron's own limits apply.
Mail as an exfiltration pathMAILTO= sends job output to any address when a mail agent is installed.Part of what edit means; not filtered.
Concurrent editsA whole-file write replaces someone else's later change.Optional if_match (the hash you read) refuses a write if the crontab changed.
Cron not runningA write succeeds while the cron service is stopped; nothing runs.Not detected by the tool; check the service with linuxctl describe system cron.

Before granting edit on a crontab, treat it as you would a shell: give it only to accounts and users you would give a shell to, and read the audit log.

Recipes​

Read-only diagnostics - load, memory, disks, processes, services, logs readable by the account: no entry at all, plus a network limit.

users:
diag:
privileged:
tools:
network/curl:
network: { deny_private: true }
network/ping:
network: { deny_private: true }

Log reader - as above, plus root reads under /var/log and the journal (the log reader example).

Web server operator - its config and site files, reloading nginx; nothing else as root.

users:
web:
privileged:
tools:
files/read:
allowed: true
paths: [/etc/nginx, /var/www, /var/log/nginx]
files/update:
allowed: true
paths: [/etc/nginx/sites-available, /var/www]
files/list:
allowed: true
paths: [/etc/nginx, /var/www, /var/log/nginx]
services/manage:
allowed: true # any unit - see the table above; prefer this only on a host that is nginx and nothing else
network/curl:
network:
allow: [127.0.0.1] # its own sites via localhost
deny_private: true

services/manage has no per-unit limit yet, so on a shared host this recipe is still "root" through it - leave it out there, and restart nginx yourself.

Before granting - a checklist​

  1. Does the task need root at all? Most diagnostics don't: try the call without privileged: true first.
  2. Is the MCP user a dedicated OS account, not root and not in docker, lxd, disk, adm?
  3. Is every path tool limited to the directories the task needs - and none of them /etc, /root, /usr, /etc/systemd, /etc/mcpd for writes?
  4. Is the grant on the "full root" list above? Then decide as if giving a root shell.
  5. Does the user have network: limits on network/curl/network/ping?
  6. Does kernel/system-control have write_keys, or no allowed at all?
  7. After the change, read the reload's summary (linuxctl edit mcpd config sudo prints it; the daemon logs it as an audit=true line) and check it says what you meant.