NOTE: Updating answer to reflect change of strategy - instead of running Xephyr on a server/container, I am running it on host/main-use environment. The reason for that is that running Xephyr on the server/container is like putting a lock on an inside door instead of the front door - bad actors would go around the inside door and access the clipboard through the X pipe remote socket directly.
Facing the same problem of clipboard vulnerability, I approached it by running a Xephyr on the host (my personal workspace), and forwarding X from the server-on-container(s) to the local Xephyr.
The Xephyr is running on display ':2' while my personal workspace windows and browser are running on the default display ':0'. These two displays do not share a clipboard - each has it's own clipboard. That is the only way to prevent snooping of my personal workspace clipboard on display ':0'. Then I have hot keys set up (e.g., function keys), one to transfer the clipboard content from ':0' to ':2', and another for ':2' to ':0', so allowing complete control.
In shell script the code could look like
xsel --display :0 --clipboard -o | xsel --display :2 --clipboard -i
although I am using javascript as shown at the bottom of this post.
Shell script to start up the X forwarding could look something like this
Xephyr <args> :2 DISPLAY=:2 ssh -X -R 44713:localhost:4713 user@container <<EOF DISPLAY=:10 PULSE_SERVER=tcp:localhost:44713 openbox --startup firefox EOF
although I am using a javascript program to do it.
Here is the javascript code for copying between ':0' and ":2" which is mapped to by hot keys. You can see it pops up a transient message box to confirm it worked.
#!/usr/bin/env node `strict`; const child_process = require('child_process'); // TODO: write unviewable error messasges to system log function notifySend(title, msg){ title = title.replace(/"/g, '\\"'); msg = msg.replace(/"/g, '\\"'); //msg = msg.replace(/'/g, '\\'') try { child_process.execSync(`notify-send "${title}" "${msg}"`); } catch(e) { console.log(`system call "notify-send" failed with ${e.message}`); } } async function clipXfer(fromDispNum,toDispNum){ var clipValue; let cmd1 = `xsel --display :${fromDispNum} --clipboard --output`; let cmd2 = `xsel --display :${toDispNum} --clipboard --input`; try { clipValue = child_process.execSync(cmd1, { encoding: 'utf-8' }); } catch(e) { throw Error(`Display ${fromDispNum} clipboard is empty`); } await new Promise((resolve, reject)=>{ // eslint-disable-next-line no-unused-vars var proc = child_process.exec(cmd2, (error, stdout, stderr) => { //if (stdout) console.log(stdout); //if (stderr) console.log(stderr); if (error) { reject(Error(`${error.message}`)); } resolve(); }); if (!proc.stdin.write(clipValue)) reject(Error('clipToCont(), pipe write failed')); proc.stdin.end(); }); } async function main() { let argOff=2; if (process.argv.length-argOff<2) throw Error('clip-xfer requires two arguments: fromDispNum, toDispNum'); let fromDispNum = process.argv[argOff]; let toDispNum = process.argv[argOff+1]; argOff+=2; let f = (outcome)=>{ notifySend("clipXfer", `${fromDispNum} => ${toDispNum} ${outcome}`); }; await clipXfer(fromDispNum,toDispNum) .then(f('SUCCESS')) .catch((e)=>{f(`FAILURE, ${e.message}`); throw e;}); } //console.log(process.argv); //console.log(process.env); main() .then(()=>{process.exitCode=0;}) .catch((e)=>{ console.log(e); process.exitCode=1; });