Skip to content

LXD socket tunnel (tunnel interface) broken since 0.9.4: lxc inside a workshop fails with "connection reset by peer" #971

Open

Description

@didrocks

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: [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: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

$ 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.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.

Activity

  1. jonathan-conder commented on Aug 3, 2026

    @jonathan-conder
    Contributor

    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.

  2. didrocks commented on Aug 4, 2026

    @didrocks
    Author

    @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:

    $ lxc list
    Error: LXD unix socket "/var/snap/lxd/common/lxd-user/unix.socket" not accessible: Get "http://unix.socket/1.0": read unix @->/var/snap/lxd/common/lxd-user/unix.socket: read: connection reset by peer
    

    I think the issue is due to lxd trying to chgrp inside 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?

  3. didrocks commented on Aug 4, 2026

    @didrocks
    Author

    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.

  4. jonathan-conder commented on Aug 4, 2026

    @jonathan-conder
    Contributor

    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 edit should allow you to set the security.gid option to the lxd group id instead of your user's primary id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions