How do I set up my machine so that a model cannot read my passwords?
The setup of your workstation decides, not the server. Three levels (uncompromising, normal, reckless) side by side with what each closes and what it costs, then the steps in order with the real output of each command, and the command that checks the result.
Contents
Your workstation decides, not the server
Whether a model can read your passwords is settled by how your workstation is set up, not by the server. The server closes the routes that it can close: the value of a credential never appears in a tool result, in the journal or on a command line; it is typed only on the origin it is bound to; a payment, a deletion or a change of secret waits for your confirmation. A model that also has a shell is another matter. It reaches whatever its account reaches: if it runs under the account that holds your encrypted directory, it lists the files there, talks to the browser on its debugging port and rewrites the profile the server obeys. No check inside the server can refuse what the system allows.
A rule written in a configuration file is something a model with a shell can edit. A refusal by the operating system, because the file belongs to another account, is not. You choose how much of that refusal you want, among three levels: uncompromising, normal and reckless (not recommended). The steps build them in the opposite order, from the workstation you have to the strictest one. This page puts the three side by side, then gives, for each, the steps in order, every command with its real output, and the check that tells you where your workstation stands.
In short: only the uncompromising level closes the capture of what you type and sudo. The normal level closes the encrypted directory, the browser profile, the debugging port and the journal, but not the account under which the model runs. The reckless level closes nothing: it is the state of a workstation where nothing was chosen.
The three levels side by side
A level is a property of the workstation, not a setting of the server. It depends on which account holds the encrypted directory, the browser and the server, and on which account runs the model.
| Level | Under your account | Under diwall-mcp | Under agent |
|---|---|---|---|
| Uncompromising | your screen, the confirmation windows, the administration | the encrypted directory, the browser profile, the browser, the server | the MCP client and the model |
| Normal | the MCP client and the model, your screen, the confirmation windows | the encrypted directory, the browser profile, the browser, the server | nothing |
| Reckless (not recommended) | everything | nothing | nothing |
| Level | Closes | Leaves open | Costs |
|---|---|---|---|
| Uncompromising | the ten routes: nine by the kernel, route 4 by the server | none | one more account (agent), in whose home directory everything the model writes ends up |
| Normal | routes 1, 2, 3, 5, 6 and 8 by the kernel, route 4 by the server | routes 7, 9 and 10 | one account (diwall-mcp), one systemd unit and one firewall rule |
| Reckless (not recommended) | nothing by the kernel | nine routes of ten; the tenth has no object | nothing |
- Uncompromising moves the client and the model to a third account. It reaches neither your screen, nor your startup files, nor your administrator rights.
- Normal moves the encrypted directory, the browser and the server to a second account. It stops a model that is careless or misled, and the ordinary injection, which go through the encrypted directory. It does not stop a model built to harm: that model runs under your account and reaches your shell and
sudo. - Reckless is the workstation that the installation gives if you do nothing more: the model and the encrypted directory share one account. The kernel closes nothing; only rules hold (the configuration of the client, the checks of the server).
The ten routes a model can take to a secret
A route is closed by the kernel when the system refuses whatever the model tries, closed by the server when the code of the server refuses it and the model can change neither that code nor its configuration, and open otherwise. The level of a workstation is that of its most open route.
| Route | What a model with a shell tries | Uncompromising | Normal | Reckless (not recommended) |
|---|---|---|---|---|
| 1. Read the encrypted directory: credentials, journal, captures, downloads | ls and cat in the mount point | kernel | kernel | open |
| 2. Read the browser profile: the cookies of the open sessions | read the profile directory | kernel | kernel | open |
| 3. Drive the browser without the server, through its debugging port | connect to port 9222 | kernel | kernel | open |
| 4. Pose as the browser: listen on the port before it, so that the server attaches and types a password there | open a listener on the port, then call status | server | server | not applicable |
| 5. Change what the server allows: operator profile, unit, command of the server | write the profile and the unit | kernel | kernel | open |
| 6. Erase or rewrite its trace in the journal | write in the journal directory | kernel | kernel | open |
| 7. Answer the confirmation window in your place | xdotool, /dev/uinput, the socket of your screen | kernel | open | open |
8. Read the memory of the server, the browser or gocryptfs | /proc/<pid>/mem, gdb -p | kernel | kernel | open |
| 9. Capture a password you type | write an alias into your shell startup files, read the keyboard events | kernel | open | open |
| 10. Become administrator | sudo -n true, sudo -l, inject keystrokes into your terminal | kernel | open | open |
Routes 7, 9 and 10 stay open at the normal level because the model runs under the account that owns your screen, your startup files and your sudo rights. At the uncompromising level the screen refuses the model account, the startup files and the keyboard events are closed to it, and it has no sudo. Routes 7 and 9, and the xhost command used below, depend on the display server of your session. They were measured twice: on X11, in a KDE session, and on Wayland, in a GNOME session; the section on a Wayland session below gives what is different there.
What every level keeps
The server does these at every level, because its code does them:
- the value of a credential is never in a tool result, in the journal or on a command line;
- a credential is typed on its own origin only;
- a payment, a deletion or a change of secret waits for your confirmation.
These checks protect from a leak through the tools. They do not protect from a model with a shell under the account of the encrypted directory, which is why the levels exist. A level protects the value of a secret, not the use of a session that is open in the dedicated browser: the model can use that session, within the limits of the red list, at every level (Limits, Connect an MCP client).
The report of isolation tells you your level
mcp-server-diwall-browser-setup isolation runs from the account of the model. It tries each of the ten routes with the trial of the table above, writes nothing, asks for no password, starts no sudo and reads no content of the server. For each route it says closed by the kernel, closed by the server, open, not applicable, or not measurable from here, then the level reached. The subcommand comes with version 0.0.2 of the server.
- Without an argument it takes
diwall-mcpas the server account if that account exists, and your account otherwise.isolationfollowed by an account name takes that account instead;--port PORTnames another debugging port;--jsonprints the verdicts for a script. - A route that needs the encrypted directory is not measurable while the directory is not mounted, and route 6 is not measurable before the server has run once, because the journal directory does not exist yet: the report lists such a route and does not count it as closed.
- Route 4 is tried by the installed code of the server on a simulated table where the model account listens, not by a real listener. Route 10 is tried without
sudo: groups, readablesudoersrules and the terminal injection switch. A rule that onlyrootcan read is invisible from the model account, and the report says so.
About the commands and their outputs
The commands were played on two Debian 13 workstations, one level after the other, then undone in the reverse order: one with a KDE session on X11, which gave the outputs of the three levels, and one with a GNOME 48.7 session on Wayland, which played the same commands again and gave the outputs of the section on a Wayland session. The outputs are copied from those runs, with replacements: your account is written alice, its home directory ~, and the scratch folder of the Wayland workstation /var/tmp/test. The progress lines of apt and the explanatory paragraphs that the setup assistant prints around the password prompts are left out. System commands ran with LC_ALL=C so that their messages read the same everywhere; yours follow your language, and the version numbers shown are those of what was installed that day. The messages of the setup assistant are in French. The trace lines that a shell prints for a command run in a subshell ($$) are left out of the Wayland outputs.
Every command uses the same example values: the operator is the account you are logged in as (alice in the outputs, $(id -un) in the commands), the server account is diwall-mcp, the model account is agent, the encrypted directory is kept in /var/lib/diwall-mcp, and the debugging port is 9222.
use_pty. Debian 13 ships Defaults use_pty in /etc/sudoers: the file of the sudo package 1.9.16 has it on line 15. Both workstations had it. The Wayland workstation has one more rule, for another program, and nothing else differs from the file of the package:
$ dpkg-query -W '-f=${Version}\n' sudo
1.9.16p2-3+deb13u2
$ mkdir -p /var/tmp/test/apt-sudo
$ cd /var/tmp/test/apt-sudo
$ apt-get download sudo
0% [Working]
Get:1 http://deb.debian.org/debian trixie/main amd64 sudo amd64 1.9.16p2-3+deb13u2 [2086 kB]
0% [1 sudo 9772 B/2086 kB 0%]
100% [Working]
Fetched 2086 kB in 0s (8236 kB/s)
$ dpkg-deb --fsys-tarfile sudo_1.9.16p2-3+deb13u2_amd64.deb
$ tar -xO ./etc/sudoers
$ grep -n use_pty
15:Defaults use_pty
$ diff /dev/fd/63 /dev/fd/62
6a7
> %alice ALL=(ALL) NOPASSWD:/usr/bin/veracrypt
Setting a level up, from the starting workstation
The steps below run in the order in which a level is built: each one adds to the one before it, so the agent account cannot exist before diwall-mcp, nor diwall-mcp before the starting workstation. They end on the uncompromising level, which is the one that answers the title; stop at normal only if you accept that the model keeps your screen, your startup files and sudo.
The starting workstation, reckless (not recommended)
What it closes: nothing by the kernel. What it leaves open: every route except route 4, which has no object because the model already holds everything. Cost: none.
This is where a workstation stands after the installation and the first launch, without any further step. A model with a shell under your account lists the encrypted directory and reads the credentials, the journal and the downloads; reads the cookies of your open sessions in the browser profile; drives the browser through its debugging port without going through the server; rewrites the operator profile and the unit that starts the server; erases its trace in the journal; answers the confirmation window in your place; reads the memory of the server and of the browser; captures a password you type by writing an alias into your shell startup files; and becomes administrator if your account belongs to the sudo group. The report at the end of this level shows nine routes open of ten. The checks of the server still hold for what goes through its tools; they do not hold against a shell.
Step 1. Install the package and the tools of the next levels, from the directory that holds the package file. The isolation subcommand comes with version 0.0.2: take that package or a later one, whatever version number the output below shows.
sudo apt install ./mcp-server-diwall-browser_*_all.deb socat nftables x11-xserver-utils chromium
Note, selecting 'mcp-server-diwall-browser' instead of './mcp-server-diwall-browser_0.0.2-1_all.deb'
socat is already the newest version (1.8.0.3-1+deb13u1).
nftables is already the newest version (1.1.3-1).
x11-xserver-utils is already the newest version (7.7+11).
chromium is already the newest version (156.0.8078.73-1~deb13u1).
Installing:
mcp-server-diwall-browser
Summary:
Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 0
Download size: 0 B / 318 kB
Space needed: 755 kB / 268 GB available
Continue? [Y/n] y
Setting up mcp-server-diwall-browser (0.0.2-1) ...
mcp-server-diwall-browser installed.
Step 2. Create the encrypted directory with its operator profile, then start the dedicated browser. create asks for a password twice, then once more to open the volume, and shows a master key once: write it on paper. Chromium is used because Google Chrome opens a terms window at its first launch on a new profile and keeps the debugging port closed until it is accepted.
mcp-server-diwall-browser-setup create --browser chromium
mcp-server-diwall-browser-setup browser
Volume chiffré gocryptfs : ~/Vaults/Main.enc, ouvert sur ~/Vaults/Main.
Choose a password for protecting your files.
Password:
Repeat:
Your master key is:
<master key>
The gocryptfs filesystem has been created successfully.
Password:
Filesystem mounted and ready.
Volume ouvert. Profil écrit : ~/.config/mcp-server-diwall-browser/profile.json.
Navigateur lancé (processus 13553) sur le profil du volume, détaché de ce terminal. Ouvrez-y vous-même vos sessions sur les sites.
Step 3. Call the server once, as your client would, so that its journal exists. The probe sends the three messages of the protocol, waits, and keeps the encrypted directory and the browser from the answer of status.
{ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"status","arguments":{}}}'; sleep 6; } | mcp-server-diwall-browser 2>/dev/null | tail -n 1 | grep -o 'encrypted_directory[^}]*}\|browser\\": {[^}]*}'
encrypted_directory\": {\"state\": \"mounted\"}
browser\": {\"attached\": true, \"product\": \"Chrome\", \"version\": \"156.0.8078.73\"}
Step 4. Read the report. Nine routes are open and route 4 has no object, so the level is reckless.
mcp-server-diwall-browser-setup isolation
Essai des dix voies depuis le compte « alice » (uid 1002) ; le serveur tourne sous « alice » (uid 1002).
Rien n'est écrit, aucun mot de passe n'est demandé, aucun sudo n'est lancé, aucun contenu du serveur n'est lu.
1. Lire le répertoire chiffré : ouverte
le point de montage ~/Vaults/Main se liste depuis ce compte
2. Lire le profil du navigateur : ouverte
le dossier du profil du navigateur se voit depuis ce compte
3. Piloter le navigateur par son port de débogage : ouverte
la connexion à 127.0.0.1:9222 aboutit
4. Se faire passer pour le navigateur : sans objet
le serveur tourne sous ce compte : le modèle a déjà tout
5. Changer ce que le serveur s'autorise : ouverte
ce compte écrit dans ~/.config/mcp-server-diwall-browser
6. Effacer ou réécrire sa trace dans le journal : ouverte
ce compte écrit dans le dossier du journal
7. Répondre à la fenêtre de confirmation : ouverte
l'écran :0 accepte une connexion de ce compte
8. Lire la mémoire du serveur, du navigateur ou de gocryptfs : ouverte
ce compte ouvre la mémoire du processus 1753 de « alice »
9. Capter un mot de passe que l'opérateur tape : ouverte
ce compte écrit dans ~/.bashrc, que le shell de « alice » lit à chaque ouverture
10. Devenir administrateur : ouverte
ce compte est du groupe « sudo » : il devient administrateur, avec un mot de passe que la voie 9 capte, ou sans lui si une règle l'en dispense
Niveau atteint : inconscient (déconseillé)
Normal level, with the server on a second account
What it closes by the kernel: routes 1, 2, 3, 5, 6 and 8. By the server: route 4. What it leaves open: routes 7, 9 and 10. Cost: the diwall-mcp account, a systemd unit and a firewall rule; the downloads and the captures are in the encrypted directory of diwall-mcp, which you read with sudo -u diwall-mcp (the mount has no allow_other, so root is refused too); to take a file out of that directory, see the paragraph that follows step 9.
Step 1. Close the volume of the starting workstation. close stops the browser first.
mcp-server-diwall-browser-setup close
Fermeture du navigateur avant le démontage.
Volume fermé : ~/Vaults/Main.
Step 2. Create the server account. It has no password, no sudo and no group but its own; adduser puts it in users, which the second command removes.
sudo adduser --disabled-password --gecos "" diwall-mcp
sudo gpasswd -d diwall-mcp users
id diwall-mcp
usermod: no changes
Removing user diwall-mcp from group users
uid=1000(diwall-mcp) gid=1000(diwall-mcp) groups=1000(diwall-mcp)
Step 3. Create the encrypted directory under that account, outside its home directory, so that the refusal of route 1 comes from the mount and not from the mode of a directory. The assistant asks for the password in the same way as for the starting workstation.
sudo install -d -m 755 -o diwall-mcp -g diwall-mcp /var/lib/diwall-mcp
sudo -iu diwall-mcp mcp-server-diwall-browser-setup create --crypt-dir /var/lib/diwall-mcp/Main.enc --mount-dir /var/lib/diwall-mcp/Main --browser chromium
Volume chiffré gocryptfs : /var/lib/diwall-mcp/Main.enc, ouvert sur /var/lib/diwall-mcp/Main.
Choose a password for protecting your files.
Password:
Repeat:
Your master key is:
<master key>
The gocryptfs filesystem has been created successfully.
Password:
Filesystem mounted and ready.
Volume ouvert. Profil écrit : ~/.config/mcp-server-diwall-browser/profile.json.
Step 4. Give diwall-mcp access to your screen, which lasts until your session closes, then start its browser. The window opens on your screen. DISPLAY=:0 is the display of your session: echo $DISPLAY tells. On a Wayland session xhost opens XWayland, the X server that GNOME starts for the applications that do not speak Wayland, and DISPLAY is the display of XWayland (:1 on the workstation measured: :0 was the server of the login screen); the section on a Wayland session below gives the outputs. This access is the one place where the boundary is opened on purpose: the account of the server can type on your screen, not only open the confirmation windows. It is acceptable because every level trusts the code of the server and treats the model as the attacker.
xhost +SI:localuser:diwall-mcp
sudo -iu diwall-mcp env DISPLAY=:0 mcp-server-diwall-browser-setup browser
localuser:diwall-mcp being added to access control list
Navigateur lancé (processus 14123) sur le profil du volume, détaché de ce terminal. Ouvrez-y vous-même vos sessions sur les sites.
Step 5. Write the bridge: a systemd socket that your account may open, and a service that starts one server per connection under diwall-mcp. SocketGroup is the group of the account that runs the client.
sudo tee /etc/systemd/system/mcp-server-diwall-browser.socket >/dev/null <<EOF
[Unit]
Description=Bridge to the browser MCP server: socket of the model account
[Socket]
ListenStream=/run/mcp-server-diwall-browser/mcp.sock
Accept=yes
SocketUser=root
SocketGroup=$(id -un)
SocketMode=0660
DirectoryMode=0755
[Install]
WantedBy=sockets.target
EOF
sudo tee /etc/systemd/system/mcp-server-diwall-browser@.service >/dev/null <<'EOF'
[Unit]
Description=Browser MCP server, one connection (%i)
[Service]
User=diwall-mcp
WorkingDirectory=~
Environment=DISPLAY=:0
ExecStart=/usr/bin/mcp-server-diwall-browser
StandardInput=socket
StandardOutput=socket
StandardError=journal
NoNewPrivileges=yes
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server-diwall-browser.socket
stat -c '%A %U %G %n' /run/mcp-server-diwall-browser/mcp.sock
Created symlink '/etc/systemd/system/sockets.target.wants/mcp-server-diwall-browser.socket' -> '/etc/systemd/system/mcp-server-diwall-browser.socket'.
srw-rw---- root alice /run/mcp-server-diwall-browser/mcp.sock
Step 6. Ask the server for its status through the bridge, with the probe of the starting workstation.
{ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"status","arguments":{}}}'; sleep 6; } | socat - UNIX-CONNECT:/run/mcp-server-diwall-browser/mcp.sock | tail -n 1 | grep -o 'encrypted_directory[^}]*}\|browser\\": {[^}]*}'
encrypted_directory\": {\"state\": \"mounted\"}
browser\": {\"attached\": true, \"product\": \"Chrome\", \"version\": \"156.0.8078.73\"}
Step 7. Close the debugging port to every account but diwall-mcp. The refusal admin-prohibited makes a connection fail at once with No route to host, an error that a closed port never gives, so the rule can be told from a browser that is not running. The port is the browser.debug_port of the profile. nftables.service loads the rule at each boot.
sudo install -d /etc/nftables.d
sudo tee /etc/nftables.d/diwall-mcp.nft >/dev/null <<'EOF'
table inet diwall_mcp {
chain output {
type filter hook output priority 0; policy accept;
oifname "lo" tcp dport 9222 meta skuid != "diwall-mcp" reject with icmpx type admin-prohibited
}
}
EOF
echo 'include "/etc/nftables.d/diwall-mcp.nft"' | sudo tee -a /etc/nftables.conf
sudo nft -c -f /etc/nftables.conf
sudo systemctl reload nftables
systemctl is-enabled nftables
include "/etc/nftables.d/diwall-mcp.nft"
enabled
On a workstation where nftables.service is disabled (systemctl is-enabled nftables answers disabled), reload fails with nftables.service is not active, cannot reload and the rule is not loaded. Do not start the service before reading /etc/nftables.conf: its own rules apply as soon as it starts, and the file of the workstation measured refuses incoming connections by default and has no rule for the SSH port. Load the table alone instead; it lasts until the next boot:
$ systemctl is-active nftables
inactive
$ sudo nft -f /etc/nftables.d/diwall-mcp.nft
$ sudo nft list ruleset
table inet diwall_mcp {
chain output {
type filter hook output priority filter; policy accept;
oifname "lo" tcp dport 9222 meta skuid != 1001 reject with icmpx admin-prohibited
}
}
$ systemctl is-active nftables
inactive
Step 8. Try three routes by hand from your account (the model account at this level). Route 3 is refused by the rule and diwall-mcp still connects; route 1 is refused by the mount; route 10 depends on your sudo rules.
bash -c 'exec 3<>/dev/tcp/127.0.0.1/9222'
sudo -u diwall-mcp bash -c 'exec 3<>/dev/tcp/127.0.0.1/9222' && echo "diwall-mcp connects"
ls /var/lib/diwall-mcp/Main
sudo -n true && echo "sudo without a password: yes"
bash: connect: No route to host
bash: line 1: /dev/tcp/127.0.0.1/9222: No route to host
diwall-mcp connects
ls: cannot access '/var/lib/diwall-mcp/Main': Permission denied
sudo without a password: yes
The line sudo without a password: yes is what the probe found on the account that was measured, not a setting to obtain. Where that line does not appear, route 10 is not closed for all that: a sudo that asks for a password stays open as long as route 9 can capture the password you type. The line is written by the echo of the command when sudo -n true succeeds, which needs a NOPASSWD rule: on an account without one, sudo answers sudo: a password is required, the line is not written and the exit code is 1 (outputs in the section on a Wayland session).
Step 9. Read the report from your account.
mcp-server-diwall-browser-setup isolation
Essai des dix voies depuis le compte « alice » (uid 1002) ; le serveur tourne sous « diwall-mcp » (uid 1000).
Rien n'est écrit, aucun mot de passe n'est demandé, aucun sudo n'est lancé, aucun contenu du serveur n'est lu.
1. Lire le répertoire chiffré : fermée par le noyau
le noyau refuse ce compte au point de montage (/var/lib/diwall-mcp/Main) : le montage n'a pas d'accès pour un autre compte
2. Lire le profil du navigateur : fermée par le noyau
le noyau refuse ce compte au dossier du profil, qui est dans le volume
3. Piloter le navigateur par son port de débogage : fermée par le noyau
le noyau écarte la connexion au port 9222 (règle de pare-feu par compte)
4. Se faire passer pour le navigateur : fermée par le serveur
le serveur installé refuse un port tenu par un autre compte (debug_endpoint_other_account)
5. Changer ce que le serveur s'autorise : fermée par le noyau
le profil de l'opérateur, l'unité du service et le code du serveur ne s'écrivent pas depuis ce compte
6. Effacer ou réécrire sa trace dans le journal : fermée par le noyau
le noyau refuse ce compte au dossier du journal, qui est dans le volume
7. Répondre à la fenêtre de confirmation : ouverte
l'écran :0 accepte une connexion de ce compte
8. Lire la mémoire du serveur, du navigateur ou de gocryptfs : fermée par le noyau
le noyau refuse ce compte à la mémoire des 13 processus essayés de « diwall-mcp »
9. Capter un mot de passe que l'opérateur tape : ouverte
ce compte écrit dans ~/.bashrc, que le shell de « alice » lit à chaque ouverture
10. Devenir administrateur : ouverte
ce compte est du groupe « sudo » : il devient administrateur, avec un mot de passe que la voie 9 capte, ou sans lui si une règle l'en dispense
Niveau atteint : normal
Taking a file out of the encrypted directory. cp run as sudo -u diwall-mcp is refused when the destination folder is yours, because diwall-mcp cannot write there. Either let your own account write the file, by reading it as diwall-mcp and redirecting the output, or copy into a folder that anyone may write in, where the copy then belongs to diwall-mcp:
$ mkdir -p /var/tmp/test/recu
$ sudo -n -u diwall-mcp sh -c 'mkdir -p /var/lib/diwall-mcp/Main/downloads && echo "contenu d essai" > /var/lib/diwall-mcp/Main/downloads/essai.txt'
$ sudo -u diwall-mcp cp /var/lib/diwall-mcp/Main/downloads/essai.txt /var/tmp/test/recu/
cp: cannot create regular file '/var/tmp/test/recu/essai.txt': Permission denied
$ sudo -u diwall-mcp cat /var/lib/diwall-mcp/Main/downloads/essai.txt
$ ls -l /var/tmp/test/recu/
total 4
-rw-rw-r-- 1 alice alice 16 Nov 1 00:49 essai.txt
$ cat /var/tmp/test/recu/essai.txt
contenu d essai
$ rm /var/tmp/test/recu/essai.txt
$ chmod 1777 /var/tmp/test/recu
$ sudo -u diwall-mcp cp /var/lib/diwall-mcp/Main/downloads/essai.txt /var/tmp/test/recu/
$ ls -l /var/tmp/test/recu/
total 4
-rw-rw-r-- 1 diwall-mcp diwall-mcp 16 Nov 1 00:49 essai.txt
Under a Wayland session (GNOME)
The commands above were played on X11, then again on a GNOME session on Wayland (Debian 13, GNOME Shell 48.7), and the same ten routes were tried there. The outputs of this section come from that second workstation.
The session. Ask your session which one it is and where XWayland is:
$ systemctl --user show-environment
$ grep -E '^(DISPLAY|WAYLAND_DISPLAY|XDG_SESSION_TYPE)='
DISPLAY=:1
WAYLAND_DISPLAY=wayland-0
XDG_SESSION_TYPE=wayland
$ ls -ld /run/user/1000 /run/user/1000/wayland-0
drwx------ 15 alice alice 580 Nov 1 00:15 /run/user/1000
srwxrwxr-x 1 alice alice 0 Nov 1 00:14 /run/user/1000/wayland-0
$ ls -l /tmp/.X11-unix
total 0
srwxrwxrwx 1 root root 0 Nov 1 00:08 X0
srwxrwxr-x 1 alice alice 0 Nov 1 00:14 X1
srwxrwxr-x 1 alice alice 0 Nov 1 00:14 X2
$ gnome-shell --version
GNOME Shell 48.7
The directory of your session is closed to the other accounts (drwx------) and the Wayland socket is inside it: an application of another account cannot reach it. It reaches your screen only through XWayland, which xhost opens.
What xhost changes. The commands are the same; the access goes through XWayland. A window of diwall-mcp opened with the DISPLAY of XWayland exists for your screen; with the sole WAYLAND_DISPLAY it does not open at all:
$ sleep 3
$ sudo -n -u diwall-mcp env DISPLAY=:1 zenity --info --title essai-b-x11 --text 'essai b' --timeout 8
MESA: error: Failed to attach to x11 shm
MESA: error: Failed to attach to x11 shm
$ xdotool search --name essai-b-x11
33554436
$ ps -o user=,comm= -p 15904
diwall-+ zenity
$ wait
$ sudo -n -u diwall-mcp ls /run/user/1000/wayland-0
ls: cannot access '/run/user/1000/wayland-0': Permission denied
$ sudo -n -u diwall-mcp env -u DISPLAY -u XAUTHORITY WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 GDK_BACKEND=wayland zenity --info --text 'essai b' --timeout 8
(zenity:16088): Gtk-WARNING **: 00:49:10.708: Failed to open display
Two details came out of these tries. The default rendering of zenity (GTK 4) under another account creates the window but leaves it invisible (MESA: error: Failed to attach to x11 shm); from version 0.0.2 the server starts zenity with GSK_RENDERER=cairo, and it prefers kdialog when that is installed. And sudo keeps the DISPLAY and XAUTHORITY of your account unless told otherwise: remove them to try Wayland alone, as above.
Route 7. At the normal level your account answers the confirmation window of diwall-mcp: the window is opened under diwall-mcp as the server opens it, then focused and answered with xdotool from your account, and the program receives grant.
$ sleep 3
$ sudo -n -u diwall-mcp env DISPLAY=:1 kdialog --title essai-reponse --radiolist 'Autoriser ?' refuse Refuser on grant Autoriser off
Detected locale "C" with character encoding "ANSI_X3.4-1968", which is not UTF-8.
Qt depends on a UTF-8 locale, and has switched to "C.UTF-8" instead.
If this causes problems, reconfigure your locale. See the locale(1) manual
for more information.
$ xdotool search --name essai-reponse
16777252
33554439
$ xdotool windowfocus 33554439
$ xdotool key Down
$ xdotool key Return
$ wait
$ cat /var/tmp/test/reponse.txt
grant
$ ls -l /dev/uinput
crw------- 1 root root 10, 223 Nov 1 00:47 /dev/uinput
At the uncompromising level agent reaches neither the Wayland socket nor the socket file of XWayland (Permission denied), and cannot use xdotool or /dev/uinput. The abstract socket of XWayland accepts the connection, so it is the authorization of XWayland that refuses agent: with no readable cookie and no xhost rule, nothing goes through. A rule opened to agent opens the screen, and the report says so:
$ sudo -iu agent xdotool getmouselocation
Authorization required, but no authorization protocol specified
Error: Can't open display: (null)
Failed creating new xdo instance
$ sudo -iu agent test -w /dev/uinput
$ sudo -iu agent head -c 1 /dev/input/event0
head: cannot open '/dev/input/event0' for reading: Permission denied
$ sudo -iu agent test -w ~/.bashrc
$ xhost +SI:localuser:agent
localuser:agent being added to access control list
$ sudo -iu agent mcp-server-diwall-browser-setup isolation
Essai des dix voies depuis le compte « agent » (uid 1002) ; le serveur tourne sous « diwall-mcp » (uid 1001).
Rien n'est écrit, aucun mot de passe n'est demandé, aucun sudo n'est lancé, aucun contenu du serveur n'est lu.
1. Lire le répertoire chiffré : fermée par le noyau
le noyau refuse ce compte au point de montage (/var/lib/diwall-mcp/Main) : le montage n'a pas d'accès pour un autre compte
2. Lire le profil du navigateur : fermée par le noyau
le noyau refuse ce compte au dossier du profil, qui est dans le volume
3. Piloter le navigateur par son port de débogage : fermée par le noyau
le noyau écarte la connexion au port 9222 (règle de pare-feu par compte)
4. Se faire passer pour le navigateur : fermée par le serveur
le serveur installé refuse un port tenu par un autre compte (debug_endpoint_other_account)
5. Changer ce que le serveur s'autorise : fermée par le noyau
le profil de l'opérateur, l'unité du service et le code du serveur ne s'écrivent pas depuis ce compte
6. Effacer ou réécrire sa trace dans le journal : fermée par le noyau
le noyau refuse ce compte au dossier du journal, qui est dans le volume
7. Répondre à la fenêtre de confirmation : ouverte
l'écran :1 accepte une connexion de ce compte par son socket abstrait
8. Lire la mémoire du serveur, du navigateur ou de gocryptfs : fermée par le noyau
le noyau refuse ce compte à la mémoire des 27 processus essayés de « diwall-mcp »
9. Capter un mot de passe que l'opérateur tape : fermée par le noyau
les fichiers de démarrage de « alice » et les événements du clavier sont fermés à ce compte
10. Devenir administrateur : fermée par le noyau
aucun groupe d'administration, aucune règle lisible à son nom, injection dans le terminal refusée ; une règle que seul root lit n'est pas visible d'ici
Niveau atteint : normal
$ sudo -iu agent xdotool getmouselocation
x:1010 y:368 screen:0 window:4194313
$ xhost -SI:localuser:agent
localuser:agent being removed from access control list
Routes 9 and 10. The same tries as on X11 give the same results. Your account writes into its startup files and cannot read the keyboard events; use_pty is present, legacy_tiocsti is 0, and your sudo rule makes the probe write its line:
$ ls -l /dev/input/event0
crw-rw---- 1 root input 13, 64 Nov 1 00:47 /dev/input/event0
$ head -c 1 /dev/input/event0
head: cannot open '/dev/input/event0' for reading: Permission denied
$ test -w ~/.bashrc
$ echo writable
writable
$ sudo -n grep -rn use_pty /etc/sudoers /etc/sudoers.d/
/etc/sudoers:15:Defaults use_pty
/etc/sudoers.d/kdesu-sudoers:4:Defaults!/usr/lib/*/libexec/kf5/kdesu_stub !use_pty
$ cat /proc/sys/dev/tty/legacy_tiocsti
0
$ sudo -n true
$ echo 'sudo without a password: yes'
sudo without a password: yes
$ sudo -n -u diwall-mcp sudo -n true
sudo: a password is required
From agent, the terminal is another one than yours, which is what use_pty gives, and sudo asks for a password:
$ sudo -iu agent tty
/dev/pts/1
$ tty
/dev/pts/0
$ sudo -iu agent sudo -n true
sudo: a password is required
Uncompromising level, with the model on a third account
What it closes: each of the ten routes, by the kernel except route 4, which the server closes. What it leaves open: none. Cost: the agent account; the client is started with sudo -iu agent, from your terminal, and use_pty of sudo prevents the client from injecting keystrokes into that terminal. Debian 13 ships Defaults use_pty in /etc/sudoers, and both workstations measured had it; on another system, check that this line is present, or the injection is not refused.
Step 1. Create the model account, the same way as diwall-mcp.
sudo adduser --disabled-password --gecos "" agent
sudo gpasswd -d agent users
id agent
usermod: no changes
Removing user agent from group users
uid=1001(agent) gid=1001(agent) groups=1001(agent)
Step 2. Hand the socket to the group of agent. A drop-in file adds the change, so that undoing it removes one file and leaves the unit of the normal level as it was.
sudo install -d /etc/systemd/system/mcp-server-diwall-browser.socket.d
printf '[Socket]\nSocketGroup=agent\n' | sudo tee /etc/systemd/system/mcp-server-diwall-browser.socket.d/uncompromising.conf
sudo systemctl daemon-reload
sudo systemctl restart mcp-server-diwall-browser.socket
stat -c '%A %U %G %n' /run/mcp-server-diwall-browser/mcp.sock
[Socket]
SocketGroup=agent
srw-rw---- root agent /run/mcp-server-diwall-browser/mcp.sock
Step 3. Your own account no longer opens the bridge; agent does.
socat - UNIX-CONNECT:/run/mcp-server-diwall-browser/mcp.sock < /dev/null 2>&1 | sed 's/^.*socat\[[0-9]*\] //'
{ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"status","arguments":{}}}'; sleep 6; } | sudo -u agent socat - UNIX-CONNECT:/run/mcp-server-diwall-browser/mcp.sock | tail -n 1 | grep -o 'encrypted_directory[^}]*}\|browser\\": {[^}]*}'
E connect(, AF=1 "/run/mcp-server-diwall-browser/mcp.sock", 41): Permission denied
encrypted_directory\": {\"state\": \"mounted\"}
browser\": {\"attached\": true, \"product\": \"Chrome\", \"version\": \"156.0.8078.73\"}
Step 4. Declare the bridge as the command of your MCP client, which runs under agent. In the declaration of the operator guide, section 2.1, the command is the line below and there is no environment block: the profile, the screen and the working directory are fixed by the unit.
{
"command": ["socat", "STDIO", "UNIX-CONNECT:/run/mcp-server-diwall-browser/mcp.sock"]
}
Step 5. Try the same three routes from agent, then read the report from agent.
sudo -iu agent bash -c 'exec 3<>/dev/tcp/127.0.0.1/9222'
sudo -iu agent ls /var/lib/diwall-mcp/Main
sudo -iu agent sudo -n true
sudo -iu agent mcp-server-diwall-browser-setup isolation
bash: connect: No route to host
bash: line 1: /dev/tcp/127.0.0.1/9222: No route to host
ls: cannot access '/var/lib/diwall-mcp/Main': Permission denied
sudo: a password is required
Essai des dix voies depuis le compte « agent » (uid 1001) ; le serveur tourne sous « diwall-mcp » (uid 1000).
Rien n'est écrit, aucun mot de passe n'est demandé, aucun sudo n'est lancé, aucun contenu du serveur n'est lu.
1. Lire le répertoire chiffré : fermée par le noyau
le noyau refuse ce compte au point de montage (/var/lib/diwall-mcp/Main) : le montage n'a pas d'accès pour un autre compte
2. Lire le profil du navigateur : fermée par le noyau
le noyau refuse ce compte au dossier du profil, qui est dans le volume
3. Piloter le navigateur par son port de débogage : fermée par le noyau
le noyau écarte la connexion au port 9222 (règle de pare-feu par compte)
4. Se faire passer pour le navigateur : fermée par le serveur
le serveur installé refuse un port tenu par un autre compte (debug_endpoint_other_account)
5. Changer ce que le serveur s'autorise : fermée par le noyau
le profil de l'opérateur, l'unité du service et le code du serveur ne s'écrivent pas depuis ce compte
6. Effacer ou réécrire sa trace dans le journal : fermée par le noyau
le noyau refuse ce compte au dossier du journal, qui est dans le volume
7. Répondre à la fenêtre de confirmation : fermée par le noyau
l'écran refuse ce compte (sans cookie lisible), et /dev/uinput lui est fermé (1 écran(s) essayé(s))
8. Lire la mémoire du serveur, du navigateur ou de gocryptfs : fermée par le noyau
le noyau refuse ce compte à la mémoire des 13 processus essayés de « diwall-mcp »
9. Capter un mot de passe que l'opérateur tape : fermée par le noyau
les fichiers de démarrage de « alice » et les événements du clavier sont fermés à ce compte
10. Devenir administrateur : fermée par le noyau
aucun groupe d'administration, aucune règle lisible à son nom, injection dans le terminal refusée ; une règle que seul root lit n'est pas visible d'ici
Niveau atteint : intransigeant
Going back to a less strict level
Undo in the reverse order. The commands remove the accounts and the units; the encrypted directory of diwall-mcp holds your browser sessions, and the last command of the second step deletes it with them: copy what you need first.
From uncompromising to normal. Remove the drop-in, then the model account with its home directory. Your own account opens the bridge again.
sudo rm /etc/systemd/system/mcp-server-diwall-browser.socket.d/uncompromising.conf
sudo rmdir /etc/systemd/system/mcp-server-diwall-browser.socket.d
sudo systemctl daemon-reload
sudo systemctl restart mcp-server-diwall-browser.socket
sudo deluser --remove-home agent
From normal to reckless. Withdraw the access to your screen, close the volume and the browser of diwall-mcp, remove the bridge, the firewall rule and the account, then the encrypted directory.
xhost -SI:localuser:diwall-mcp
sudo -iu diwall-mcp mcp-server-diwall-browser-setup close
sudo systemctl disable --now mcp-server-diwall-browser.socket
sudo rm /etc/systemd/system/mcp-server-diwall-browser.socket /etc/systemd/system/mcp-server-diwall-browser@.service
sudo systemctl daemon-reload
sudo rm /etc/nftables.d/diwall-mcp.nft
sudo rmdir /etc/nftables.d
sudo sed -i '\|^include "/etc/nftables.d/diwall-mcp.nft"$|d' /etc/nftables.conf
sudo systemctl reload nftables
sudo deluser --remove-home diwall-mcp
sudo rm -r /var/lib/diwall-mcp
localuser:diwall-mcp being removed from access control list
Fermeture du navigateur avant le démontage.
Volume fermé : /var/lib/diwall-mcp/Main.
Removed '/etc/systemd/system/sockets.target.wants/mcp-server-diwall-browser.socket'.
When a confirmation window has run under diwall-mcp, the last command can fail with userdel: user diwall-mcp is currently used by process …: the window left a session bus behind. Stop the processes of the account, wait a few seconds, remove the account again, then remove what stays: the socket file in /run (the unit is gone, the file is not removed with it) and, on a workstation where nftables.service is disabled, the table loaded by hand, which reload does not unload:
$ ps -u diwall-mcp -o pid,comm
PID COMMAND
15910 dbus-launch
15911 dbus-daemon
15949 xdg-desktop-por
15955 xdg-permission-
15961 xdg-document-po
15972 xdg-desktop-por
15977 gvfsd
15983 gvfsd-fuse
15999 at-spi-bus-laun
16005 dbus-daemon
16011 at-spi2-registr
$ sudo pkill -u diwall-mcp
$ sleep 5
$ ps -u diwall-mcp -o pid,comm
PID COMMAND
$ sudo deluser --remove-home diwall-mcp
$ ls -la /run/mcp-server-diwall-browser
total 0
drwxr-xr-x 2 root root 60 Nov 1 00:49 .
drwxr-xr-x 40 root root 960 Nov 1 00:49 ..
srw-rw---- 1 root alice 0 Nov 1 00:49 mcp.sock
$ sudo nft list ruleset
table inet diwall_mcp {
chain output {
type filter hook output priority filter; policy accept;
oifname "lo" tcp dport 9222 meta skuid != 1001 reject with icmpx admin-prohibited
}
}
$ sudo nft delete table inet diwall_mcp
$ sudo rm -r /run/mcp-server-diwall-browser
From reckless to nothing. Close your volume and remove the package. Your encrypted directory stays where create put it; delete it yourself if you do not need it.
mcp-server-diwall-browser-setup close
sudo apt purge mcp-server-diwall-browser
Le volume est déjà fermé.
REMOVING:
mcp-server-diwall-browser*
Summary:
Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 0
Freed space: 755 kB
Continue? [Y/n] y
Removing mcp-server-diwall-browser (0.0.2-1) ...
mcp-server-diwall-browser removed. Your operator profile and your encrypted directory were not touched.
Purging configuration files for mcp-server-diwall-browser (0.0.2-1) ...
mcp-server-diwall-browser removed. Your operator profile and your encrypted directory were not touched.
What no level promises
- A flaw of the kernel or of the server.
- A secret that you paste into a conversation.
- The use of a session that is open in the dedicated browser: the model can act in it, within the limits of the red list, at every level.
- What the provider of the model receives from the pages that the model reads. A local model removes that exposure; it does not remove an injection.
A level is measured at a given moment: a group added to an account, a sudoers rule or an xhost change lowers it. Run mcp-server-diwall-browser-setup isolation again after any change of the workstation.