DevOps MCP is designed for trusted self-hosted environments. It is not a public-facing service.
What confirmed=true actually is. The flag is set by the model itself, in the same
call it is meant to authorize. Nothing stops a model from setting it, and in five months
of real use it was set on 47% of ssh_exec calls. So the allowlist is a guard against an
accidental destructive command, not a security boundary. The boundaries that do hold are
the permission prompt in the MCP client and the fact that the endpoint listens on
localhost only. Tool annotations (readOnlyHint, destructiveHint) exist so the client
can enforce that prompt by protocol instead of by convention.
What it protects against:
- SSH read-only by default — only safe, read-only commands are allowed without
confirmed=true(uptime, df, cat, grep, journalctl, systemctl status, docker ps, etc.) - Conditionally safe commands —
sed,curl,wget,findallowed only when no mutating flags are present (sed -i,curl -X POST,find -execrequireconfirmed=true) - SSH command injection — blocks
$(...)and backtick substitution; output redirection always blocked - Path traversal in log access —
log_tailresolves symlinks and checks against an allowlist - Unauthorized Docker container control —
stopandrestartrequireconfirmed=true - Privilege escalation — non-root container (mcpuser), all Linux capabilities dropped,
no-new-privileges, source mounted read-only - Remote file access —
file_getandfile_putrefuseshadow,gshadow,sudoers,authorized_keysand private keys outright;file_putneedsconfirmed=true, keeps mode and owner, and writes through a temporary file in the same directory - Verified writes —
file_put's optionalverify_cmdrestores the previous content when the check fails. Because that command runs automatically as part of a write, it is limited to config tests (nginx -tand the like) with no operators, redirects or substitutions; general execution stays inssh_exec, where it appears as its own tool call the user can see and refuse - Audit trail — every tool call logged to
/audit/audit.jsonlwith an outcome ofok,refusedorerror; write failures emit a warning instead of being silently swallowed. File bodies and other long values are stored as<N chars, sha256=...>, so the log proves what was written without becoming a copy of it - Pooled connections are per credential — a kept-open SSH connection is keyed by host, port, username and which key or password opened it, so one host's session is never handed to a different credential; passwords are stored as a hash, sessions close after five minutes idle and on shutdown, and
ssh_sessionscloses them on demand - Host keys are checked by default — once
known_hostsis populated, a host that is not in it is refused rather than warned about; an empty file falls back to warn mode so a fresh install is usable - Jump hosts carry bytes, not credentials — with
jump_hostthe target's authentication happens end to end inside the tunnel, so its password never appears in the process list of the intermediate server, which is what runningsshpassas a command there would do - SSH password auth disabled — password authentication off by default (ALLOW_SSH_PASSWORD=false)
What it does NOT protect against:
- A compromised AI client with access to the MCP endpoint
- Insider threats with physical access to the server
- Docker socket abuse — mounting
/var/run/docker.sockgives significant host access by design
Intended deployment: localhost-only (127.0.0.1:8765), accessed via SSH tunnel from a trusted machine.
If you find a security issue, please do not open a public GitHub issue.
Email: oleg.v.usoltsev@gmail.com
Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
You can expect a response within 72 hours.