Repository navigation
Description
Component: workshop snap, lxd-tunnel SDK / tunnel interface
Severity: High — lxc (and any LXD client) inside a workshop is completely unusable
Affected versions: workshop 0.9.4 and later (regression)
Introduced by: commit dbbca6369325e3d6cb0cee2db090ba7009909f1f — "Drop privileges for tunnels from workshop to host" (2026-07-09)
Environment
- Host: Ubuntu 26.04 LTS, kernel
7.0.0-28-generic(x86_64) - workshop: snap 0.9.4-ebb8bb7 (rev 386,
latest/stable,classic) - LXD: snap 6.9 (host daemon at
/var/snap/lxd/common/lxd/unix.socket) - Host user: uid 10000, primary gid 10000;
lxdgroup gid 114, membership supplementary only (getent group lxd→lxd:x:114:myuser)
Summary
Since workshop 0.9.4, a workshop that tunnels the host LXD socket into the container exposes the socket, but every connection is reset. Inside the container:
$ lxc list
Error: LXD unix socket "/var/lib/workshop/run/host-lxd/unix.socket" not accessible: Get "http://unix.socket/1.0": read unix @->/var/lib/workshop/run/host-lxd/unix.socket: read: connection reset by peer
curl through the same tunneled socket fails identically:
$ curl --unix-socket /var/lib/workshop/run/host-lxd/unix.socket http://unix.socket/1.0
curl: (56) Recv failure: Connection reset by peer
Direct access to the real host socket works fine:
$ curl --unix-socket /var/snap/lxd/common/lxd/unix.socket http://unix.socket/1.0
{"api_status":"stable", ...}
Steps to reproduce
1. Define a workshop with a tunnel interface to the host LXD socket
workshop.yaml (abridged — only the tunnel-relevant parts):
name: dev
base: [email protected]
sdks:
- name: system
slots:
lxd:
interface: tunnel
endpoint: /var/snap/lxd/common/lxd/unix.socket
# ... other SDKs (go, node, rust, opencode, vscode-remote ...)
connections:
- plug: lxd-tunnel:lxd
slot: system:lxdWhere lxd-tunnel is a project SDK whose plug attaches the tunnel to the container:
.workshop/lxd-tunnel/sdk.yaml:
name: lxd-tunnel
plugs:
lxd:
interface: tunnel
endpoint: /var/lib/workshop/run/host-lxd/unix.socket2. Launch the workshop
$ workshop launch dev
3. Try to use LXD from inside the container
$ workshop exec dev -- lxc list
Expected: list of host LXD instances/projects.
Actual:
Error: LXD unix socket "/var/lib/workshop/run/host-lxd/unix.socket" not accessible: Get "http://unix.socket/1.0": read unix @->/var/lib/workshop/run/host-lxd/unix.socket: read: connection reset by peer
The failure is on the first request — any LXD client (Go SDK, curl) is affected.
Root cause
The tunnel proxy device created for the workshop carries the host user's credentials as security.uid/security.gid:
$ lxc --project workshop.10000 profile show dev-6bc2a0cf-lxd-tunnel
devices:
lxd-tunnel_lxd:
bind: instance
connect: unix:/var/snap/lxd/common/lxd/unix.socket
gid: "1000"
listen: unix:/var/lib/workshop/run/host-lxd/unix.socket
security.gid: "10000"
security.uid: "10000"
type: proxy
uid: "1000"
LXD spawns forkproxy for the connect side of this bind: instance proxy. Observed on the host, that process runs as the host user with no supplementary groups:
$ ps -o user,pid,args -p <pid>
didier.+ ... /snap/lxd/current/sbin/lxd forkproxy -- ... unix:/var/lib/workshop/run/host-lxd/unix.socket ... unix:/var/snap/lxd/common/lxd/unix.socket 1000 1000 0644 10000 10000
$ grep -E "Uid|Gid|Groups" /proc/<pid>/status
Uid: 10000 10000 10000 10000
Gid: 10000 10000 10000 10000
Groups:
The host LXD socket is owned srw-rw---- root:lxd (0660):
$ ls -l /var/snap/lxd/common/lxd/unix.socket
srw-rw---- 1 root lxd 0 Aug 3 16:52 /var/snap/lxd/common/lxd/unix.socket
For this user, lxd access is a supplementary group. Once forkproxy's gid is the user's primary gid (10000), the supplementary lxd membership is gone, the connect to the 0660 root:lxd socket fails with EACCES, forkproxy aborts and closes the server-side connection, and the client — which has already written its request — sees connection reset by peer.
This is a direct consequence of the 0.9.4 hardening change, which adds exactly these two keys to every workshop→host tunnel device:
// internal/workshop/lxd/lxd_backend.go (proxyToLxdDevice)
case workshop.WorkshopToHost:
device["bind"] = "instance"
device["security.uid"] = usr.Uid
device["security.gid"] = usr.Gid// internal/interfaces/lxd_device/spec.go (addProxyEntry)
device["security.uid"] = s.User.Uid
device["security.gid"] = s.User.Gidusr/s.User come from os/user.Lookup (internal/osutil/user.go), i.e. the primary uid/gid from /etc/passwd.
Why it affects only some setups
-
Broken: host user whose primary gid is not the
lxdgroup gid — i.e. anyone added tolxdwithadduser $USER lxd(a supplementary group). This is the common case. -
Works by staleness: an unaffected machine, running the same workshop 0.9.4, has a tunnel device created before the upgrade that carries no
security.uid/security.gidkeys, soforkproxykeeps its previous (root) privileges and the tunnel works:$ lxc --project workshop.1993010624 profile show dev-ffa5204a-lxd-tunnel devices: lxd-tunnel_lxd: bind: instance connect: unix:/var/snap/lxd/common/lxd/unix.socket listen: unix:/var/lib/workshop/run/host-lxd/unix.socket type: proxyRecreating that workshop under 0.9.4 reproduces the bug.
Activity
jonathan-conder commentedon Aug 3, 2026
Sorry about this one. It was actually your previous bug report that made me realise we had a security issue here. Unfortunately that means we can't just roll back the fix.
I guess we can either make the GID configurable or deduce it from the socket path (after checking the user is actually in the group). Or maybe just get LXD to add the supplementary groups.
In the meantime, you could try
snap set lxd daemon.group=$(id -gn)as a workaround if you're on a single-user system.didrocks commentedon Aug 4, 2026
@jonathan-conder THanksf or the answer! Indeed, that was my guess and it’s not easy to get to the best approach for a generic one.
I tried though the workaround, but after setting it to my user (which is managed under authd) on my single user system, it does not work:
I think the issue is due to lxd trying to
chgrpinside the snap, but the authd group is not available from the snap (which is a bug in how the core snap integrates the PAM user and groups). This one will be fixed AFAIK but not in due time. Any other idea on possible workarounds while you fix the workshop side?didrocks commentedon Aug 4, 2026
For your information, my current workaround (so that it does not stay persistenly until you find some fix) is to chown the group of the lxd unix socket and restart forkproxy for the dev container.
jonathan-conder commentedon Aug 4, 2026
You could use ACLs instead of chown but probably also won't be persistent.
After connecting the tunnel, there should be a corresponding LXD profile. I forget the exact command but something like
lxc profile editshould allow you to set the security.gid option to the lxd group id instead of your user's primary id