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;
lxd group 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: ubuntu@24.04
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:lxd
Where 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.socket
2. Launch the workshop
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.Gid
usr/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 lxd group gid — i.e. anyone added to lxd with adduser $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.gid keys, so forkproxy keeps 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: proxy
Recreating that workshop under 0.9.4 reproduces the bug.
Component: workshop snap,
lxd-tunnelSDK /tunnelinterfaceSeverity: High —
lxc(and any LXD client) inside a workshop is completely unusableAffected versions: workshop 0.9.4 and later (regression)
Introduced by: commit
dbbca6369325e3d6cb0cee2db090ba7009909f1f— "Drop privileges for tunnels from workshop to host" (2026-07-09)Environment
7.0.0-28-generic(x86_64)latest/stable,classic)/var/snap/lxd/common/lxd/unix.socket)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:
curlthrough the same tunneled socket fails identically:Direct access to the real host socket works fine:
Steps to reproduce
1. Define a workshop with a
tunnelinterface to the host LXD socketworkshop.yaml(abridged — only the tunnel-relevant parts):Where
lxd-tunnelis a project SDK whose plug attaches the tunnel to the container:.workshop/lxd-tunnel/sdk.yaml:2. Launch the workshop
3. Try to use LXD from inside the container
Expected: list of host LXD instances/projects.
Actual:
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:LXD spawns
forkproxyfor the connect side of thisbind: instanceproxy. Observed on the host, that process runs as the host user with no supplementary groups:The host LXD socket is owned
srw-rw---- root:lxd(0660):For this user,
lxdaccess is a supplementary group. Onceforkproxy's gid is the user's primary gid (10000), the supplementarylxdmembership is gone, the connect to the 0660root:lxdsocket fails withEACCES,forkproxyaborts and closes the server-side connection, and the client — which has already written its request — seesconnection 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:
usr/s.Usercome fromos/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:Recreating that workshop under 0.9.4 reproduces the bug.